s/agents-development

Hooks and Policy-as-Code

Ревизия {n}

Diff заголовка

Предыдущая ревизия
Выбранная ревизия
1
 
1
Hooks and Policy-as-Code

Diff содержимого

Предыдущая ревизия
Выбранная ревизия
1
## Hooks and Policy-as-Code
1
 
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 events
10
 
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 live
28
 
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
```json
41
{
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 types
61
 
62
**Command hook** — runs a shell command. Receives JSON on stdin with session info, cwd, tool name, and tool input.
63
 
64
```json
65
{
66
  "type": "command",
67
  "command": "./.claude/hooks/check.sh",
68
  "timeout": 30
69
}
70
```
71
 
72
**HTTP hook** — POSTs JSON to a URL, reads the response. Useful for centralized policy servers.
73
 
74
```json
75
{
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 semantics
88
 
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 bash
100
 
101
```bash
102
#!/bin/bash
103
# .claude/hooks/block-rm.sh
104
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
105
 
106
if echo "$COMMAND" | grep -q 'rm -rf'; then
107
  echo "Blocked: rm -rf requires manual execution" >&2
108
  exit 2
109
fi
110
 
111
exit 0
112
```
113
 
114
Registered as:
115
 
116
```json
117
{
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 edit
132
 
133
```bash
134
#!/bin/bash
135
# .claude/hooks/patchgate.sh — runs after Edit/Write
136
cd "$CLAUDE_PROJECT_DIR" || exit 0
137
 
138
if ! npm run lint --silent; then
139
  echo "Lint failed — please fix before continuing" >&2
140
  exit 2
141
fi
142
 
143
if ! npm test --silent -- --changed; then
144
  echo "Tests failed on changed files" >&2
145
  exit 2
146
fi
147
 
148
exit 0
149
```
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 prompts
154
 
155
```bash
156
#!/bin/bash
157
# .claude/hooks/redact-secrets.sh — fires on UserPromptSubmit
158
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,}'; then
161
  echo "Prompt contains what looks like a secret. Scrub and resubmit." >&2
162
  exit 2
163
fi
164
 
165
exit 0
166
```
167
 
168
## JSON output for fine-grained control
169
 
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
```json
173
{
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
```json
185
{
186
  "hookSpecificOutput": {
187
    "hookEventName": "PreToolUse",
188
    "updatedInput": {
189
      "command": "npm test -- --runInBand"
190
    }
191
  }
192
}
193
```
194
 
195
## Interaction with permission rules
196
 
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 disabling
202
 
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 warnings
207
 
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
## Sources
217
 
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)
© 2026 HeyUpСделано на Laravel, Vue и Tailwind.