@schalkneethling/toolkit 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +134 -0
- package/hooks/auto-approve-safe-commands/hook.mts +188 -0
- package/hooks/auto-approve-safe-commands/settings-fragment.json +17 -0
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +23 -10
- package/package.json +8 -10
package/dist/index.mjs
CHANGED
|
@@ -120,7 +120,7 @@ function addHook(name) {
|
|
|
120
120
|
const fragmentPath = join(srcDir, "settings-fragment.json");
|
|
121
121
|
const hooksDir = join(CLAUDE_DIR, "hooks");
|
|
122
122
|
mkdirSync(hooksDir, { recursive: true });
|
|
123
|
-
const destHook = join(hooksDir, `${name}.
|
|
123
|
+
const destHook = join(hooksDir, `${name}.mjs`);
|
|
124
124
|
writeFileSync(destHook, readFileSync(hookSrc));
|
|
125
125
|
if (existsSync(fragmentPath)) {
|
|
126
126
|
const fragment = JSON.parse(readFileSync(fragmentPath, "utf8"));
|
|
@@ -178,7 +178,7 @@ async function update(force) {
|
|
|
178
178
|
const srcDir = join(HOOKS_SRC, name);
|
|
179
179
|
if (!existsSync(srcDir)) continue;
|
|
180
180
|
const sourceHash = hashHookSource(name);
|
|
181
|
-
const installedPath = join(CLAUDE_DIR, "hooks", `${name}.
|
|
181
|
+
const installedPath = join(CLAUDE_DIR, "hooks", `${name}.mjs`);
|
|
182
182
|
const installedHash = existsSync(installedPath) ? shortHash(readFileSync(installedPath)) : null;
|
|
183
183
|
const sourceChanged = sourceHash !== entry.hash;
|
|
184
184
|
const locallyModified = installedHash !== null && installedHash !== entry.hash;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks and skills.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype Manifest = {\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\n readFileSync(MANIFEST_PATH, \"utf8\"),\n ) as Partial<Manifest>;\n return { hooks: parsed.hooks ?? {}, skills: parsed.skills ?? {} };\n } catch {\n return { hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction addHook(name: string): void {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = join(hooksDir, `${name}.ts`);\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath)\n ? JSON.parse(readFileSync(settingsPath, \"utf8\"))\n : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addSkill(name: string, links: string[]): void {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.ts`);\n const installedHash = existsSync(installedPath)\n ? shortHash(readFileSync(installedPath))\n : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified =\n installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath)\n ? readFileSync(installedPath, \"utf8\")\n : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\"): void {\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit update [--force]\n toolkit list hook\n toolkit list skill`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (command === \"list\" && (resource === \"hook\" || resource === \"skill\")) {\n list(resource);\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks and skills.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype Manifest = {\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\n readFileSync(MANIFEST_PATH, \"utf8\"),\n ) as Partial<Manifest>;\n return { hooks: parsed.hooks ?? {}, skills: parsed.skills ?? {} };\n } catch {\n return { hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction addHook(name: string): void {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = join(hooksDir, `${name}.mjs`);\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath)\n ? JSON.parse(readFileSync(settingsPath, \"utf8\"))\n : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addSkill(name: string, links: string[]): void {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath)\n ? shortHash(readFileSync(installedPath))\n : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified =\n installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath)\n ? readFileSync(installedPath, \"utf8\")\n : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\"): void {\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit update [--force]\n toolkit list hook\n toolkit list skill`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (command === \"list\" && (resource === \"hook\" || resource === \"skill\")) {\n list(resource);\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env -S node
|
|
2
|
+
/**
|
|
3
|
+
* auto-approve-safe-commands: PermissionRequest hook
|
|
4
|
+
*
|
|
5
|
+
* Reads a Claude Code hook payload from stdin, inspects tool_input.command,
|
|
6
|
+
* and allows safe commands that match known-safe patterns.
|
|
7
|
+
*
|
|
8
|
+
* Exit 0 = defers to permission prompt
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
// --- Safe command patterns ---
|
|
12
|
+
//
|
|
13
|
+
// Each entry is a pattern and a label for logging purposes.
|
|
14
|
+
// Order matters — more specific patterns should come before broader ones.
|
|
15
|
+
const SAFE_PATTERNS = [
|
|
16
|
+
// Test runners
|
|
17
|
+
{ pattern: /^npm\s+test\b/, label: "npm test" },
|
|
18
|
+
{ pattern: /^npx\s+vitest\b/, label: "vitest" },
|
|
19
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
20
|
+
{ pattern: /^pnpm\s+test\b/, label: "pnpm test" },
|
|
21
|
+
{ pattern: /^yarn\s+test\b/, label: "yarn test" },
|
|
22
|
+
{ pattern: /^bun\s+test\b/, label: "bun test" },
|
|
23
|
+
{ pattern: /^jest\b/, label: "jest" },
|
|
24
|
+
{ pattern: /^vitest\b/, label: "vitest (direct)" },
|
|
25
|
+
// Linting and formatting (analysis only, never destructive)
|
|
26
|
+
{ pattern: /^npm\s+run\s+lint\b/, label: "npm run lint" },
|
|
27
|
+
{ pattern: /^pnpm\s+run\s+lint\b/, label: "pnpm run lint" },
|
|
28
|
+
{ pattern: /^yarn\s+lint\b/, label: "yarn lint" },
|
|
29
|
+
{ pattern: /^bun\s+run\s+lint\b/, label: "bun run lint" },
|
|
30
|
+
{ pattern: /^eslint\b/, label: "eslint" },
|
|
31
|
+
{ pattern: /^prettier\s+--check\b/, label: "prettier --check" },
|
|
32
|
+
{ pattern: /^stylelint\b/, label: "stylelint" },
|
|
33
|
+
// Type checking
|
|
34
|
+
{ pattern: /^tsc\s+--noEmit\b/, label: "tsc --noEmit" },
|
|
35
|
+
{ pattern: /^npx\s+tsc\s+--noEmit\b/, label: "npx tsc --noEmit" },
|
|
36
|
+
{ pattern: /^npm\s+run\s+typecheck\b/, label: "npm run typecheck" },
|
|
37
|
+
{ pattern: /^pnpm\s+run\s+typecheck\b/, label: "pnpm run typecheck" },
|
|
38
|
+
{ pattern: /^bun\s+run\s+typecheck\b/, label: "bun run typecheck" },
|
|
39
|
+
// Build commands
|
|
40
|
+
{ pattern: /^npm\s+run\s+build\b/, label: "npm run build" },
|
|
41
|
+
{ pattern: /^pnpm\s+run\s+build\b/, label: "pnpm run build" },
|
|
42
|
+
{ pattern: /^yarn\s+build\b/, label: "yarn build" },
|
|
43
|
+
{ pattern: /^bun\s+run\s+build\b/, label: "bun run build" },
|
|
44
|
+
{ pattern: /^vite\s+build\b/, label: "vite build" },
|
|
45
|
+
{ pattern: /^tsc\b/, label: "tsc" },
|
|
46
|
+
// Vite+ (vp) commands - https://viteplus.dev/guide/#core-commands
|
|
47
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
48
|
+
{ pattern: /^vp\s+check\b/, label: "vp check" },
|
|
49
|
+
{ pattern: /^vp\s+lint\b/, label: "vp lint" },
|
|
50
|
+
{ pattern: /^vp\s+fmt\b/, label: "vp fmt" },
|
|
51
|
+
{ pattern: /^vp\s+build\b/, label: "vp build" },
|
|
52
|
+
{ pattern: /^vp\s+dev\b/, label: "vp dev" },
|
|
53
|
+
{ pattern: /^vp\s+preview\b/, label: "vp preview" },
|
|
54
|
+
{ pattern: /^vp\s+run\b/, label: "vp run" },
|
|
55
|
+
{ pattern: /^vp\s+outdated\b/, label: "vp outdated" },
|
|
56
|
+
{ pattern: /^vp\s+why\b/, label: "vp why" },
|
|
57
|
+
{ pattern: /^vp\s+info\b/, label: "vp info" },
|
|
58
|
+
{ pattern: /^vpx\b/, label: "vpx" },
|
|
59
|
+
// Dev server
|
|
60
|
+
{ pattern: /^npm\s+run\s+dev\b/, label: "npm run dev" },
|
|
61
|
+
{ pattern: /^pnpm\s+run\s+dev\b/, label: "pnpm run dev" },
|
|
62
|
+
{ pattern: /^yarn\s+dev\b/, label: "yarn dev" },
|
|
63
|
+
{ pattern: /^bun\s+run\s+dev\b/, label: "bun run dev" },
|
|
64
|
+
{ pattern: /^vite\b/, label: "vite" },
|
|
65
|
+
// Git read operations (no side effects)
|
|
66
|
+
{ pattern: /^git\s+status\b/, label: "git status" },
|
|
67
|
+
{ pattern: /^git\s+log\b/, label: "git log" },
|
|
68
|
+
{ pattern: /^git\s+diff\b/, label: "git diff" },
|
|
69
|
+
{ pattern: /^git\s+branch\b/, label: "git branch" },
|
|
70
|
+
{ pattern: /^git\s+show\b/, label: "git show" },
|
|
71
|
+
// Filesystem reads
|
|
72
|
+
{ pattern: /^cat\b/, label: "cat" },
|
|
73
|
+
{ pattern: /^ls\b/, label: "ls" },
|
|
74
|
+
{ pattern: /^find\b/, label: "find" },
|
|
75
|
+
{ pattern: /^grep\b/, label: "grep" },
|
|
76
|
+
{ pattern: /^rg\b/, label: "ripgrep" },
|
|
77
|
+
// Environment checks
|
|
78
|
+
{ pattern: /^node\s+--version\b/, label: "node --version" },
|
|
79
|
+
{ pattern: /^node\s+-v\b/, label: "node -v" },
|
|
80
|
+
{ pattern: /^bun\s+--version\b/, label: "bun --version" },
|
|
81
|
+
{ pattern: /^npm\s+--version\b/, label: "npm --version" },
|
|
82
|
+
{ pattern: /^git\s+--version\b/, label: "git --version" },
|
|
83
|
+
];
|
|
84
|
+
// --- Helpers ---
|
|
85
|
+
function approve() {
|
|
86
|
+
const output = {
|
|
87
|
+
hookSpecificOutput: {
|
|
88
|
+
hookEventName: "PermissionRequest",
|
|
89
|
+
decision: {
|
|
90
|
+
behavior: "allow",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
function defer() {
|
|
98
|
+
// Exit 0 with no output — falls through to the normal permission prompt
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
// --- Main ---
|
|
102
|
+
function main() {
|
|
103
|
+
const raw = readFileSync("/dev/stdin", "utf-8").trim();
|
|
104
|
+
let input;
|
|
105
|
+
try {
|
|
106
|
+
input = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
process.stderr.write(`[auto-approve-safe-commands] Failed to parse stdin JSON: ${raw}\n`);
|
|
110
|
+
defer();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Only handle Bash tool permission requests
|
|
114
|
+
if (input.tool_name !== "Bash") {
|
|
115
|
+
defer();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { command } = input.tool_input;
|
|
119
|
+
if (typeof command !== "string") {
|
|
120
|
+
defer();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const trimmed = command.trim();
|
|
124
|
+
for (const { pattern, label } of SAFE_PATTERNS) {
|
|
125
|
+
if (pattern.test(trimmed)) {
|
|
126
|
+
process.stderr.write(`[auto-approve-safe-commands] Auto-approved: ${label}\n`);
|
|
127
|
+
approve();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Not on the allowlist — fall through to normal permission prompt
|
|
132
|
+
defer();
|
|
133
|
+
}
|
|
134
|
+
main();
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env -S node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto-approve-safe-commands: PermissionRequest hook
|
|
5
|
+
*
|
|
6
|
+
* Reads a Claude Code hook payload from stdin, inspects tool_input.command,
|
|
7
|
+
* and allows safe commands that match known-safe patterns.
|
|
8
|
+
*
|
|
9
|
+
* Exit 0 = defers to permission prompt
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
// --- Types ---
|
|
15
|
+
|
|
16
|
+
interface BashToolInput {
|
|
17
|
+
command: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PermissionRequestInput {
|
|
21
|
+
hook_event_name: "PermissionRequest";
|
|
22
|
+
session_id: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
transcript_path: string;
|
|
25
|
+
tool_name: string;
|
|
26
|
+
tool_input: BashToolInput | Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ApproveOutput {
|
|
30
|
+
hookSpecificOutput: {
|
|
31
|
+
hookEventName: "PermissionRequest";
|
|
32
|
+
decision: {
|
|
33
|
+
behavior: "allow";
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Safe command patterns ---
|
|
39
|
+
//
|
|
40
|
+
// Each entry is a pattern and a label for logging purposes.
|
|
41
|
+
// Order matters — more specific patterns should come before broader ones.
|
|
42
|
+
|
|
43
|
+
const SAFE_PATTERNS: { pattern: RegExp; label: string }[] = [
|
|
44
|
+
// Test runners
|
|
45
|
+
{ pattern: /^npm\s+test\b/, label: "npm test" },
|
|
46
|
+
{ pattern: /^npx\s+vitest\b/, label: "vitest" },
|
|
47
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
48
|
+
{ pattern: /^pnpm\s+test\b/, label: "pnpm test" },
|
|
49
|
+
{ pattern: /^yarn\s+test\b/, label: "yarn test" },
|
|
50
|
+
{ pattern: /^bun\s+test\b/, label: "bun test" },
|
|
51
|
+
{ pattern: /^jest\b/, label: "jest" },
|
|
52
|
+
{ pattern: /^vitest\b/, label: "vitest (direct)" },
|
|
53
|
+
|
|
54
|
+
// Linting and formatting (analysis only, never destructive)
|
|
55
|
+
{ pattern: /^npm\s+run\s+lint\b/, label: "npm run lint" },
|
|
56
|
+
{ pattern: /^pnpm\s+run\s+lint\b/, label: "pnpm run lint" },
|
|
57
|
+
{ pattern: /^yarn\s+lint\b/, label: "yarn lint" },
|
|
58
|
+
{ pattern: /^bun\s+run\s+lint\b/, label: "bun run lint" },
|
|
59
|
+
{ pattern: /^eslint\b/, label: "eslint" },
|
|
60
|
+
{ pattern: /^prettier\s+--check\b/, label: "prettier --check" },
|
|
61
|
+
{ pattern: /^stylelint\b/, label: "stylelint" },
|
|
62
|
+
|
|
63
|
+
// Type checking
|
|
64
|
+
{ pattern: /^tsc\s+--noEmit\b/, label: "tsc --noEmit" },
|
|
65
|
+
{ pattern: /^npx\s+tsc\s+--noEmit\b/, label: "npx tsc --noEmit" },
|
|
66
|
+
{ pattern: /^npm\s+run\s+typecheck\b/, label: "npm run typecheck" },
|
|
67
|
+
{ pattern: /^pnpm\s+run\s+typecheck\b/, label: "pnpm run typecheck" },
|
|
68
|
+
{ pattern: /^bun\s+run\s+typecheck\b/, label: "bun run typecheck" },
|
|
69
|
+
|
|
70
|
+
// Build commands
|
|
71
|
+
{ pattern: /^npm\s+run\s+build\b/, label: "npm run build" },
|
|
72
|
+
{ pattern: /^pnpm\s+run\s+build\b/, label: "pnpm run build" },
|
|
73
|
+
{ pattern: /^yarn\s+build\b/, label: "yarn build" },
|
|
74
|
+
{ pattern: /^bun\s+run\s+build\b/, label: "bun run build" },
|
|
75
|
+
{ pattern: /^vite\s+build\b/, label: "vite build" },
|
|
76
|
+
{ pattern: /^tsc\b/, label: "tsc" },
|
|
77
|
+
|
|
78
|
+
// Vite+ (vp) commands - https://viteplus.dev/guide/#core-commands
|
|
79
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
80
|
+
{ pattern: /^vp\s+check\b/, label: "vp check" },
|
|
81
|
+
{ pattern: /^vp\s+lint\b/, label: "vp lint" },
|
|
82
|
+
{ pattern: /^vp\s+fmt\b/, label: "vp fmt" },
|
|
83
|
+
{ pattern: /^vp\s+build\b/, label: "vp build" },
|
|
84
|
+
{ pattern: /^vp\s+dev\b/, label: "vp dev" },
|
|
85
|
+
{ pattern: /^vp\s+preview\b/, label: "vp preview" },
|
|
86
|
+
{ pattern: /^vp\s+run\b/, label: "vp run" },
|
|
87
|
+
{ pattern: /^vp\s+outdated\b/, label: "vp outdated" },
|
|
88
|
+
{ pattern: /^vp\s+why\b/, label: "vp why" },
|
|
89
|
+
{ pattern: /^vp\s+info\b/, label: "vp info" },
|
|
90
|
+
{ pattern: /^vpx\b/, label: "vpx" },
|
|
91
|
+
|
|
92
|
+
// Dev server
|
|
93
|
+
{ pattern: /^npm\s+run\s+dev\b/, label: "npm run dev" },
|
|
94
|
+
{ pattern: /^pnpm\s+run\s+dev\b/, label: "pnpm run dev" },
|
|
95
|
+
{ pattern: /^yarn\s+dev\b/, label: "yarn dev" },
|
|
96
|
+
{ pattern: /^bun\s+run\s+dev\b/, label: "bun run dev" },
|
|
97
|
+
{ pattern: /^vite\b/, label: "vite" },
|
|
98
|
+
|
|
99
|
+
// Git read operations (no side effects)
|
|
100
|
+
{ pattern: /^git\s+status\b/, label: "git status" },
|
|
101
|
+
{ pattern: /^git\s+log\b/, label: "git log" },
|
|
102
|
+
{ pattern: /^git\s+diff\b/, label: "git diff" },
|
|
103
|
+
{ pattern: /^git\s+branch\b/, label: "git branch" },
|
|
104
|
+
{ pattern: /^git\s+show\b/, label: "git show" },
|
|
105
|
+
|
|
106
|
+
// Filesystem reads
|
|
107
|
+
{ pattern: /^cat\b/, label: "cat" },
|
|
108
|
+
{ pattern: /^ls\b/, label: "ls" },
|
|
109
|
+
{ pattern: /^find\b/, label: "find" },
|
|
110
|
+
{ pattern: /^grep\b/, label: "grep" },
|
|
111
|
+
{ pattern: /^rg\b/, label: "ripgrep" },
|
|
112
|
+
|
|
113
|
+
// Environment checks
|
|
114
|
+
{ pattern: /^node\s+--version\b/, label: "node --version" },
|
|
115
|
+
{ pattern: /^node\s+-v\b/, label: "node -v" },
|
|
116
|
+
{ pattern: /^bun\s+--version\b/, label: "bun --version" },
|
|
117
|
+
{ pattern: /^npm\s+--version\b/, label: "npm --version" },
|
|
118
|
+
{ pattern: /^git\s+--version\b/, label: "git --version" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// --- Helpers ---
|
|
122
|
+
|
|
123
|
+
function approve(): void {
|
|
124
|
+
const output: ApproveOutput = {
|
|
125
|
+
hookSpecificOutput: {
|
|
126
|
+
hookEventName: "PermissionRequest",
|
|
127
|
+
decision: {
|
|
128
|
+
behavior: "allow",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defer(): void {
|
|
138
|
+
// Exit 0 with no output — falls through to the normal permission prompt
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Main ---
|
|
143
|
+
|
|
144
|
+
function main(): void {
|
|
145
|
+
const raw = readFileSync("/dev/stdin", "utf-8").trim();
|
|
146
|
+
|
|
147
|
+
let input: PermissionRequestInput;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
input = JSON.parse(raw) as PermissionRequestInput;
|
|
151
|
+
} catch {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`[auto-approve-safe-commands] Failed to parse stdin JSON: ${raw}\n`,
|
|
154
|
+
);
|
|
155
|
+
defer();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Only handle Bash tool permission requests
|
|
160
|
+
if (input.tool_name !== "Bash") {
|
|
161
|
+
defer();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { command } = input.tool_input as BashToolInput;
|
|
166
|
+
|
|
167
|
+
if (typeof command !== "string") {
|
|
168
|
+
defer();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const trimmed = command.trim();
|
|
173
|
+
|
|
174
|
+
for (const { pattern, label } of SAFE_PATTERNS) {
|
|
175
|
+
if (pattern.test(trimmed)) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`[auto-approve-safe-commands] Auto-approved: ${label}\n`,
|
|
178
|
+
);
|
|
179
|
+
approve();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Not on the allowlist — fall through to normal permission prompt
|
|
185
|
+
defer();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PermissionRequest": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node .claude/hooks/auto-approve-safe-commands.mjs",
|
|
10
|
+
"timeout": 5,
|
|
11
|
+
"statusMessage": "Checking if command can be auto-approved..."
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -31,8 +31,7 @@ const rules = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
id: "chmod-777",
|
|
34
|
-
test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) &&
|
|
35
|
-
/-R|--recursive|777/.test(c),
|
|
34
|
+
test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) && /-R|--recursive|777/.test(c),
|
|
36
35
|
message: "`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
|
|
37
36
|
},
|
|
38
37
|
{
|
|
@@ -47,7 +46,8 @@ const rules = [
|
|
|
47
46
|
},
|
|
48
47
|
{
|
|
49
48
|
id: "fork-bomb",
|
|
50
|
-
test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
49
|
+
test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
50
|
+
/\.\s*\|\s*\.\s*&/.test(c),
|
|
51
51
|
message: "Fork bomb pattern detected and blocked.",
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* block-dangerous-commands: PreToolUse hook for Bash.
|
|
4
5
|
*
|
|
@@ -18,13 +19,15 @@ type Rule = {
|
|
|
18
19
|
const rules: Rule[] = [
|
|
19
20
|
{
|
|
20
21
|
id: "rm-rf",
|
|
21
|
-
test: (c) =>
|
|
22
|
+
test: (c) =>
|
|
23
|
+
/\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)(?:\s|$)/.test(c),
|
|
22
24
|
message:
|
|
23
25
|
"`rm -rf` (and flag variants) is blocked. Delete specific paths with a non-recursive `rm`, or move them to a backup location.",
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
id: "git-push-force",
|
|
27
|
-
test: (c) =>
|
|
29
|
+
test: (c) =>
|
|
30
|
+
/\bgit\s+push\b.*\s(?:--force\b|--force-with-lease\b|-f\b)/.test(c),
|
|
28
31
|
message:
|
|
29
32
|
"`git push --force` is blocked. Use `--force-with-lease` only after coordinating with collaborators, or create a new branch.",
|
|
30
33
|
},
|
|
@@ -46,8 +49,9 @@ const rules: Rule[] = [
|
|
|
46
49
|
{
|
|
47
50
|
id: "chmod-777",
|
|
48
51
|
test: (c) =>
|
|
49
|
-
/\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(
|
|
50
|
-
|
|
52
|
+
/\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(
|
|
53
|
+
c,
|
|
54
|
+
) && /-R|--recursive|777/.test(c),
|
|
51
55
|
message:
|
|
52
56
|
"`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
|
|
53
57
|
},
|
|
@@ -58,19 +62,24 @@ const rules: Rule[] = [
|
|
|
58
62
|
},
|
|
59
63
|
{
|
|
60
64
|
id: "system-redirect",
|
|
61
|
-
test: (c) =>
|
|
65
|
+
test: (c) =>
|
|
66
|
+
/(?:>|>>|tee(?:\s+-[a-zA-Z]*)?)\s+\/(?:etc|boot|usr|bin|sbin)\//.test(c),
|
|
62
67
|
message:
|
|
63
68
|
"Writing into /etc, /boot, /usr, /bin, or /sbin is blocked. These are system directories; use a user-writable path.",
|
|
64
69
|
},
|
|
65
70
|
{
|
|
66
71
|
id: "fork-bomb",
|
|
67
|
-
test: (c) =>
|
|
72
|
+
test: (c) =>
|
|
73
|
+
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
74
|
+
/\.\s*\|\s*\.\s*&/.test(c),
|
|
68
75
|
message: "Fork bomb pattern detected and blocked.",
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
id: "curl-pipe-shell",
|
|
72
79
|
test: (c) =>
|
|
73
|
-
/\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(
|
|
80
|
+
/\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(
|
|
81
|
+
c,
|
|
82
|
+
),
|
|
74
83
|
message:
|
|
75
84
|
"Piping remote content directly into a shell is blocked. Download the script, inspect it, then run it.",
|
|
76
85
|
},
|
|
@@ -83,8 +92,11 @@ const rules: Rule[] = [
|
|
|
83
92
|
{
|
|
84
93
|
id: "kill-9",
|
|
85
94
|
test: (c) =>
|
|
86
|
-
/\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(
|
|
87
|
-
|
|
95
|
+
/\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(
|
|
96
|
+
c,
|
|
97
|
+
),
|
|
98
|
+
message:
|
|
99
|
+
"`kill -9` is blocked — it prevents cleanup. Try SIGTERM (default) first.",
|
|
88
100
|
},
|
|
89
101
|
{
|
|
90
102
|
id: "npm-publish",
|
|
@@ -95,7 +107,8 @@ const rules: Rule[] = [
|
|
|
95
107
|
{
|
|
96
108
|
id: "history-clear",
|
|
97
109
|
test: (c) => /\bhistory\s+-c\b/.test(c),
|
|
98
|
-
message:
|
|
110
|
+
message:
|
|
111
|
+
"`history -c` is blocked — erasing shell history hides what happened.",
|
|
99
112
|
},
|
|
100
113
|
];
|
|
101
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for managing Claude Code hooks and skills across projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -18,18 +18,16 @@
|
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"toolkit": "tsx src/index.ts",
|
|
23
|
-
"prepare": "vp pack && vp run build:hooks",
|
|
24
|
-
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
25
|
-
},
|
|
26
21
|
"dependencies": {
|
|
27
22
|
"vite-plus": "^0.1.18"
|
|
28
23
|
},
|
|
29
24
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^
|
|
25
|
+
"@types/node": "^25.6.0",
|
|
31
26
|
"tsx": "^4.21.0",
|
|
32
|
-
"typescript": "^
|
|
27
|
+
"typescript": "^6.0.2"
|
|
33
28
|
},
|
|
34
|
-
"
|
|
35
|
-
|
|
29
|
+
"scripts": {
|
|
30
|
+
"toolkit": "tsx src/index.ts",
|
|
31
|
+
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
32
|
+
}
|
|
33
|
+
}
|