start-vibing 2.0.21 → 2.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing",
3
- "version": "2.0.21",
3
+ "version": "2.0.23",
4
4
  "description": "Setup Claude Code agents, skills, and hooks in your project. Smart copy that preserves your custom domains and configurations.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,11 +67,17 @@ function checkRuntime(cmd: string): boolean {
67
67
  }
68
68
  }
69
69
 
70
+ interface RuntimeResult {
71
+ exitCode: number;
72
+ output: string;
73
+ error?: string;
74
+ }
75
+
70
76
  function runWithRuntime(
71
77
  cmd: string,
72
78
  args: string[],
73
79
  input: string
74
- ): { success: boolean; output: string; error?: string } {
80
+ ): RuntimeResult {
75
81
  try {
76
82
  const result = spawnSync(cmd, args, {
77
83
  input,
@@ -83,13 +89,13 @@ function runWithRuntime(
83
89
  });
84
90
 
85
91
  return {
86
- success: result.status === 0,
92
+ exitCode: result.status ?? 1,
87
93
  output: result.stdout?.toString() || '',
88
94
  error: result.stderr?.toString() || undefined,
89
95
  };
90
96
  } catch (err) {
91
97
  return {
92
- success: false,
98
+ exitCode: 1,
93
99
  output: '',
94
100
  error: err instanceof Error ? err.message : 'Unknown error',
95
101
  };
@@ -98,19 +104,16 @@ function runWithRuntime(
98
104
 
99
105
  async function runHook(hookName: string, stdinData: string): Promise<void> {
100
106
  const tsPath = join(HOOKS_DIR, `${hookName}.ts`);
101
- const pyPath = join(HOOKS_DIR, `${hookName}.py`);
102
107
 
103
108
  // Runtime detection order - TypeScript ONLY (source of truth)
104
109
  // Python files are deprecated and should be removed
105
- const runtimes: Array<{ name: string; cmd: string; ext: string }> = [
106
- { name: 'bun', cmd: 'bun', ext: '.ts' },
107
- { name: 'npx-tsx', cmd: 'npx tsx', ext: '.ts' },
110
+ const runtimes: Array<{ name: string; cmd: string }> = [
111
+ { name: 'bun', cmd: 'bun' },
112
+ { name: 'npx-tsx', cmd: 'npx tsx' },
108
113
  ];
109
114
 
110
115
  for (const runtime of runtimes) {
111
- const hookPath = runtime.ext === '.ts' ? tsPath : pyPath;
112
-
113
- if (!existsSync(hookPath)) {
116
+ if (!existsSync(tsPath)) {
114
117
  continue;
115
118
  }
116
119
 
@@ -118,17 +121,38 @@ async function runHook(hookName: string, stdinData: string): Promise<void> {
118
121
  continue;
119
122
  }
120
123
 
121
- const result = runWithRuntime(runtime.cmd, [hookPath], stdinData);
124
+ const result = runWithRuntime(runtime.cmd, [tsPath], stdinData);
122
125
 
123
- if (result.success || !result.error?.includes('not found')) {
124
- // Runtime worked (success or runtime-specific failure)
126
+ // Handle exit codes according to Claude Code hook specification:
127
+ // - Exit code 0: Success (stdout in transcript)
128
+ // - Exit code 2: Blocking error (stderr feeds back to Claude)
129
+ // - Other: Non-blocking error
130
+
131
+ if (result.exitCode === 0) {
132
+ // Success - output stdout
133
+ process.stdout.write(result.output);
134
+ process.exit(0);
135
+ } else if (result.exitCode === 2) {
136
+ // Blocking error - for Stop hooks, JSON is in stdout
137
+ // Pass through both stdout (JSON response) and stderr (debug logs)
138
+ process.stdout.write(result.output);
139
+ if (result.error) {
140
+ process.stderr.write(result.error);
141
+ }
142
+ process.exit(2);
143
+ } else {
144
+ // Non-blocking error or runtime not found
145
+ if (result.error?.includes('not found')) {
146
+ // Runtime not available, try next
147
+ continue;
148
+ }
149
+ // Hook failed but not blocking
125
150
  process.stdout.write(result.output);
126
- if (result.error && !result.success) {
151
+ if (result.error) {
127
152
  process.stderr.write(result.error);
128
153
  }
129
- process.exit(result.success ? 0 : 1);
154
+ process.exit(result.exitCode);
130
155
  }
131
- // Runtime not available, try next
132
156
  }
133
157
 
134
158
  // No runtime available - return safe default
@@ -936,6 +936,64 @@ interface HookResult {
936
936
  reason: string;
937
937
  }
938
938
 
939
+ /**
940
+ * Maps error types to the subagent that should be launched to fix them.
941
+ */
942
+ const ERROR_TO_SUBAGENT: Record<string, { agent: string; prompt: string }> = {
943
+ FEATURE_BRANCH_NOT_MERGED: {
944
+ agent: 'commit-manager',
945
+ prompt: 'Complete the git workflow: commit all changes, merge to main, sync with remote',
946
+ },
947
+ NOT_ON_MAIN_BRANCH: {
948
+ agent: 'commit-manager',
949
+ prompt: 'Merge current branch to main and checkout main',
950
+ },
951
+ DIRECT_MAIN_COMMIT_FORBIDDEN: {
952
+ agent: 'branch-manager',
953
+ prompt: 'Create a feature branch from the current changes on main',
954
+ },
955
+ GIT_TREE_NOT_CLEAN: {
956
+ agent: 'commit-manager',
957
+ prompt: 'Commit all pending changes with appropriate conventional commit message',
958
+ },
959
+ CLAUDE_MD_MISSING: {
960
+ agent: 'documenter',
961
+ prompt: 'Create CLAUDE.md with required sections: Last Change, 30s Overview, Stack, Architecture',
962
+ },
963
+ CLAUDE_MD_SIZE_EXCEEDED: {
964
+ agent: 'claude-md-compactor',
965
+ prompt: 'Compact CLAUDE.md to under 40k characters while preserving critical project knowledge',
966
+ },
967
+ CLAUDE_MD_TEMPLATE_MERGE_NEEDED: {
968
+ agent: 'documenter',
969
+ prompt: 'Merge existing CLAUDE.md with template in .claude/CLAUDE.template.md, then delete template',
970
+ },
971
+ CLAUDE_MD_MISSING_SECTIONS: {
972
+ agent: 'documenter',
973
+ prompt: 'Add missing required sections to CLAUDE.md',
974
+ },
975
+ CLAUDE_MD_LAST_CHANGE_EMPTY: {
976
+ agent: 'documenter',
977
+ prompt: 'Update the Last Change section in CLAUDE.md with current session info',
978
+ },
979
+ CLAUDE_MD_STACKED_CHANGES: {
980
+ agent: 'documenter',
981
+ prompt: 'Remove stacked Last Change sections in CLAUDE.md, keep only the latest',
982
+ },
983
+ CLAUDE_MD_NOT_UPDATED: {
984
+ agent: 'documenter',
985
+ prompt: 'Update CLAUDE.md Last Change section with current session summary',
986
+ },
987
+ SOURCE_FILES_NOT_DOCUMENTED: {
988
+ agent: 'documenter',
989
+ prompt: 'Update documentation for all modified source files',
990
+ },
991
+ DOMAIN_DOCUMENTATION_INCOMPLETE: {
992
+ agent: 'documenter',
993
+ prompt: 'Update domain documentation for all modified files in .claude/skills/codebase-knowledge/domains/',
994
+ },
995
+ };
996
+
939
997
  async function readStdinWithTimeout(timeoutMs: number): Promise<string> {
940
998
  return new Promise((resolve) => {
941
999
  const timeout = setTimeout(() => {
@@ -964,21 +1022,75 @@ async function readStdinWithTimeout(timeoutMs: number): Promise<string> {
964
1022
  });
965
1023
  }
966
1024
 
1025
+ /**
1026
+ * Run validations in priority order and return the FIRST error found.
1027
+ * This ensures Claude fixes one issue at a time in the correct sequence.
1028
+ */
1029
+ function runValidationsInOrder(
1030
+ currentBranch: string,
1031
+ modifiedFiles: string[],
1032
+ sourceFiles: string[],
1033
+ isMainBranch: boolean,
1034
+ isCleanTree: boolean
1035
+ ): ValidationError | null {
1036
+ // PRIORITY ORDER - most critical first, one at a time
1037
+ // Each validation returns early on first error
1038
+
1039
+ // 1. Branch validation - can't complete on feature branch
1040
+ const branchError = validateBranch(currentBranch, modifiedFiles);
1041
+ if (branchError) return branchError;
1042
+
1043
+ // 2. Git tree - must be clean (only check if on main)
1044
+ if (isMainBranch) {
1045
+ const treeError = validateGitTree(modifiedFiles);
1046
+ if (treeError) return treeError;
1047
+ }
1048
+
1049
+ // 3. CLAUDE.md must exist
1050
+ const claudeMdExistsError = validateClaudeMdExists();
1051
+ if (claudeMdExistsError) return claudeMdExistsError;
1052
+
1053
+ // 4. Check if template merge is needed
1054
+ const templateMergeError = validateClaudeMdTemplateMerge();
1055
+ if (templateMergeError) return templateMergeError;
1056
+
1057
+ // 5. CLAUDE.md size - must be under 40k
1058
+ const sizeError = validateClaudeMdSize();
1059
+ if (sizeError) return sizeError;
1060
+
1061
+ // 6. CLAUDE.md structure - must have required sections
1062
+ const structureError = validateClaudeMdStructure();
1063
+ if (structureError) return structureError;
1064
+
1065
+ // 7. Last Change must not be stacked
1066
+ const lastChangeError = validateClaudeMdLastChange();
1067
+ if (lastChangeError) return lastChangeError;
1068
+
1069
+ // 8. CLAUDE.md must be updated if files changed
1070
+ const updatedError = validateClaudeMdUpdated(modifiedFiles);
1071
+ if (updatedError) return updatedError;
1072
+
1073
+ // 9. Source files must be documented
1074
+ const docError = validateDocumentation(sourceFiles);
1075
+ if (docError) return docError;
1076
+
1077
+ // 10. Domain documentation must be complete
1078
+ const domainDocError = validateDomainDocumentation(modifiedFiles);
1079
+ if (domainDocError) return domainDocError;
1080
+
1081
+ return null; // All validations passed
1082
+ }
1083
+
967
1084
  async function main(): Promise<void> {
968
- // Debug logging - always output to stderr for visibility
969
- console.error('[stop-validator] ========================================');
1085
+ // Debug logging - writes to stderr (visible in claude debug mode)
970
1086
  console.error('[stop-validator] Starting validation...');
971
- console.error(`[stop-validator] CWD: ${process.cwd()}`);
972
1087
  console.error(`[stop-validator] PROJECT_DIR: ${PROJECT_DIR}`);
973
- console.error(`[stop-validator] CLAUDE_MD exists: ${existsSync(CLAUDE_MD_PATH)}`);
974
1088
 
975
1089
  let hookInput: HookInput = {};
976
1090
  try {
977
1091
  const stdin = await readStdinWithTimeout(1000);
978
- console.error(`[stop-validator] stdin received: ${stdin.length} chars`);
979
1092
  if (stdin && stdin.trim()) {
980
1093
  hookInput = JSON.parse(stdin);
981
- console.error(`[stop-validator] Parsed input keys: ${Object.keys(hookInput).join(', ') || 'none'}`);
982
1094
  }
983
1095
  } catch {
984
1096
  hookInput = {};
@@ -1001,101 +1113,53 @@ async function main(): Promise<void> {
1001
1113
  const isMainBranch = currentBranch === 'main' || currentBranch === 'master';
1002
1114
  const isCleanTree = modifiedFiles.length === 0;
1003
1115
 
1004
- // Run all validations
1005
- console.error('[stop-validator] Running validations...');
1006
- console.error(`[stop-validator] Branch: ${currentBranch}, isMain: ${isMainBranch}`);
1007
- console.error(`[stop-validator] Modified files: ${modifiedFiles.length}`);
1008
- console.error(`[stop-validator] Source files: ${sourceFiles.length}`);
1009
-
1010
- const errors: ValidationError[] = [];
1011
-
1012
- // Validation order matters - most critical first
1013
- const branchError = validateBranch(currentBranch, modifiedFiles);
1014
- if (branchError) errors.push(branchError);
1015
-
1016
- // Only check these if we're close to completion (on main or clean tree)
1017
- if (isMainBranch || isCleanTree) {
1018
- const treeError = validateGitTree(modifiedFiles);
1019
- if (treeError) errors.push(treeError);
1020
- }
1021
-
1022
- const claudeMdExistsError = validateClaudeMdExists();
1023
- if (claudeMdExistsError) errors.push(claudeMdExistsError);
1024
-
1025
- if (!claudeMdExistsError) {
1026
- // Check if there's a template pending merge (from start-vibing install)
1027
- const templateMergeError = validateClaudeMdTemplateMerge();
1028
- if (templateMergeError) errors.push(templateMergeError);
1029
-
1030
- const sizeError = validateClaudeMdSize();
1031
- if (sizeError) errors.push(sizeError);
1032
-
1033
- const structureError = validateClaudeMdStructure();
1034
- if (structureError) errors.push(structureError);
1035
-
1036
- const lastChangeError = validateClaudeMdLastChange();
1037
- if (lastChangeError) errors.push(lastChangeError);
1038
-
1039
- const updatedError = validateClaudeMdUpdated(modifiedFiles);
1040
- if (updatedError) errors.push(updatedError);
1041
- }
1116
+ console.error(`[stop-validator] Branch: ${currentBranch}, Modified: ${modifiedFiles.length}`);
1042
1117
 
1043
- const docError = validateDocumentation(sourceFiles);
1044
- if (docError) errors.push(docError);
1045
-
1046
- const domainDocError = validateDomainDocumentation(modifiedFiles);
1047
- if (domainDocError) errors.push(domainDocError);
1118
+ // Run validations in order - get FIRST error only
1119
+ const error = runValidationsInOrder(
1120
+ currentBranch,
1121
+ modifiedFiles,
1122
+ sourceFiles,
1123
+ isMainBranch,
1124
+ isCleanTree
1125
+ );
1048
1126
 
1049
1127
  // ============================================================================
1050
- // OUTPUT RESULTS
1128
+ // OUTPUT RESULTS - ONE ERROR AT A TIME
1051
1129
  // ============================================================================
1052
1130
 
1053
- console.error(`[stop-validator] Validation complete. Errors found: ${errors.length}`);
1054
- if (errors.length > 0) {
1055
- console.error(`[stop-validator] Error types: ${errors.map((e) => e.type).join(', ')}`);
1056
- }
1131
+ if (error) {
1132
+ const subagentInfo = ERROR_TO_SUBAGENT[error.type] || {
1133
+ agent: 'general-purpose',
1134
+ prompt: 'Fix the validation error',
1135
+ };
1057
1136
 
1058
- if (errors.length > 0) {
1059
- let output = `
1060
- ################################################################################
1061
- # STOP VALIDATOR - TASK COMPLETION BLOCKED #
1062
- ################################################################################
1137
+ const blockReason = `
1138
+ ================================================================================
1139
+ STOP VALIDATOR - BLOCKED (1 issue to fix)
1140
+ ================================================================================
1063
1141
 
1064
- ${errors.length} validation(s) failed. You MUST fix these before the task can complete.
1142
+ ERROR: ${error.type}
1065
1143
 
1066
- `;
1144
+ ${error.message}
1145
+
1146
+ ${error.action}
1067
1147
 
1068
- for (let i = 0; i < errors.length; i++) {
1069
- const err = errors[i];
1070
- output += `
1071
1148
  --------------------------------------------------------------------------------
1072
- ERROR ${i + 1}/${errors.length}: ${err.type}
1149
+ REQUIRED ACTION: Launch the following subagent to fix this issue
1073
1150
  --------------------------------------------------------------------------------
1074
1151
 
1075
- ${err.message}
1076
-
1077
- ${err.action}
1078
- `;
1079
- }
1080
-
1081
- output += `
1082
- ################################################################################
1083
- # FIX ALL ERRORS ABOVE BEFORE TASK CAN COMPLETE #
1084
- ################################################################################
1152
+ Task(subagent_type="${subagentInfo.agent}", prompt="${subagentInfo.prompt}")
1085
1153
 
1086
- SYNTHESIS REMINDER:
1087
- Before completing, ask yourself:
1088
- - Did the user mention any preferences I should remember?
1089
- - Did I learn any patterns that should be documented?
1090
- - Were there any corrections I should add as rules?
1091
-
1092
- Update CLAUDE.md with any learnings from this session.
1154
+ After fixing, try to complete the task again. The stop hook will re-validate.
1155
+ ================================================================================
1093
1156
  `;
1094
1157
 
1095
- // IMPORTANT: For blocking, output to STDERR and exit with code 2
1096
- const result: HookResult = { decision: 'block', reason: output.trim() };
1097
- console.error(JSON.stringify(result));
1098
- process.exit(2); // Exit code 2 = block and show to Claude
1158
+ // Stop hooks MUST return JSON with decision field
1159
+ // Exit code 2 signals blocking, but the JSON format is required for Stop hooks
1160
+ const result: HookResult = { decision: 'block', reason: blockReason.trim() };
1161
+ console.log(JSON.stringify(result));
1162
+ process.exit(2);
1099
1163
  }
1100
1164
 
1101
1165
  // All validations passed
@@ -0,0 +1,320 @@
1
+ # Hook Development for Claude Code
2
+
3
+ > Event-driven automation scripts for Claude Code. Hooks execute in response to system events, enabling validation, policy enforcement, context loading, and workflow integration.
4
+
5
+ ---
6
+
7
+ ## Hook Types
8
+
9
+ ### Prompt-Based Hooks (Recommended)
10
+
11
+ Use LLM-driven decision making for context-aware validation. Natural language reasoning for complex decisions.
12
+
13
+ **Supported Events:** Stop, SubagentStop, UserPromptSubmit, PreToolUse
14
+
15
+ ### Command Hooks
16
+
17
+ Execute bash/shell scripts for deterministic operations. Fast, predictable, no LLM calls.
18
+
19
+ **Best For:** File system operations, external tool integration, quick validations
20
+
21
+ ---
22
+
23
+ ## Hook Events
24
+
25
+ | Event | Purpose | Use Case |
26
+ |-------|---------|----------|
27
+ | **PreToolUse** | Validate/modify tool calls before execution | Block dangerous operations, inject context |
28
+ | **PostToolUse** | React to tool completion | Log results, trigger follow-up actions |
29
+ | **Stop** | Validate task completeness before agent halts | Enforce branch rules, documentation checks |
30
+ | **SubagentStop** | Validate subagent completion | Quality gates for agent outputs |
31
+ | **UserPromptSubmit** | Process incoming prompts | Add context, block invalid requests |
32
+ | **SessionStart** | Initialize session | Load project context, set environment |
33
+ | **SessionEnd** | Cleanup on session close | Save state, cleanup temp files |
34
+ | **PreCompact** | Before context compaction | Preserve critical information |
35
+ | **Notification** | React to Claude notifications | Custom notification handling |
36
+
37
+ ---
38
+
39
+ ## Configuration
40
+
41
+ ### Plugin Format (hooks/hooks.json)
42
+
43
+ ```json
44
+ {
45
+ "description": "Optional explanation",
46
+ "hooks": {
47
+ "PreToolUse": [...],
48
+ "Stop": [...]
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Settings Format (.claude/settings.json)
54
+
55
+ ```json
56
+ {
57
+ "hooks": {
58
+ "Stop": [
59
+ {
60
+ "hooks": [
61
+ {
62
+ "type": "command",
63
+ "command": "npx tsx .claude/hooks/stop-validator.ts",
64
+ "timeout": 30
65
+ }
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Input/Output
76
+
77
+ ### Standard Input (JSON via stdin)
78
+
79
+ All hooks receive:
80
+
81
+ ```json
82
+ {
83
+ "session_id": "string",
84
+ "transcript_path": "string",
85
+ "cwd": "string",
86
+ "permission_mode": "string",
87
+ "hook_event_name": "string"
88
+ }
89
+ ```
90
+
91
+ Event-specific fields:
92
+ - **PreToolUse/PostToolUse**: `tool_name`, `tool_input`
93
+ - **UserPromptSubmit**: `user_prompt`
94
+ - **Stop**: Current state info
95
+
96
+ ### Standard Output
97
+
98
+ ```json
99
+ {
100
+ "continue": true,
101
+ "suppressOutput": false,
102
+ "systemMessage": "Optional context for Claude"
103
+ }
104
+ ```
105
+
106
+ ### Exit Codes
107
+
108
+ | Code | Meaning | Behavior |
109
+ |------|---------|----------|
110
+ | 0 | Success | stdout appears in transcript |
111
+ | 2 | Blocking error | stderr feeds back to Claude for action |
112
+ | Other | Non-blocking error | Hook failed, but doesn't block |
113
+
114
+ ---
115
+
116
+ ## Environment Variables
117
+
118
+ Available in all command hooks:
119
+
120
+ | Variable | Description |
121
+ |----------|-------------|
122
+ | `$CLAUDE_PROJECT_DIR` | Project root directory |
123
+ | `$CLAUDE_PLUGIN_ROOT` | Plugin directory (for portability) |
124
+ | `$CLAUDE_ENV_FILE` | SessionStart persistence file |
125
+ | `$CLAUDE_CODE_REMOTE` | Remote execution flag |
126
+
127
+ ---
128
+
129
+ ## Matcher Patterns
130
+
131
+ Matchers determine which tools trigger hooks:
132
+
133
+ | Pattern | Example | Matches |
134
+ |---------|---------|---------|
135
+ | Exact | `"Write"` | Only Write tool |
136
+ | Multiple | `"Read\|Write\|Edit"` | Any of these tools |
137
+ | Wildcard | `"*"` | All tools |
138
+ | Regex | `"mcp__.*__delete.*"` | MCP delete operations |
139
+
140
+ ---
141
+
142
+ ## Best Practices
143
+
144
+ ### Security
145
+
146
+ ```typescript
147
+ // Always validate paths
148
+ if (filePath.includes('..') || filePath.startsWith('/etc')) {
149
+ throw new Error('Path traversal blocked');
150
+ }
151
+
152
+ // Always quote variables in bash
153
+ const cmd = `cat "${filePath}"`; // Correct
154
+ const cmd = `cat ${filePath}`; // WRONG - injection risk
155
+ ```
156
+
157
+ ### Performance
158
+
159
+ - All matching hooks execute **in parallel**
160
+ - Use command hooks for quick, deterministic checks
161
+ - Use prompt hooks for complex reasoning
162
+ - Default timeout: 60s (command), 30s (prompt)
163
+
164
+ ### Error Handling
165
+
166
+ ```typescript
167
+ // For blocking errors - use exit code 2
168
+ if (validationFailed) {
169
+ process.stderr.write(errorMessage);
170
+ process.exit(2); // Blocks and shows to Claude
171
+ }
172
+
173
+ // For non-blocking errors
174
+ if (warningCondition) {
175
+ console.error('Warning:', message);
176
+ process.exit(1); // Logs but doesn't block
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Stop Hook Template
183
+
184
+ ```typescript
185
+ #!/usr/bin/env node
186
+ import { execSync } from 'child_process';
187
+
188
+ interface HookResult {
189
+ decision: 'approve' | 'block';
190
+ reason: string;
191
+ }
192
+
193
+ async function main(): Promise<void> {
194
+ // Read stdin
195
+ const stdin = await readStdin();
196
+ const input = JSON.parse(stdin);
197
+
198
+ // Validation logic
199
+ const error = validateSomething();
200
+
201
+ if (error) {
202
+ // Block with actionable message
203
+ const message = `
204
+ ERROR: ${error.type}
205
+
206
+ ${error.message}
207
+
208
+ REQUIRED ACTION:
209
+ Task(subagent_type="${error.agent}", prompt="${error.prompt}")
210
+ `;
211
+ process.stderr.write(message);
212
+ process.exit(2); // Blocking
213
+ }
214
+
215
+ // Success
216
+ const result: HookResult = { decision: 'approve', reason: 'All checks passed' };
217
+ console.log(JSON.stringify(result));
218
+ process.exit(0);
219
+ }
220
+
221
+ main();
222
+ ```
223
+
224
+ ---
225
+
226
+ ## PreToolUse Hook Template
227
+
228
+ ```typescript
229
+ #!/usr/bin/env node
230
+ interface PreToolUseInput {
231
+ tool_name: string;
232
+ tool_input: Record<string, unknown>;
233
+ }
234
+
235
+ async function main(): Promise<void> {
236
+ const input: PreToolUseInput = JSON.parse(await readStdin());
237
+
238
+ // Check if this is a dangerous operation
239
+ if (input.tool_name === 'Write' && input.tool_input.file_path?.includes('.env')) {
240
+ process.stderr.write('BLOCKED: Cannot write to .env files');
241
+ process.exit(2);
242
+ }
243
+
244
+ // Allow operation
245
+ console.log(JSON.stringify({ continue: true }));
246
+ process.exit(0);
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Debugging
253
+
254
+ **Important:** Hooks load at session startup. Config changes require restarting Claude Code.
255
+
256
+ ```bash
257
+ # Debug hook execution
258
+ claude --debug
259
+
260
+ # View hook registration
261
+ claude --debug 2>&1 | grep -i hook
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Implementation Checklist
267
+
268
+ 1. [ ] Identify target events (Stop, PreToolUse, etc.)
269
+ 2. [ ] Choose hook type (prompt-based vs command)
270
+ 3. [ ] Write configuration in settings.json
271
+ 4. [ ] Create hook script with proper exit codes
272
+ 5. [ ] Test with `claude --debug`
273
+ 6. [ ] Document in project README
274
+
275
+ ---
276
+
277
+ ## Common Patterns
278
+
279
+ ### Validation Gate (Stop Hook)
280
+
281
+ Check conditions before task completion:
282
+
283
+ ```typescript
284
+ const validations = [
285
+ checkBranch(), // Must be on main
286
+ checkGitClean(), // No uncommitted changes
287
+ checkDocumentation() // All files documented
288
+ ];
289
+
290
+ const firstError = validations.find(v => v !== null);
291
+ if (firstError) {
292
+ process.stderr.write(formatError(firstError));
293
+ process.exit(2);
294
+ }
295
+ ```
296
+
297
+ ### Context Injection (UserPromptSubmit)
298
+
299
+ Add context to user prompts:
300
+
301
+ ```typescript
302
+ const context = loadProjectContext();
303
+ const result = {
304
+ continue: true,
305
+ systemMessage: `Project context: ${context}`
306
+ };
307
+ console.log(JSON.stringify(result));
308
+ ```
309
+
310
+ ### Tool Blocking (PreToolUse)
311
+
312
+ Block dangerous operations:
313
+
314
+ ```typescript
315
+ const BLOCKED_PATHS = ['.env', 'credentials', 'secrets'];
316
+ if (BLOCKED_PATHS.some(p => input.tool_input.path?.includes(p))) {
317
+ process.stderr.write('Blocked: Cannot access sensitive files');
318
+ process.exit(2);
319
+ }
320
+ ```