s/agents-development
Hooks and Policy-as-Code
Ревизия {n}
Diff заголовка
Предыдущая ревизия
Выбранная ревизия
1
1
Hooks and Policy-as-Code
Diff содержимого
Предыдущая ревизия
Выбранная ревизия
1
## Hooks and Policy-as-Code1
2
3
CLAUDE.md tells the agent *what* you want. Permission rules declare *which tools it may use*. Neither of those is enough on its own for production. Sooner or later you need deterministic, programmatic checks that run regardless of what the model decides — "if it tries to edit a file matching `infra/prod/**`, block it. Every time. No exceptions."4
5
That's what hooks are for. A hook is a shell command, HTTP endpoint, or LLM prompt that Claude Code runs at a specific point in its lifecycle. Hooks get JSON on stdin, control the flow through exit codes, and can block actions the model was about to take.6
7
This page covers the hook model, all the event types that matter in production, and a few concrete patterns.8
9
## Hook events10
11
| Event | When it fires | Can block? |12
| --- | --- | --- |13
| `SessionStart` | Session begins or resumes | No |14
| `UserPromptSubmit` | User submits a prompt, before processing | Yes |15
| `PreToolUse` | Before a tool call executes | Yes |16
| `PostToolUse` | After a tool call succeeds | No |17
| `PostToolUseFailure` | After a tool call fails | No |18
| `PermissionRequest` | A permission dialog is about to show | Yes |19
| `PermissionDenied` | Auto mode denied a tool call | No |20
| `Stop` | Claude finishes responding | Yes |21
| `SubagentStart` / `SubagentStop` | Subagent lifecycle | Yes (Stop) |22
| `PreCompact` / `PostCompact` | Around context compaction | No |23
| `SessionEnd` | Session terminates | No |24
25
The ones you will actually use: `PreToolUse` (block dangerous actions), `PostToolUse` (lint, format, test after edits), `SessionStart` (load environment), and `UserPromptSubmit` (sanitize / log prompts).26
27
## Where hooks live28
29
Hooks are configured in `settings.json`, which has a clear precedence chain:30
31
```32
~/.claude/settings.json (user, all projects)33
.claude/settings.json (project, checked in)34
.claude/settings.local.json (project, gitignored)35
managed-settings.json (org-wide, cannot be overridden)36
```37
38
The structure is three levels: event → matcher group → handlers.39
40
```json41
{42
"hooks": {43
"PreToolUse": [44
{45
"matcher": "Bash",46
"hooks": [47
{48
"type": "command",49
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-bash.sh"50
}51
]52
}53
]54
}55
}56
```57
58
The `matcher` field can be the tool name (`"Bash"`), a pipe-separated list (`"Edit|Write"`), or a JS regex (`"mcp__memory__.*"`).59
60
## Handler types61
62
**Command hook** — runs a shell command. Receives JSON on stdin with session info, cwd, tool name, and tool input.63
64
```json65
{66
"type": "command",67
"command": "./.claude/hooks/check.sh",68
"timeout": 3069
}70
```71
72
**HTTP hook** — POSTs JSON to a URL, reads the response. Useful for centralized policy servers.73
74
```json75
{76
"type": "http",77
"url": "http://policy.internal/claude",78
"headers": { "Authorization": "Bearer $POLICY_TOKEN" },79
"allowedEnvVars": ["POLICY_TOKEN"]80
}81
```82
83
**Prompt hook** — runs a quick LLM check (cheap model). Good for fuzzy rules like "does this command match our ops policy?".84
85
**Agent hook** — runs a full agent review. Heavy, but sometimes worth it.86
87
## Exit code semantics88
89
Claude Code's exit code conventions are unusual. Read this once and pin it.90
91
| Exit code | Meaning |92
| --- | --- |93
| `0` | Success. Claude parses stdout as JSON for structured control. |94
| `2` | **Blocking error.** Stops the action. stderr is shown to Claude. |95
| anything else | Non-blocking error. Shown in transcript. Execution continues. |96
97
Exit `1` is treated as a non-blocking error. Use exit `2` only for policy enforcement.98
99
## Pattern 1: block dangerous bash100
101
```bash102
#!/bin/bash103
# .claude/hooks/block-rm.sh104
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)105
106
if echo "$COMMAND" | grep -q 'rm -rf'; then107
echo "Blocked: rm -rf requires manual execution" >&2108
exit 2109
fi110
111
exit 0112
```113
114
Registered as:115
116
```json117
{118
"hooks": {119
"PreToolUse": [120
{121
"matcher": "Bash",122
"hooks": [123
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-rm.sh" }124
]125
}126
]127
}128
}129
```130
131
## Pattern 2: PatchGate — lint and test after every edit132
133
```bash134
#!/bin/bash135
# .claude/hooks/patchgate.sh — runs after Edit/Write136
cd "$CLAUDE_PROJECT_DIR" || exit 0137
138
if ! npm run lint --silent; then139
echo "Lint failed — please fix before continuing" >&2140
exit 2141
fi142
143
if ! npm test --silent -- --changed; then144
echo "Tests failed on changed files" >&2145
exit 2146
fi147
148
exit 0149
```150
151
Registered on `PostToolUse` for `Edit|Write`. This turns "green CI" into a per-edit invariant — the agent cannot advance past a broken state.152
153
## Pattern 3: redact secrets from prompts154
155
```bash156
#!/bin/bash157
# .claude/hooks/redact-secrets.sh — fires on UserPromptSubmit158
PROMPT=$(jq -r '.user_prompt' < /dev/stdin)159
160
if echo "$PROMPT" | grep -qE 'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{40,}'; then161
echo "Prompt contains what looks like a secret. Scrub and resubmit." >&2162
exit 2163
fi164
165
exit 0166
```167
168
## JSON output for fine-grained control169
170
Instead of plain exit codes, a hook can exit 0 and print structured JSON to stdout. This lets you allow, deny, modify input, or inject context:171
172
```json173
{174
"hookSpecificOutput": {175
"hookEventName": "PreToolUse",176
"permissionDecision": "deny",177
"permissionDecisionReason": "Writes to infra/prod/ require a change ticket"178
}179
}180
```181
182
Or to modify the tool call before it runs:183
184
```json185
{186
"hookSpecificOutput": {187
"hookEventName": "PreToolUse",188
"updatedInput": {189
"command": "npm test -- --runInBand"190
}191
}192
}193
```194
195
## Interaction with permission rules196
197
Hooks run *before* permission rules are consulted. A hook that exits 2 blocks the action regardless of allow rules. A hook that exits 0 does not bypass deny rules — they still fire afterward.198
199
This lets you write open policies ("allow Bash") with targeted hooks that block specific patterns, rather than maintaining an ever-growing deny list.200
201
## Viewing and disabling202
203
- `/hooks` inside Claude Code shows a read-only browser of all configured hooks and their sources.204
- `"disableAllHooks": true` in settings disables everything temporarily — useful for debugging, never in CI.205
206
## Security warnings207
208
- Hook scripts run with your user's permissions. Treat them like any cron job — they can do damage if written wrong.209
- JSON on stdin can be corrupted by `.bashrc` or `.zshrc` output. Make sure hook output is only the JSON object.210
- Don't commit hooks that reference secrets. Use env vars and document them.211
212
---213
214
Next: [Multi-Agent Patterns](/s/agents-development/wiki/multi-agent-patterns)215
216
## Sources217
218
- [Claude Code hooks reference](https://code.claude.com/docs/en/hooks)219
- [Hooks guide with more examples](https://code.claude.com/docs/en/hooks-guide)220
- [Permissions and their interaction with hooks](https://code.claude.com/docs/en/permissions)