@schalkneethling/toolkit 0.1.5 → 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.
@@ -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) || /\.\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) => /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)(?:\s|$)/.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) => /\bgit\s+push\b.*\s(?:--force\b|--force-with-lease\b|-f\b)/.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(c) &&
50
- /-R|--recursive|777/.test(c),
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) => /(?:>|>>|tee(?:\s+-[a-zA-Z]*)?)\s+\/(?:etc|boot|usr|bin|sbin)\//.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) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) || /\.\s*\|\s*\.\s*&/.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(c),
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(c),
87
- message: "`kill -9` is blocked — it prevents cleanup. Try SIGTERM (default) first.",
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: "`history -c` is blocked — erasing shell history hides what happened.",
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.1.5",
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": "^22.19.17",
25
+ "@types/node": "^25.6.0",
31
26
  "tsx": "^4.21.0",
32
- "typescript": "^5.9.3"
27
+ "typescript": "^6.0.2"
33
28
  },
34
- "packageManager": "pnpm@10.33.0"
35
- }
29
+ "scripts": {
30
+ "toolkit": "tsx src/index.ts",
31
+ "build:hooks": "tsc --project tsconfig.hooks.json"
32
+ }
33
+ }