docs
0
# Hooks
1
2
Hooks allow you to inject custom Python functions at specific points in Claude's execution loop. They enable deterministic behavior, logging, validation, and custom control flow based on Claude's actions.
3
4
## Capabilities
5
6
### Hook Events
7
8
Supported hook event types that can trigger custom logic.
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
Supported hook event types.
21
22
Hook events allow you to inject custom logic at specific points in Claude's
23
execution loop:
24
25
- PreToolUse: Before a tool is executed
26
- PostToolUse: After a tool completes execution
27
- UserPromptSubmit: When user submits a prompt
28
- Stop: When conversation stops (main agent)
29
- SubagentStop: When a subagent conversation stops
30
- PreCompact: Before conversation history is compacted
31
32
Note: Due to setup limitations, the Python SDK does not support SessionStart,
33
SessionEnd, and Notification hooks that are available in the TypeScript SDK.
34
"""
35
```
36
37
### Hook Callback
38
39
Type alias for hook callback functions.
40
41
```python { .api }
42
HookCallback = Callable[
43
[dict[str, Any], str | None, HookContext],
44
Awaitable[HookJSONOutput]
45
]
46
"""
47
Hook callback function type.
48
49
A hook callback is an async function invoked when a hook event occurs.
50
51
Args:
52
input: Hook input data. Structure varies by hook type:
53
- PreToolUse: {"name": tool_name, "input": tool_input}
54
- PostToolUse: {"name": tool_name, "output": tool_output}
55
- UserPromptSubmit: {"prompt": prompt_text}
56
- Stop/SubagentStop: {"result": result_data}
57
- PreCompact: {"messages": messages_to_compact}
58
tool_use_id: Optional tool use ID (relevant for tool-related hooks)
59
context: Hook context with additional information
60
61
Returns:
62
HookJSONOutput dictionary with optional decision, systemMessage,
63
and hookSpecificOutput fields
64
65
See https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input
66
for detailed input structures for each hook type.
67
"""
68
```
69
70
### Hook Context
71
72
Context information passed to hook callbacks.
73
74
```python { .api }
75
@dataclass
76
class HookContext:
77
"""
78
Context information for hook callbacks.
79
80
Provides additional context and control mechanisms for hooks.
81
Future versions may add more fields.
82
83
Attributes:
84
signal: Abort signal support (future feature, currently None)
85
"""
86
87
signal: Any | None = None
88
"""Future: abort signal support.
89
90
Reserved for future use to allow canceling hook execution.
91
Currently always None.
92
"""
93
```
94
95
### Hook Output
96
97
Output structure returned by hook callbacks.
98
99
```python { .api }
100
class HookJSONOutput(TypedDict):
101
"""
102
Hook output structure.
103
104
Hooks return this dictionary to control behavior and communicate results.
105
106
Fields:
107
decision: Optional. Set to "block" to prevent the action
108
systemMessage: Optional. Add a system message to chat transcript
109
hookSpecificOutput: Optional. Hook-specific data
110
111
Note: Currently, "continue", "stopReason", and "suppressOutput" from the
112
TypeScript SDK are not supported in the Python SDK.
113
114
See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
115
for detailed documentation.
116
"""
117
118
decision: NotRequired[Literal["block"]]
119
"""Whether to block the action related to the hook.
120
121
When set to "block", the action associated with the hook will be prevented:
122
- PreToolUse: Block tool execution
123
- UserPromptSubmit: Block prompt submission
124
125
If not specified or set to any other value, the action proceeds normally.
126
"""
127
128
systemMessage: NotRequired[str]
129
"""Optional system message.
130
131
Add a system message that is saved in the chat transcript but not visible
132
to Claude. Useful for logging, debugging, or adding context for later review.
133
"""
134
135
hookSpecificOutput: NotRequired[Any]
136
"""Hook-specific output data.
137
138
Each hook type may define specific output fields. See individual hook
139
documentation for guidance on what can be included here.
140
"""
141
```
142
143
### Hook Matcher
144
145
Configuration for matching hooks to specific events.
146
147
```python { .api }
148
@dataclass
149
class HookMatcher:
150
"""
151
Hook matcher configuration.
152
153
Defines which events should trigger which hooks. Matchers filter events
154
based on patterns like tool names or other criteria.
155
156
Attributes:
157
matcher: Optional pattern to filter events
158
hooks: List of callback functions to invoke
159
"""
160
161
matcher: str | None = None
162
"""Matcher pattern string.
163
164
Filters which events trigger the hooks. The format depends on the hook type:
165
166
For PreToolUse/PostToolUse:
167
- Tool name: "Bash" (exact match)
168
- Multiple tools: "Write|Edit|MultiEdit" (OR pattern)
169
- All tools: None or "" (match any tool)
170
171
For other hooks:
172
- Usually None (no filtering)
173
174
See https://docs.anthropic.com/en/docs/claude-code/hooks#structure
175
for detailed matcher syntax.
176
"""
177
178
hooks: list[HookCallback] = field(default_factory=list)
179
"""List of hook callback functions.
180
181
All callbacks in this list will be invoked when the matcher condition
182
is met. Callbacks are executed in order.
183
"""
184
```
185
186
## Usage Examples
187
188
### Basic PreToolUse Hook
189
190
```python
191
from claude_agent_sdk import (
192
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
193
)
194
195
async def log_tool_use(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
196
"""Log when tools are used."""
197
tool_name = input_data.get("name")
198
tool_input = input_data.get("input")
199
print(f"Tool called: {tool_name}")
200
print(f"Input: {tool_input}")
201
return {}
202
203
options = ClaudeAgentOptions(
204
hooks={
205
"PreToolUse": [
206
HookMatcher(matcher=None, hooks=[log_tool_use])
207
]
208
}
209
)
210
211
async for msg in query(prompt="List files in current directory", options=options):
212
print(msg)
213
```
214
215
### Blocking Dangerous Commands
216
217
```python
218
from claude_agent_sdk import (
219
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
220
)
221
222
async def block_dangerous_bash(
223
input_data: dict,
224
tool_use_id: str | None,
225
context: HookContext
226
) -> HookJSONOutput:
227
"""Block dangerous bash commands."""
228
tool_input = input_data.get("input", {})
229
command = tool_input.get("command", "")
230
231
# Check for dangerous commands
232
dangerous_keywords = ["rm -rf", "dd if=", "mkfs", "format", "> /dev/"]
233
234
for keyword in dangerous_keywords:
235
if keyword in command:
236
return {
237
"decision": "block",
238
"systemMessage": f"Blocked dangerous command: {command}"
239
}
240
241
return {}
242
243
options = ClaudeAgentOptions(
244
allowed_tools=["Bash"],
245
hooks={
246
"PreToolUse": [
247
HookMatcher(matcher="Bash", hooks=[block_dangerous_bash])
248
]
249
}
250
)
251
252
async for msg in query(prompt="Delete all files", options=options):
253
print(msg)
254
```
255
256
### PostToolUse Validation
257
258
```python
259
from claude_agent_sdk import (
260
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
261
)
262
263
async def validate_write_output(
264
input_data: dict,
265
tool_use_id: str | None,
266
context: HookContext
267
) -> HookJSONOutput:
268
"""Validate file write operations."""
269
tool_name = input_data.get("name")
270
tool_output = input_data.get("output", {})
271
272
if tool_name in ["Write", "Edit"]:
273
# Check if write was successful
274
is_error = tool_output.get("is_error", False)
275
276
if is_error:
277
return {
278
"systemMessage": f"File operation failed: {tool_output.get('content')}"
279
}
280
else:
281
return {
282
"systemMessage": "File operation completed successfully"
283
}
284
285
return {}
286
287
options = ClaudeAgentOptions(
288
allowed_tools=["Write", "Edit", "Read"],
289
hooks={
290
"PostToolUse": [
291
HookMatcher(matcher="Write|Edit", hooks=[validate_write_output])
292
]
293
}
294
)
295
296
async for msg in query(prompt="Create a new Python file", options=options):
297
print(msg)
298
```
299
300
### Multiple Hooks for Same Event
301
302
```python
303
from claude_agent_sdk import (
304
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
305
)
306
307
async def log_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
308
"""Log tool usage."""
309
print(f"[LOG] Tool: {input_data.get('name')}")
310
return {}
311
312
async def audit_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
313
"""Audit tool usage."""
314
tool_name = input_data.get("name")
315
tool_input = input_data.get("input")
316
317
# Save to audit log
318
with open("audit.log", "a") as f:
319
f.write(f"Tool: {tool_name}, Input: {tool_input}\n")
320
321
return {}
322
323
async def security_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
324
"""Security checks."""
325
tool_name = input_data.get("name")
326
327
# Block certain tools during business hours
328
from datetime import datetime
329
hour = datetime.now().hour
330
331
if 9 <= hour <= 17 and tool_name == "Bash":
332
return {
333
"decision": "block",
334
"systemMessage": "Bash commands blocked during business hours"
335
}
336
337
return {}
338
339
options = ClaudeAgentOptions(
340
hooks={
341
"PreToolUse": [
342
HookMatcher(
343
matcher=None,
344
hooks=[log_hook, audit_hook, security_hook]
345
)
346
]
347
}
348
)
349
350
async for msg in query(prompt="Analyze this project", options=options):
351
print(msg)
352
```
353
354
### User Prompt Validation
355
356
```python
357
from claude_agent_sdk import (
358
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
359
)
360
361
async def validate_prompt(
362
input_data: dict,
363
tool_use_id: str | None,
364
context: HookContext
365
) -> HookJSONOutput:
366
"""Validate user prompts before submission."""
367
prompt = input_data.get("prompt", "")
368
369
# Block prompts with sensitive information
370
sensitive_patterns = ["password", "api_key", "secret", "token"]
371
372
for pattern in sensitive_patterns:
373
if pattern.lower() in prompt.lower():
374
return {
375
"decision": "block",
376
"systemMessage": f"Blocked prompt containing sensitive data: {pattern}"
377
}
378
379
return {}
380
381
options = ClaudeAgentOptions(
382
hooks={
383
"UserPromptSubmit": [
384
HookMatcher(matcher=None, hooks=[validate_prompt])
385
]
386
}
387
)
388
389
async for msg in query(prompt="What is my password?", options=options):
390
print(msg)
391
```
392
393
### Stop Hook for Cleanup
394
395
```python
396
from claude_agent_sdk import (
397
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
398
)
399
400
async def cleanup_on_stop(
401
input_data: dict,
402
tool_use_id: str | None,
403
context: HookContext
404
) -> HookJSONOutput:
405
"""Clean up resources when conversation stops."""
406
result = input_data.get("result", {})
407
408
print(f"Conversation ended")
409
print(f"Result: {result}")
410
411
# Perform cleanup
412
# - Close database connections
413
# - Save state
414
# - Log metrics
415
416
return {
417
"systemMessage": "Cleanup completed"
418
}
419
420
options = ClaudeAgentOptions(
421
hooks={
422
"Stop": [
423
HookMatcher(matcher=None, hooks=[cleanup_on_stop])
424
]
425
}
426
)
427
428
async for msg in query(prompt="Hello Claude", options=options):
429
print(msg)
430
```
431
432
### Rate Limiting Hook
433
434
```python
435
from datetime import datetime, timedelta
436
from claude_agent_sdk import (
437
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
438
)
439
440
# Rate limiting state
441
tool_usage = {}
442
RATE_LIMIT = 5 # Max 5 calls per minute
443
444
async def rate_limit_tools(
445
input_data: dict,
446
tool_use_id: str | None,
447
context: HookContext
448
) -> HookJSONOutput:
449
"""Rate limit tool usage."""
450
tool_name = input_data.get("name")
451
now = datetime.now()
452
453
# Initialize tracking for this tool
454
if tool_name not in tool_usage:
455
tool_usage[tool_name] = []
456
457
# Remove old entries (older than 1 minute)
458
tool_usage[tool_name] = [
459
ts for ts in tool_usage[tool_name]
460
if now - ts < timedelta(minutes=1)
461
]
462
463
# Check rate limit
464
if len(tool_usage[tool_name]) >= RATE_LIMIT:
465
return {
466
"decision": "block",
467
"systemMessage": f"Rate limit exceeded for {tool_name} (max {RATE_LIMIT}/min)"
468
}
469
470
# Record usage
471
tool_usage[tool_name].append(now)
472
473
return {}
474
475
options = ClaudeAgentOptions(
476
allowed_tools=["Bash"],
477
hooks={
478
"PreToolUse": [
479
HookMatcher(matcher="Bash", hooks=[rate_limit_tools])
480
]
481
}
482
)
483
484
async for msg in query(prompt="Run many commands", options=options):
485
print(msg)
486
```
487
488
### Conditional Tool Blocking
489
490
```python
491
from claude_agent_sdk import (
492
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
493
)
494
495
# Application state
496
class AppState:
497
def __init__(self):
498
self.readonly_mode = False
499
500
app_state = AppState()
501
502
async def enforce_readonly(
503
input_data: dict,
504
tool_use_id: str | None,
505
context: HookContext
506
) -> HookJSONOutput:
507
"""Block write operations in readonly mode."""
508
if not app_state.readonly_mode:
509
return {}
510
511
tool_name = input_data.get("name")
512
write_tools = ["Write", "Edit", "MultiEdit", "Bash"]
513
514
if tool_name in write_tools:
515
return {
516
"decision": "block",
517
"systemMessage": f"Blocked {tool_name} in readonly mode"
518
}
519
520
return {}
521
522
options = ClaudeAgentOptions(
523
hooks={
524
"PreToolUse": [
525
HookMatcher(matcher=None, hooks=[enforce_readonly])
526
]
527
}
528
)
529
530
# Enable readonly mode
531
app_state.readonly_mode = True
532
533
async for msg in query(prompt="Modify this file", options=options):
534
print(msg)
535
```
536
537
### Logging and Metrics
538
539
```python
540
import time
541
from claude_agent_sdk import (
542
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
543
)
544
545
# Metrics storage
546
metrics = {
547
"tool_calls": {},
548
"tool_durations": {}
549
}
550
start_times = {}
551
552
async def track_tool_start(
553
input_data: dict,
554
tool_use_id: str | None,
555
context: HookContext
556
) -> HookJSONOutput:
557
"""Track tool execution start."""
558
tool_name = input_data.get("name")
559
560
# Count tool calls
561
metrics["tool_calls"][tool_name] = metrics["tool_calls"].get(tool_name, 0) + 1
562
563
# Record start time
564
if tool_use_id:
565
start_times[tool_use_id] = time.time()
566
567
return {}
568
569
async def track_tool_end(
570
input_data: dict,
571
tool_use_id: str | None,
572
context: HookContext
573
) -> HookJSONOutput:
574
"""Track tool execution completion."""
575
tool_name = input_data.get("name")
576
577
# Calculate duration
578
if tool_use_id and tool_use_id in start_times:
579
duration = time.time() - start_times[tool_use_id]
580
581
if tool_name not in metrics["tool_durations"]:
582
metrics["tool_durations"][tool_name] = []
583
584
metrics["tool_durations"][tool_name].append(duration)
585
586
del start_times[tool_use_id]
587
588
return {}
589
590
async def print_metrics(
591
input_data: dict,
592
tool_use_id: str | None,
593
context: HookContext
594
) -> HookJSONOutput:
595
"""Print metrics when conversation stops."""
596
print("\n=== Tool Usage Metrics ===")
597
print("\nTool Calls:")
598
for tool, count in metrics["tool_calls"].items():
599
print(f" {tool}: {count}")
600
601
print("\nAverage Durations:")
602
for tool, durations in metrics["tool_durations"].items():
603
avg = sum(durations) / len(durations)
604
print(f" {tool}: {avg:.3f}s")
605
606
return {}
607
608
options = ClaudeAgentOptions(
609
hooks={
610
"PreToolUse": [
611
HookMatcher(matcher=None, hooks=[track_tool_start])
612
],
613
"PostToolUse": [
614
HookMatcher(matcher=None, hooks=[track_tool_end])
615
],
616
"Stop": [
617
HookMatcher(matcher=None, hooks=[print_metrics])
618
]
619
}
620
)
621
622
async for msg in query(prompt="Analyze this project", options=options):
623
print(msg)
624
```
625
626
### Hook with Hook-Specific Output
627
628
```python
629
from claude_agent_sdk import (
630
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
631
)
632
633
async def custom_tool_output(
634
input_data: dict,
635
tool_use_id: str | None,
636
context: HookContext
637
) -> HookJSONOutput:
638
"""Modify tool behavior with hook-specific output."""
639
tool_name = input_data.get("name")
640
641
# Example: Add metadata to tool output
642
return {
643
"systemMessage": f"Tool {tool_name} executed",
644
"hookSpecificOutput": {
645
"metadata": {
646
"tool": tool_name,
647
"timestamp": time.time(),
648
"source": "hook"
649
}
650
}
651
}
652
653
options = ClaudeAgentOptions(
654
hooks={
655
"PostToolUse": [
656
HookMatcher(matcher=None, hooks=[custom_tool_output])
657
]
658
}
659
)
660
661
async for msg in query(prompt="List files", options=options):
662
print(msg)
663
```
664
665
### Multiple Hook Events
666
667
```python
668
from claude_agent_sdk import (
669
ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query
670
)
671
672
async def log_pre_tool(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
673
print(f"[PRE] Tool: {input_data.get('name')}")
674
return {}
675
676
async def log_post_tool(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
677
print(f"[POST] Tool: {input_data.get('name')}")
678
return {}
679
680
async def log_prompt(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
681
print(f"[PROMPT] {input_data.get('prompt')}")
682
return {}
683
684
async def log_stop(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:
685
print("[STOP] Conversation ended")
686
return {}
687
688
options = ClaudeAgentOptions(
689
hooks={
690
"PreToolUse": [HookMatcher(matcher=None, hooks=[log_pre_tool])],
691
"PostToolUse": [HookMatcher(matcher=None, hooks=[log_post_tool])],
692
"UserPromptSubmit": [HookMatcher(matcher=None, hooks=[log_prompt])],
693
"Stop": [HookMatcher(matcher=None, hooks=[log_stop])]
694
}
695
)
696
697
async for msg in query(prompt="Hello Claude", options=options):
698
print(msg)
699
```
700