Hooks
Hooks are Claude Code's automation surface — deterministic bash (or any shell) scripts that fire on lifecycle events. Each hook receives JSON on stdin and returns JSON on stdout: {"decision": "approve"|"block", "reason"?: "..."}.
Events (matcher + input + output)
Every hook also receives common fields on stdin: session_id, transcript_path, cwd, permission_mode, and hook_event_name (plus agent_id/agent_type inside subagents).
| Event |
Fires |
Input (event-specific) |
Output shape |
PreToolUse |
Before any tool |
{tool_name, tool_input} |
hookSpecificOutput.permissionDecision: allow|deny|ask|defer, updatedInput?, additionalContext? |
PostToolUse |
After tool succeeds |
{tool_name, tool_input, tool_output} |
{decision, reason?} |
PostToolUseFailure |
After tool fails |
{tool_name, tool_input, error} |
{decision} — almost always approve |
UserPromptSubmit |
Prompt submitted |
{prompt} |
{decision} + additionalContext? / optional modified prompt |
Notification |
Claude emits a user-facing notification |
{message} |
{decision} |
Stop |
Claude is about to stop responding |
{stop_reason} |
{decision} — block to force more work (e.g. tests must pass) |
SessionStart |
New session begins |
{source} |
additionalContext? — context/rule injection |
SessionEnd |
Session is ending |
{} |
{decision} — summarize, archive to memory |
PreCompact |
Before /compact (manual or auto) |
{} |
save key context to memory before it's summarized |
SubagentStart |
A subagent is spawned |
{agent_type} |
block to enforce budget / gate which agents may fire |
SubagentStop |
A subagent completes or is stopped |
{agent_id, agent_type} |
collect telemetry, capture output |
TeammateIdle |
A teammate process has been idle too long |
{agent_id} |
kill stale processes, alert |
PermissionRequest |
A permission check fires |
{tool_name, tool_input} |
auto-approve/deny known-safe tools, add permission rules |
Setup |
During initial session setup |
{} |
bootstrap rules, check environment |
This is the practical working set. Hook events that block (return a deny/block decision): PreToolUse, Stop, UserPromptSubmit, SubagentStart, TeammateIdle, PermissionRequest. The rest are observe-only.
Installing a hook pack
Use MCP cc_kb_hook_recipe(name) to fetch a security-hardened pack. Each pack returns:
script — the bash script content
script_path — target path (.claude/hooks/{name}.sh)
settings_snippet — the JSON to merge into .claude/settings.json
verify — one-line manual test
Available pack names (fetch a shortlist first via cc_docs_hook_pack_recommend(signals)):
protect-sensitive-files — block writes to .env/credentials (always recommended)
auto-format-after-edit — prettier/black/rustfmt on Write|Edit
stop-until-tests-pass — block Stop if tests fail
post-compact-context-restoration — re-load memory rules after /compact
direnv-reload-on-cwd-change — reload .envrc on cd
task-created-governance — log new tasks to memory
task-completed-quality-gate — enforce lint+test before task completion
teammate-idle-enforcement — prevent idle teammate processes
Authoring a new hook
#!/bin/bash
set -euo pipefail
INPUT=$(head -c 65536)
# Validate JSON input
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
echo '{"decision":"approve"}'; exit 0
fi
# Extract fields you need
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
# Validate path (prevent injection + traversal)
[ -z "$FILE" ] && { echo '{"decision":"approve"}'; exit 0; }
REAL=$(realpath "$FILE" 2>/dev/null) || { echo '{"decision":"approve"}'; exit 0; }
WD=$(realpath "$PWD")
[[ "$REAL" != "$WD"/* ]] && { echo '{"decision":"approve"}'; exit 0; }
BN=$(basename "$REAL")
[[ "$BN" == -* ]] && { echo '{"decision":"approve"}'; exit 0; }
# Do the work...
# Always emit valid JSON
echo '{"decision":"approve"}'
Safety rules:
- Always cap input with
head -c.
- Always validate JSON before parsing.
- Always
realpath + PWD-prefix check before touching a file.
- Reject filenames starting with
- (flag injection).
- Never
eval or unquoted-interpolate user content.
- Default to
{"decision":"approve"} on any error — never block on bugs.
- For concurrent writes to shared files (e.g. a log), use
flock.
Registering in settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/protect-sensitive-files.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format-after-edit.sh" }]
}
]
}
}
Matchers are regex. Write|Edit fires for both tools. * fires for all tools.
Debugging
- Hook not firing: check
.claude/settings.json has the event + matcher. Matcher regex is case-sensitive.
- Hook blocks unexpectedly: check the hook's stderr output — Claude Code prints it.
- Hook slow: profile with
time bash .claude/hooks/.... Target <100ms for PreToolUse, <500ms for PostToolUse.
- JSON parse error: run
bash .claude/hooks/x.sh < fixture.json locally.
MCP delegation
| Need |
Tool |
| Fetch a specific hook pack |
cc_kb_hook_recipe(name) |
| Recommend packs from signals |
cc_docs_hook_pack_recommend({has_formatter, has_tests, has_secrets, has_git, ...}) |
Anti-patterns
- Hooks that call LLMs → slow and non-deterministic. Hooks should be fast local computation.
- Hooks that
rm -rf or delete files → use Bash tool with user confirmation, not a hook.
- Hooks that swallow errors without emitting approve → hangs the session.
- Multiple hooks on same event that fight each other → run them sequentially in one script or use distinct matchers.
Reference
- hook-event-matrix.md — full event → input → output contract per event
1---2name: cc-hook-authoring3description: Design, install, and debug Claude Code hooks across the full lifecycle (PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Notification, Stop, SessionStart, SessionEnd, PreCompact, SubagentStart, SubagentStop, TeammateIdle, PermissionRequest, Setup). Use this skill whenever a user asks to "install hooks", "add a pre-tool hook", "format on save", "block dangerous commands", "protect sensitive files", "restore context after compact", "enforce tests before stop", capture subagent telemetry, or runs /cc-hooks. Also triggers on "hooks not firing", "hook keeps blocking", or any configuration of .claude/settings.json hook sections.4---5
6# Hooks
7
8Hooks are Claude Code's automation surface — deterministic bash (or any shell) scripts that fire on lifecycle events. Each hook receives JSON on stdin and returns JSON on stdout: `{"decision": "approve"|"block", "reason"?: "..."}`.
9
10## Events (matcher + input + output)
11
12Every hook also receives common fields on stdin: `session_id`, `transcript_path`, `cwd`, `permission_mode`, and `hook_event_name` (plus `agent_id`/`agent_type` inside subagents).
13
14| Event | Fires | Input (event-specific) | Output shape |
15|---|---|---|---|
16| `PreToolUse` | Before any tool | `{tool_name, tool_input}` | `hookSpecificOutput.permissionDecision: allow\|deny\|ask\|defer`, `updatedInput?`, `additionalContext?` |
17| `PostToolUse` | After tool succeeds | `{tool_name, tool_input, tool_output}` | `{decision, reason?}` |
18| `PostToolUseFailure` | After tool fails | `{tool_name, tool_input, error}` | `{decision}` — almost always approve |
19| `UserPromptSubmit` | Prompt submitted | `{prompt}` | `{decision}` + `additionalContext?` / optional modified prompt |
20| `Notification` | Claude emits a user-facing notification | `{message}` | `{decision}` |
21| `Stop` | Claude is about to stop responding | `{stop_reason}` | `{decision}` — block to force more work (e.g. tests must pass) |
22| `SessionStart` | New session begins | `{source}` | `additionalContext?` — context/rule injection |
23| `SessionEnd` | Session is ending | `{}` | `{decision}` — summarize, archive to memory |
24| `PreCompact` | Before `/compact` (manual or auto) | `{}` | save key context to memory before it's summarized |
25| `SubagentStart` | A subagent is spawned | `{agent_type}` | block to enforce budget / gate which agents may fire |
26| `SubagentStop` | A subagent completes or is stopped | `{agent_id, agent_type}` | collect telemetry, capture output |
27| `TeammateIdle` | A teammate process has been idle too long | `{agent_id}` | kill stale processes, alert |
28| `PermissionRequest` | A permission check fires | `{tool_name, tool_input}` | auto-approve/deny known-safe tools, add permission rules |
29| `Setup` | During initial session setup | `{}` | bootstrap rules, check environment |
30
31This is the practical working set. Hook events that block (return a deny/block decision): `PreToolUse`, `Stop`, `UserPromptSubmit`, `SubagentStart`, `TeammateIdle`, `PermissionRequest`. The rest are observe-only.
32
33## Installing a hook pack
34
35Use MCP `cc_kb_hook_recipe(name)` to fetch a security-hardened pack. Each pack returns:
36- `script` — the bash script content
37- `script_path` — target path (`.claude/hooks/{name}.sh`)
38- `settings_snippet` — the JSON to merge into `.claude/settings.json`
39- `verify` — one-line manual test
40
41Available pack names (fetch a shortlist first via `cc_docs_hook_pack_recommend(signals)`):
42- `protect-sensitive-files` — block writes to .env/credentials (always recommended)
43- `auto-format-after-edit` — prettier/black/rustfmt on Write|Edit
44- `stop-until-tests-pass` — block Stop if tests fail
45- `post-compact-context-restoration` — re-load memory rules after /compact
46- `direnv-reload-on-cwd-change` — reload .envrc on cd
47- `task-created-governance` — log new tasks to memory
48- `task-completed-quality-gate` — enforce lint+test before task completion
49- `teammate-idle-enforcement` — prevent idle teammate processes
50
51## Authoring a new hook
52
53```bash
54#!/bin/bash
55set -euo pipefail
56INPUT=$(head -c 65536)
57
58# Validate JSON input
59if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
60 echo '{"decision":"approve"}'; exit 0
61fi
62
63# Extract fields you need
64FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
65
66# Validate path (prevent injection + traversal)
67[ -z "$FILE" ] && { echo '{"decision":"approve"}'; exit 0; }
68REAL=$(realpath "$FILE" 2>/dev/null) || { echo '{"decision":"approve"}'; exit 0; }
69WD=$(realpath "$PWD")
70[[ "$REAL" != "$WD"/* ]] && { echo '{"decision":"approve"}'; exit 0; }
71BN=$(basename "$REAL")
72[[ "$BN" == -* ]] && { echo '{"decision":"approve"}'; exit 0; }
73
74# Do the work...
75
76# Always emit valid JSON
77echo '{"decision":"approve"}'
78```
79
80**Safety rules:**
811. Always cap input with `head -c`.
822. Always validate JSON before parsing.
833. Always `realpath` + PWD-prefix check before touching a file.
844. Reject filenames starting with `-` (flag injection).
855. Never `eval` or unquoted-interpolate user content.
866. Default to `{"decision":"approve"}` on any error — never block on bugs.
877. For concurrent writes to shared files (e.g. a log), use `flock`.
88
89## Registering in settings.json
90
91```json
92{
93 "hooks": {
94 "PreToolUse": [
95 {
96 "matcher": "Write|Edit",
97 "hooks": [{ "type": "command", "command": "bash .claude/hooks/protect-sensitive-files.sh" }]
98 }
99 ],
100 "PostToolUse": [
101 {
102 "matcher": "Write|Edit",
103 "hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format-after-edit.sh" }]
104 }
105 ]
106 }
107}
108```
109
110Matchers are regex. `Write|Edit` fires for both tools. `*` fires for all tools.
111
112## Debugging
113
1141. **Hook not firing**: check `.claude/settings.json` has the event + matcher. Matcher regex is case-sensitive.
1152. **Hook blocks unexpectedly**: check the hook's stderr output — Claude Code prints it.
1163. **Hook slow**: profile with `time bash .claude/hooks/...`. Target <100ms for PreToolUse, <500ms for PostToolUse.
1174. **JSON parse error**: run `bash .claude/hooks/x.sh < fixture.json` locally.
118
119## MCP delegation
120
121| Need | Tool |
122|---|---|
123| Fetch a specific hook pack | `cc_kb_hook_recipe(name)` |
124| Recommend packs from signals | `cc_docs_hook_pack_recommend({has_formatter, has_tests, has_secrets, has_git, ...})` |
125
126## Anti-patterns
127
128- Hooks that call LLMs → slow and non-deterministic. Hooks should be fast local computation.
129- Hooks that `rm -rf` or delete files → use `Bash` tool with user confirmation, not a hook.
130- Hooks that swallow errors without emitting approve → hangs the session.
131- Multiple hooks on same event that fight each other → run them sequentially in one script or use distinct matchers.
132
133## Reference
134
135- [hook-event-matrix.md](references/hook-event-matrix.md) — full event → input → output contract per event