0
# Hook System
1
2
Python functions that the Claude Code application invokes at specific points of the Claude agent loop. Hooks provide deterministic processing and automated feedback for Claude, enabling custom validation, permission checks, and automated responses.
3
4
## Capabilities
5
6
### Hook Events
7
8
Supported hook event types in the Python SDK.
9
10
```python { .api }
11
HookEvent = (
12
Literal["PreToolUse"]
13
| Literal["PostToolUse"]
14
| Literal["UserPromptSubmit"]
15
| Literal["Stop"]
16
| Literal["SubagentStop"]
17
| Literal["PreCompact"]
18
)
19
```
20
21
**Hook Events:**
22
- `"PreToolUse"`: Before tool execution - can block or modify tool usage
23
- `"PostToolUse"`: After tool execution - process tool results
24
- `"UserPromptSubmit"`: When user submits a prompt - validate or modify input
25
- `"Stop"`: When Claude stops processing - cleanup or logging
26
- `"SubagentStop"`: When a subagent stops - subagent lifecycle management
27
- `"PreCompact"`: Before message compaction - control conversation history
28
29
### Hook Callback Function
30
31
Function signature for hook callbacks that process hook events.
32
33
```python { .api }
34
HookCallback = Callable[
35
[dict[str, Any], str | None, HookContext],
36
Awaitable[HookJSONOutput]
37
]
38
```
39
40
**Parameters:**
41
- `input`: Hook input data (varies by hook event type)
42
- `tool_use_id`: Tool use identifier (None for non-tool events)
43
- `context`: Hook execution context with signal support
44
45
### Hook Context
46
47
Context information provided to hook callbacks during execution.
48
49
```python { .api }
50
@dataclass
51
class HookContext:
52
"""Context information for hook callbacks."""
53
signal: Any | None = None # Future: abort signal support
54
```
55
56
### Hook Matcher
57
58
Configuration for matching specific events and associating them with callback functions.
59
60
```python { .api }
61
@dataclass
62
class HookMatcher:
63
"""Hook matcher configuration."""
64
65
matcher: str | None = None
66
hooks: list[HookCallback] = field(default_factory=list)
67
```
68
69
**Matcher Patterns:**
70
- Tool names: `"Bash"`, `"Write"`, `"Read"`
71
- Multiple tools: `"Write|MultiEdit|Edit"`
72
- All events: `None` or omit matcher
73
74
### Hook Output
75
76
Response structure that hooks return to control Claude Code behavior.
77
78
```python { .api }
79
class HookJSONOutput(TypedDict):
80
# Whether to block the action related to the hook
81
decision: NotRequired[Literal["block"]]
82
# Optionally add a system message that is not visible to Claude but saved in
83
# the chat transcript
84
systemMessage: NotRequired[str]
85
# See each hook's individual "Decision Control" section in the documentation
86
# for guidance
87
hookSpecificOutput: NotRequired[Any]
88
```
89
90
## Usage Examples
91
92
### Basic Tool Validation Hook
93
94
```python
95
from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions, HookMatcher
96
97
async def validate_bash_commands(input_data, tool_use_id, context):
98
"""Validate bash commands before execution."""
99
tool_name = input_data.get("tool_name")
100
tool_input = input_data.get("tool_input", {})
101
102
if tool_name != "Bash":
103
return {} # Only validate Bash commands
104
105
command = tool_input.get("command", "")
106
dangerous_patterns = ["rm -rf", "format", "dd if=", "> /dev/"]
107
108
for pattern in dangerous_patterns:
109
if pattern in command:
110
return {
111
"decision": "block",
112
"systemMessage": f"Blocked dangerous command: {command[:50]}...",
113
"hookSpecificOutput": {
114
"hookEventName": "PreToolUse",
115
"permissionDecision": "deny",
116
"permissionDecisionReason": f"Command contains dangerous pattern: {pattern}",
117
}
118
}
119
120
return {} # Allow command
121
122
async def main():
123
options = ClaudeCodeOptions(
124
allowed_tools=["Bash"],
125
hooks={
126
"PreToolUse": [
127
HookMatcher(matcher="Bash", hooks=[validate_bash_commands])
128
]
129
}
130
)
131
132
async with ClaudeSDKClient(options=options) as client:
133
# This will be blocked
134
await client.query("Run the bash command: rm -rf /important-data")
135
136
async for msg in client.receive_response():
137
print(msg)
138
```
139
140
### Multiple Hook Events
141
142
```python
143
async def log_tool_use(input_data, tool_use_id, context):
144
"""Log all tool usage for monitoring."""
145
tool_name = input_data.get("tool_name")
146
print(f"Tool used: {tool_name} (ID: {tool_use_id})")
147
148
return {
149
"systemMessage": f"Logged tool usage: {tool_name}"
150
}
151
152
async def log_tool_result(input_data, tool_use_id, context):
153
"""Log tool results."""
154
tool_name = input_data.get("tool_name")
155
success = input_data.get("success", False)
156
print(f"Tool {tool_name} {'succeeded' if success else 'failed'}")
157
158
return {}
159
160
async def validate_user_input(input_data, tool_use_id, context):
161
"""Validate user prompts."""
162
content = input_data.get("content", "")
163
164
if "secret" in content.lower():
165
return {
166
"decision": "block",
167
"systemMessage": "Blocked prompt containing sensitive information"
168
}
169
170
return {}
171
172
async def main():
173
options = ClaudeCodeOptions(
174
allowed_tools=["Read", "Write", "Bash"],
175
hooks={
176
"PreToolUse": [
177
HookMatcher(hooks=[log_tool_use]) # All tools
178
],
179
"PostToolUse": [
180
HookMatcher(hooks=[log_tool_result]) # All tools
181
],
182
"UserPromptSubmit": [
183
HookMatcher(hooks=[validate_user_input])
184
]
185
}
186
)
187
188
async with ClaudeSDKClient(options=options) as client:
189
await client.query("Create a Python file with my secret password")
190
191
async for msg in client.receive_response():
192
print(msg)
193
```
194
195
### Conditional Tool Modification
196
197
```python
198
async def modify_file_operations(input_data, tool_use_id, context):
199
"""Modify file operations to add safety checks."""
200
tool_name = input_data.get("tool_name")
201
tool_input = input_data.get("tool_input", {})
202
203
if tool_name == "Write":
204
file_path = tool_input.get("file_path", "")
205
206
# Prevent overwriting important files
207
if file_path.endswith((".env", "config.json", ".env.local")):
208
return {
209
"hookSpecificOutput": {
210
"hookEventName": "PreToolUse",
211
"modifiedInput": {
212
**tool_input,
213
"file_path": file_path + ".backup"
214
}
215
},
216
"systemMessage": f"Redirected write to backup file: {file_path}.backup"
217
}
218
219
return {}
220
221
async def main():
222
options = ClaudeCodeOptions(
223
allowed_tools=["Read", "Write"],
224
hooks={
225
"PreToolUse": [
226
HookMatcher(matcher="Write", hooks=[modify_file_operations])
227
]
228
}
229
)
230
231
async with ClaudeSDKClient(options=options) as client:
232
await client.query("Create a .env file with database credentials")
233
234
async for msg in client.receive_response():
235
print(msg)
236
```
237
238
### Complex Workflow Hook
239
240
```python
241
import json
242
from datetime import datetime
243
244
class WorkflowTracker:
245
def __init__(self):
246
self.operations = []
247
self.start_time = None
248
249
def log_operation(self, operation):
250
self.operations.append({
251
"timestamp": datetime.now().isoformat(),
252
"operation": operation
253
})
254
255
tracker = WorkflowTracker()
256
257
async def track_workflow_start(input_data, tool_use_id, context):
258
"""Track workflow initiation."""
259
if tracker.start_time is None:
260
tracker.start_time = datetime.now()
261
262
content = input_data.get("content", "")
263
tracker.log_operation({"type": "user_prompt", "content": content[:100]})
264
265
return {
266
"systemMessage": "Workflow tracking initiated"
267
}
268
269
async def track_tool_execution(input_data, tool_use_id, context):
270
"""Track all tool executions in workflow."""
271
tool_name = input_data.get("tool_name")
272
tool_input = input_data.get("tool_input", {})
273
274
tracker.log_operation({
275
"type": "tool_use",
276
"tool": tool_name,
277
"input_preview": str(tool_input)[:100]
278
})
279
280
return {}
281
282
async def finalize_workflow(input_data, tool_use_id, context):
283
"""Generate workflow summary on completion."""
284
duration = datetime.now() - tracker.start_time if tracker.start_time else None
285
286
summary = {
287
"duration_seconds": duration.total_seconds() if duration else 0,
288
"total_operations": len(tracker.operations),
289
"tools_used": list(set(
290
op["operation"]["tool"]
291
for op in tracker.operations
292
if op["operation"].get("type") == "tool_use"
293
))
294
}
295
296
return {
297
"systemMessage": f"Workflow completed: {json.dumps(summary, indent=2)}"
298
}
299
300
async def main():
301
options = ClaudeCodeOptions(
302
allowed_tools=["Read", "Write", "Bash", "Edit"],
303
hooks={
304
"UserPromptSubmit": [
305
HookMatcher(hooks=[track_workflow_start])
306
],
307
"PreToolUse": [
308
HookMatcher(hooks=[track_tool_execution])
309
],
310
"Stop": [
311
HookMatcher(hooks=[finalize_workflow])
312
]
313
}
314
)
315
316
async with ClaudeSDKClient(options=options) as client:
317
await client.query("Create a web server, write tests, and run them")
318
319
async for msg in client.receive_response():
320
print(msg)
321
```
322
323
### Custom Permission Logic
324
325
```python
326
class PermissionManager:
327
def __init__(self):
328
self.allowed_files = {".py", ".js", ".html", ".css", ".md"}
329
self.blocked_commands = ["curl", "wget", "ssh"]
330
331
async def check_file_permission(self, input_data, tool_use_id, context):
332
"""Check file operation permissions."""
333
tool_name = input_data.get("tool_name")
334
tool_input = input_data.get("tool_input", {})
335
336
if tool_name in ["Write", "Edit", "MultiEdit"]:
337
file_path = tool_input.get("file_path", "")
338
file_ext = Path(file_path).suffix if file_path else ""
339
340
if file_ext not in self.allowed_files:
341
return {
342
"decision": "block",
343
"systemMessage": f"File type {file_ext} not allowed",
344
"hookSpecificOutput": {
345
"hookEventName": "PreToolUse",
346
"permissionDecision": "deny",
347
"permissionDecisionReason": f"File extension {file_ext} not in allowed list"
348
}
349
}
350
351
elif tool_name == "Bash":
352
command = tool_input.get("command", "")
353
for blocked_cmd in self.blocked_commands:
354
if blocked_cmd in command:
355
return {
356
"decision": "block",
357
"systemMessage": f"Command '{blocked_cmd}' blocked",
358
"hookSpecificOutput": {
359
"hookEventName": "PreToolUse",
360
"permissionDecision": "deny",
361
"permissionDecisionReason": f"Command contains blocked executable: {blocked_cmd}"
362
}
363
}
364
365
return {} # Allow operation
366
367
async def main():
368
permission_manager = PermissionManager()
369
370
options = ClaudeCodeOptions(
371
allowed_tools=["Read", "Write", "Edit", "Bash"],
372
hooks={
373
"PreToolUse": [
374
HookMatcher(hooks=[permission_manager.check_file_permission])
375
]
376
}
377
)
378
379
async with ClaudeSDKClient(options=options) as client:
380
await client.query("Download a file using curl and save it as data.exe")
381
382
async for msg in client.receive_response():
383
print(msg)
384
```
385
386
## Hook Event Input Data
387
388
Different hook events receive different input data structures:
389
390
### PreToolUse / PostToolUse
391
```python
392
{
393
"tool_name": "Bash",
394
"tool_input": {"command": "ls -la"},
395
"tool_use_id": "toolu_123456",
396
# PostToolUse additionally includes:
397
"success": True,
398
"result": "...",
399
"error": None
400
}
401
```
402
403
### UserPromptSubmit
404
```python
405
{
406
"content": "Create a web server",
407
"session_id": "default",
408
"timestamp": "2024-01-01T12:00:00Z"
409
}
410
```
411
412
### Stop / SubagentStop
413
```python
414
{
415
"reason": "completed",
416
"session_id": "default",
417
"duration_ms": 5000,
418
"num_turns": 3
419
}
420
```
421
422
### PreCompact
423
```python
424
{
425
"message_count": 25,
426
"total_tokens": 4000,
427
"compact_threshold": 3500
428
}
429
```
430
431
## Hook Execution Flow
432
433
1. **Event Trigger**: Claude Code reaches a hook point in the agent loop
434
2. **Matcher Evaluation**: Check if any matchers match the current event/tool
435
3. **Hook Execution**: Call all matching hook functions in registration order
436
4. **Decision Aggregation**: Combine all hook responses
437
5. **Action Application**: Apply decisions (block, modify, log, etc.)
438
439
## Hook Limitations
440
441
**Python SDK Restrictions:**
442
- SessionStart, SessionEnd, and Notification hooks are not supported
443
- No support for "continue", "stopReason", and "suppressOutput" controls
444
- Hook execution is synchronous within the Claude Code process
445
446
**Performance Considerations:**
447
- Hooks execute synchronously and can impact response times
448
- Complex hook logic should be optimized for speed
449
- Avoid blocking I/O operations in hook functions
450
451
## Integration with Permission System
452
453
Hooks work alongside the permission system and can:
454
- Override permission decisions through `hookSpecificOutput`
455
- Provide custom permission validation logic
456
- Log permission decisions for audit purposes
457
- Modify tool inputs before permission checks
458
459
For permission callback configuration, see [Configuration and Options](./configuration-options.md).
460
461
For hook usage with custom tools, see [Custom Tools](./custom-tools.md).