start-vibing 2.0.21 → 2.0.22
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
|
@@ -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
|
-
):
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
{ name: 'bun', cmd: 'bun'
|
|
107
|
-
{ name: 'npx-tsx', cmd: 'npx tsx'
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
if (!existsSync(hookPath)) {
|
|
116
|
+
if (!existsSync(tsPath)) {
|
|
114
117
|
continue;
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -118,17 +121,36 @@ async function runHook(hookName: string, stdinData: string): Promise<void> {
|
|
|
118
121
|
continue;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
const result = runWithRuntime(runtime.cmd, [
|
|
124
|
+
const result = runWithRuntime(runtime.cmd, [tsPath], stdinData);
|
|
125
|
+
|
|
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
|
|
122
130
|
|
|
123
|
-
if (result.
|
|
124
|
-
//
|
|
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 - stderr goes to Claude for action
|
|
137
|
+
if (result.error) {
|
|
138
|
+
process.stderr.write(result.error);
|
|
139
|
+
}
|
|
140
|
+
process.exit(2);
|
|
141
|
+
} else {
|
|
142
|
+
// Non-blocking error or runtime not found
|
|
143
|
+
if (result.error?.includes('not found')) {
|
|
144
|
+
// Runtime not available, try next
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Hook failed but not blocking
|
|
125
148
|
process.stdout.write(result.output);
|
|
126
|
-
if (result.error
|
|
149
|
+
if (result.error) {
|
|
127
150
|
process.stderr.write(result.error);
|
|
128
151
|
}
|
|
129
|
-
process.exit(result.
|
|
152
|
+
process.exit(result.exitCode);
|
|
130
153
|
}
|
|
131
|
-
// Runtime not available, try next
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
// 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 -
|
|
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,54 @@ async function main(): Promise<void> {
|
|
|
1001
1113
|
const isMainBranch = currentBranch === 'main' || currentBranch === 'master';
|
|
1002
1114
|
const isCleanTree = modifiedFiles.length === 0;
|
|
1003
1115
|
|
|
1004
|
-
|
|
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);
|
|
1116
|
+
console.error(`[stop-validator] Branch: ${currentBranch}, Modified: ${modifiedFiles.length}`);
|
|
1015
1117
|
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
}
|
|
1042
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1059
|
-
let output = `
|
|
1137
|
+
const output = `
|
|
1060
1138
|
################################################################################
|
|
1061
|
-
# STOP VALIDATOR -
|
|
1139
|
+
# STOP VALIDATOR - BLOCKED #
|
|
1062
1140
|
################################################################################
|
|
1063
1141
|
|
|
1064
|
-
${
|
|
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
|
-
|
|
1149
|
+
REQUIRED ACTION: Launch the following subagent to fix this issue
|
|
1073
1150
|
--------------------------------------------------------------------------------
|
|
1074
1151
|
|
|
1075
|
-
${
|
|
1152
|
+
Task(subagent_type="${subagentInfo.agent}", prompt="${subagentInfo.prompt}")
|
|
1076
1153
|
|
|
1077
|
-
|
|
1078
|
-
`;
|
|
1079
|
-
}
|
|
1154
|
+
After fixing this issue, the stop hook will run again to check for remaining issues.
|
|
1080
1155
|
|
|
1081
|
-
output += `
|
|
1082
|
-
################################################################################
|
|
1083
|
-
# FIX ALL ERRORS ABOVE BEFORE TASK CAN COMPLETE #
|
|
1084
1156
|
################################################################################
|
|
1085
|
-
|
|
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.
|
|
1093
1157
|
`;
|
|
1094
1158
|
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1159
|
+
// Write block reason to stderr (Claude sees this)
|
|
1160
|
+
process.stderr.write(output);
|
|
1161
|
+
|
|
1162
|
+
// Exit with code 2 = blocking error, stderr goes to Claude
|
|
1163
|
+
process.exit(2);
|
|
1099
1164
|
}
|
|
1100
1165
|
|
|
1101
1166
|
// 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
|
+
```
|