docs
0
# Hook System
1
2
The hook system allows you to intercept and control Claude's operations at specific points in the agent loop. Hooks can modify inputs, add context, block execution, or trigger custom logic during PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, and PreCompact events.
3
4
## Capabilities
5
6
### Hook Events
7
8
Six hook event types are supported.
9
10
```python { .api }
11
HookEvent = Literal[
12
"PreToolUse",
13
"PostToolUse",
14
"UserPromptSubmit",
15
"Stop",
16
"SubagentStop",
17
"PreCompact"
18
]
19
```
20
21
**Event Types:**
22
23
- `PreToolUse`: Before a tool is executed (can modify input or block execution)
24
- `PostToolUse`: After a tool is executed (can add context to results)
25
- `UserPromptSubmit`: When user submits a prompt (can add context)
26
- `Stop`: When agent execution stops
27
- `SubagentStop`: When a sub-agent stops
28
- `PreCompact`: Before transcript compaction
29
30
### Hook Matcher
31
32
Configure hooks with pattern matching.
33
34
```python { .api }
35
@dataclass
36
class HookMatcher:
37
"""Hook configuration with pattern matching."""
38
39
matcher: str | None = None
40
hooks: list[HookCallback] = field(default_factory=list)
41
```
42
43
**Fields:**
44
45
- `matcher` (str | None): Tool name pattern for PreToolUse and PostToolUse. Examples:
46
- `"Bash"`: Match only Bash tool
47
- `"Write|Edit"`: Match Write or Edit tools
48
- `"Write|MultiEdit|Edit"`: Match multiple tools
49
- `None`: Match all tools (for PreToolUse/PostToolUse) or not applicable (for other hooks)
50
51
- `hooks` (list[HookCallback]): List of callback functions to execute for this matcher.
52
53
**Usage Example:**
54
55
```python
56
from claude_agent_sdk import HookMatcher, ClaudeAgentOptions
57
58
async def my_hook(input, tool_use_id, context):
59
return {}
60
61
# Match specific tool
62
matcher = HookMatcher(matcher="Bash", hooks=[my_hook])
63
64
# Match multiple tools
65
matcher = HookMatcher(matcher="Write|Edit", hooks=[my_hook])
66
67
# Match all tools
68
matcher = HookMatcher(matcher=None, hooks=[my_hook])
69
70
# Use in options
71
options = ClaudeAgentOptions(
72
hooks={
73
"PreToolUse": [matcher]
74
}
75
)
76
```
77
78
### Hook Callback
79
80
Callback function type for hooks.
81
82
```python { .api }
83
HookCallback = Callable[
84
[HookInput, str | None, HookContext],
85
Awaitable[HookJSONOutput]
86
]
87
```
88
89
**Parameters:**
90
91
1. `input` (HookInput): Hook-specific input with discriminated union based on `hook_event_name`
92
2. `tool_use_id` (str | None): Optional tool use identifier
93
3. `context` (HookContext): Hook context with settings directory
94
95
**Returns:**
96
97
`HookJSONOutput`: Either `AsyncHookJSONOutput` or `SyncHookJSONOutput`
98
99
**Signature Example:**
100
101
```python
102
async def my_hook_callback(
103
input: HookInput,
104
tool_use_id: str | None,
105
context: HookContext
106
) -> HookJSONOutput:
107
# Hook logic
108
return {}
109
```
110
111
### Hook Context
112
113
Context information provided to hook callbacks.
114
115
```python { .api }
116
class HookContext(TypedDict):
117
"""Context provided to hooks."""
118
119
signal: Any | None
120
```
121
122
**Fields:**
123
124
- `signal` (Any | None): Reserved for future abort signal support. Currently always None.
125
126
**Usage Example:**
127
128
```python
129
async def my_hook(input, tool_use_id, context):
130
signal = context["signal"]
131
# Future: Use signal for abort operations
132
return {}
133
```
134
135
## Hook Input Types
136
137
All hook inputs extend `BaseHookInput` and use discriminated unions based on `hook_event_name`.
138
139
### Base Hook Input
140
141
Base fields present in all hook inputs.
142
143
```python { .api }
144
class BaseHookInput(TypedDict):
145
"""Base hook input fields present across many hook events."""
146
147
session_id: str
148
transcript_path: str
149
cwd: str
150
permission_mode: NotRequired[str]
151
```
152
153
**Fields:**
154
155
- `session_id` (str): Session identifier
156
- `transcript_path` (str): Path to conversation transcript file
157
- `cwd` (str): Current working directory
158
- `permission_mode` (str, optional): Current permission mode
159
160
### PreToolUse Hook Input
161
162
Input for PreToolUse hooks (before tool execution).
163
164
```python { .api }
165
class PreToolUseHookInput(BaseHookInput):
166
"""Input data for PreToolUse hook events."""
167
168
hook_event_name: Literal["PreToolUse"]
169
tool_name: str
170
tool_input: dict[str, Any]
171
```
172
173
**Fields:**
174
175
- `hook_event_name`: Always `"PreToolUse"`
176
- `tool_name` (str): Name of tool being invoked
177
- `tool_input` (dict[str, Any]): Tool input parameters
178
179
**Usage Example:**
180
181
```python
182
async def pre_tool_use_hook(input, tool_use_id, context):
183
if input["hook_event_name"] == "PreToolUse":
184
tool_name = input["tool_name"]
185
tool_input = input["tool_input"]
186
187
# Validate or modify input
188
if tool_name == "Bash":
189
command = tool_input.get("command", "")
190
if "rm -rf" in command:
191
return {
192
"decision": "block",
193
"reason": "Dangerous command blocked"
194
}
195
196
# Allow with modified input
197
return {
198
"hookSpecificOutput": {
199
"hookEventName": "PreToolUse",
200
"permissionDecision": "allow",
201
"updatedInput": tool_input
202
}
203
}
204
205
return {}
206
```
207
208
### PostToolUse Hook Input
209
210
Input for PostToolUse hooks (after tool execution).
211
212
```python { .api }
213
class PostToolUseHookInput(BaseHookInput):
214
"""Input data for PostToolUse hook events."""
215
216
hook_event_name: Literal["PostToolUse"]
217
tool_name: str
218
tool_input: dict[str, Any]
219
tool_response: Any
220
```
221
222
**Fields:**
223
224
- `hook_event_name`: Always `"PostToolUse"`
225
- `tool_name` (str): Name of tool that was invoked
226
- `tool_input` (dict[str, Any]): Tool input parameters
227
- `tool_response` (Any): Tool execution response
228
229
**Usage Example:**
230
231
```python
232
async def post_tool_use_hook(input, tool_use_id, context):
233
if input["hook_event_name"] == "PostToolUse":
234
tool_name = input["tool_name"]
235
tool_response = input["tool_response"]
236
237
# Add context based on results
238
if tool_name == "Bash":
239
return {
240
"hookSpecificOutput": {
241
"hookEventName": "PostToolUse",
242
"additionalContext": "Command executed successfully"
243
}
244
}
245
246
return {}
247
```
248
249
### UserPromptSubmit Hook Input
250
251
Input for UserPromptSubmit hooks (when user submits prompt).
252
253
```python { .api }
254
class UserPromptSubmitHookInput(BaseHookInput):
255
"""Input data for UserPromptSubmit hook events."""
256
257
hook_event_name: Literal["UserPromptSubmit"]
258
prompt: str
259
```
260
261
**Fields:**
262
263
- `hook_event_name`: Always `"UserPromptSubmit"`
264
- `prompt` (str): User prompt text
265
266
**Usage Example:**
267
268
```python
269
async def user_prompt_hook(input, tool_use_id, context):
270
if input["hook_event_name"] == "UserPromptSubmit":
271
prompt = input["prompt"]
272
273
# Add context to prompt
274
return {
275
"hookSpecificOutput": {
276
"hookEventName": "UserPromptSubmit",
277
"additionalContext": f"Processing prompt: {len(prompt)} characters"
278
}
279
}
280
281
return {}
282
```
283
284
### Stop Hook Input
285
286
Input for Stop hooks (when agent execution stops).
287
288
```python { .api }
289
class StopHookInput(BaseHookInput):
290
"""Input data for Stop hook events."""
291
292
hook_event_name: Literal["Stop"]
293
stop_hook_active: bool
294
```
295
296
**Fields:**
297
298
- `hook_event_name`: Always `"Stop"`
299
- `stop_hook_active` (bool): Whether stop hook is active
300
301
**Usage Example:**
302
303
```python
304
async def stop_hook(input, tool_use_id, context):
305
if input["hook_event_name"] == "Stop":
306
# Cleanup or logging
307
return {}
308
309
return {}
310
```
311
312
### SubagentStop Hook Input
313
314
Input for SubagentStop hooks (when sub-agent stops).
315
316
```python { .api }
317
class SubagentStopHookInput(BaseHookInput):
318
"""Input data for SubagentStop hook events."""
319
320
hook_event_name: Literal["SubagentStop"]
321
stop_hook_active: bool
322
```
323
324
**Fields:**
325
326
- `hook_event_name`: Always `"SubagentStop"`
327
- `stop_hook_active` (bool): Whether stop hook is active
328
329
**Usage Example:**
330
331
```python
332
async def subagent_stop_hook(input, tool_use_id, context):
333
if input["hook_event_name"] == "SubagentStop":
334
# Handle sub-agent completion
335
return {}
336
337
return {}
338
```
339
340
### PreCompact Hook Input
341
342
Input for PreCompact hooks (before transcript compaction).
343
344
```python { .api }
345
class PreCompactHookInput(BaseHookInput):
346
"""Input data for PreCompact hook events."""
347
348
hook_event_name: Literal["PreCompact"]
349
trigger: Literal["manual", "auto"]
350
custom_instructions: str | None
351
```
352
353
**Fields:**
354
355
- `hook_event_name`: Always `"PreCompact"`
356
- `trigger`: Compaction trigger type (`"manual"` or `"auto"`)
357
- `custom_instructions` (str | None): Custom compaction instructions
358
359
**Usage Example:**
360
361
```python
362
async def pre_compact_hook(input, tool_use_id, context):
363
if input["hook_event_name"] == "PreCompact":
364
trigger = input["trigger"]
365
# Perform pre-compaction actions
366
return {}
367
368
return {}
369
```
370
371
### Hook Input Union
372
373
Union of all hook input types.
374
375
```python { .api }
376
HookInput = (
377
PreToolUseHookInput
378
| PostToolUseHookInput
379
| UserPromptSubmitHookInput
380
| StopHookInput
381
| SubagentStopHookInput
382
| PreCompactHookInput
383
)
384
```
385
386
## Hook Output Types
387
388
Hooks return either synchronous or asynchronous outputs.
389
390
### Synchronous Hook Output
391
392
Standard hook response with control and decision fields.
393
394
```python { .api }
395
class SyncHookJSONOutput(TypedDict):
396
"""Synchronous hook output with control and decision fields."""
397
398
# Common control fields
399
continue_: NotRequired[bool]
400
suppressOutput: NotRequired[bool]
401
stopReason: NotRequired[str]
402
403
# Decision fields
404
decision: NotRequired[Literal["block"]]
405
systemMessage: NotRequired[str]
406
reason: NotRequired[str]
407
408
# Hook-specific outputs
409
hookSpecificOutput: NotRequired[HookSpecificOutput]
410
```
411
412
**Fields:**
413
414
- `continue_` (bool, optional): Whether Claude should proceed after hook execution. Default: True. **Note**: Use `continue_` in Python code; it's automatically converted to `"continue"` for CLI.
415
416
- `suppressOutput` (bool, optional): Hide stdout from transcript mode. Default: False.
417
418
- `stopReason` (str, optional): Message shown when `continue_` is False.
419
420
- `decision` (Literal["block"], optional): Set to `"block"` to indicate blocking behavior.
421
422
- `systemMessage` (str, optional): Warning message displayed to the user.
423
424
- `reason` (str, optional): Feedback message for Claude about the decision.
425
426
- `hookSpecificOutput` (HookSpecificOutput, optional): Event-specific controls (see below).
427
428
**Usage Example:**
429
430
```python
431
# Allow execution
432
async def allow_hook(input, tool_use_id, context):
433
return {} # Empty dict means continue normally
434
435
# Block execution
436
async def block_hook(input, tool_use_id, context):
437
return {
438
"decision": "block",
439
"systemMessage": "This operation is not allowed",
440
"reason": "Security policy violation"
441
}
442
443
# Stop execution
444
async def stop_hook(input, tool_use_id, context):
445
return {
446
"continue_": False, # Note the underscore
447
"stopReason": "User intervention required"
448
}
449
450
# Suppress output
451
async def quiet_hook(input, tool_use_id, context):
452
return {
453
"suppressOutput": True
454
}
455
```
456
457
### Asynchronous Hook Output
458
459
Deferred hook execution response.
460
461
```python { .api }
462
class AsyncHookJSONOutput(TypedDict):
463
"""Async hook output that defers hook execution."""
464
465
async_: Literal[True]
466
asyncTimeout: NotRequired[int]
467
```
468
469
**Fields:**
470
471
- `async_` (Literal[True]): Set to True to defer hook execution. **Note**: Use `async_` in Python code; it's automatically converted to `"async"` for CLI.
472
473
- `asyncTimeout` (int, optional): Timeout in milliseconds for the async operation.
474
475
**Usage Example:**
476
477
```python
478
async def async_hook(input, tool_use_id, context):
479
# Defer execution
480
return {
481
"async_": True, # Note the underscore
482
"asyncTimeout": 5000 # 5 second timeout
483
}
484
```
485
486
### Hook Output Union
487
488
Union of hook output types.
489
490
```python { .api }
491
HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput
492
```
493
494
## Hook-Specific Output Types
495
496
Event-specific controls in `hookSpecificOutput`.
497
498
### PreToolUse Specific Output
499
500
```python { .api }
501
class PreToolUseHookSpecificOutput(TypedDict):
502
"""Output specific to PreToolUse hooks."""
503
504
hookEventName: Literal["PreToolUse"]
505
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
506
permissionDecisionReason: NotRequired[str]
507
updatedInput: NotRequired[dict[str, Any]]
508
```
509
510
**Fields:**
511
512
- `hookEventName`: Must be `"PreToolUse"`
513
- `permissionDecision`: Permission decision (`"allow"`, `"deny"`, or `"ask"`)
514
- `permissionDecisionReason`: Reason for the decision
515
- `updatedInput`: Modified tool input parameters
516
517
**Usage Example:**
518
519
```python
520
async def pre_tool_hook(input, tool_use_id, context):
521
if input["hook_event_name"] == "PreToolUse":
522
# Modify tool input
523
modified_input = input["tool_input"].copy()
524
modified_input["safe_mode"] = True
525
526
return {
527
"hookSpecificOutput": {
528
"hookEventName": "PreToolUse",
529
"permissionDecision": "allow",
530
"permissionDecisionReason": "Added safety flag",
531
"updatedInput": modified_input
532
}
533
}
534
return {}
535
```
536
537
### PostToolUse Specific Output
538
539
```python { .api }
540
class PostToolUseHookSpecificOutput(TypedDict):
541
"""Output specific to PostToolUse hooks."""
542
543
hookEventName: Literal["PostToolUse"]
544
additionalContext: NotRequired[str]
545
```
546
547
**Fields:**
548
549
- `hookEventName`: Must be `"PostToolUse"`
550
- `additionalContext`: Additional context to provide to Claude
551
552
**Usage Example:**
553
554
```python
555
async def post_tool_hook(input, tool_use_id, context):
556
if input["hook_event_name"] == "PostToolUse":
557
return {
558
"hookSpecificOutput": {
559
"hookEventName": "PostToolUse",
560
"additionalContext": "Tool executed successfully with no errors"
561
}
562
}
563
return {}
564
```
565
566
### UserPromptSubmit Specific Output
567
568
```python { .api }
569
class UserPromptSubmitHookSpecificOutput(TypedDict):
570
"""Output specific to UserPromptSubmit hooks."""
571
572
hookEventName: Literal["UserPromptSubmit"]
573
additionalContext: NotRequired[str]
574
```
575
576
**Fields:**
577
578
- `hookEventName`: Must be `"UserPromptSubmit"`
579
- `additionalContext`: Additional context to provide to Claude
580
581
**Usage Example:**
582
583
```python
584
async def prompt_submit_hook(input, tool_use_id, context):
585
if input["hook_event_name"] == "UserPromptSubmit":
586
return {
587
"hookSpecificOutput": {
588
"hookEventName": "UserPromptSubmit",
589
"additionalContext": "Context: User is working on a Python project"
590
}
591
}
592
return {}
593
```
594
595
## Complete Examples
596
597
### Security Hook
598
599
Block dangerous bash commands:
600
601
```python
602
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
603
604
DANGEROUS_PATTERNS = ["rm -rf", "sudo rm", "> /dev/sda", ":(){ :|:& };:"]
605
606
async def security_hook(input, tool_use_id, context):
607
if input["hook_event_name"] == "PreToolUse":
608
if input["tool_name"] == "Bash":
609
command = input["tool_input"].get("command", "")
610
611
for pattern in DANGEROUS_PATTERNS:
612
if pattern in command:
613
return {
614
"decision": "block",
615
"systemMessage": f"Blocked dangerous command: {pattern}",
616
"reason": "Security policy violation",
617
"hookSpecificOutput": {
618
"hookEventName": "PreToolUse",
619
"permissionDecision": "deny",
620
"permissionDecisionReason": f"Command contains dangerous pattern: {pattern}"
621
}
622
}
623
return {}
624
625
options = ClaudeAgentOptions(
626
allowed_tools=["Read", "Write", "Bash"],
627
hooks={
628
"PreToolUse": [
629
HookMatcher(matcher="Bash", hooks=[security_hook])
630
]
631
}
632
)
633
```
634
635
### Logging Hook
636
637
Log all tool usage:
638
639
```python
640
import logging
641
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
642
643
logging.basicConfig(level=logging.INFO)
644
logger = logging.getLogger(__name__)
645
646
async def pre_tool_logger(input, tool_use_id, context):
647
if input["hook_event_name"] == "PreToolUse":
648
logger.info(f"Tool {input['tool_name']} called with: {input['tool_input']}")
649
return {}
650
651
async def post_tool_logger(input, tool_use_id, context):
652
if input["hook_event_name"] == "PostToolUse":
653
logger.info(f"Tool {input['tool_name']} completed: {input['tool_response']}")
654
return {}
655
656
options = ClaudeAgentOptions(
657
allowed_tools=["Read", "Write", "Bash"],
658
hooks={
659
"PreToolUse": [HookMatcher(matcher=None, hooks=[pre_tool_logger])],
660
"PostToolUse": [HookMatcher(matcher=None, hooks=[post_tool_logger])]
661
}
662
)
663
```
664
665
### Context Enhancement Hook
666
667
Add context based on tool results:
668
669
```python
670
async def context_enhancer(input, tool_use_id, context):
671
if input["hook_event_name"] == "PostToolUse":
672
tool_name = input["tool_name"]
673
response = input["tool_response"]
674
675
if tool_name == "Read":
676
# Add context about file type
677
content = response.get("content", "")
678
if "import" in content and "def" in content:
679
return {
680
"hookSpecificOutput": {
681
"hookEventName": "PostToolUse",
682
"additionalContext": "This appears to be Python code with imports and functions"
683
}
684
}
685
686
elif tool_name == "Bash":
687
# Add context about command success
688
if response.get("exit_code") == 0:
689
return {
690
"hookSpecificOutput": {
691
"hookEventName": "PostToolUse",
692
"additionalContext": "Command executed successfully"
693
}
694
}
695
696
return {}
697
698
options = ClaudeAgentOptions(
699
hooks={
700
"PostToolUse": [HookMatcher(matcher=None, hooks=[context_enhancer])]
701
}
702
)
703
```
704
705
### File Protection Hook
706
707
Protect critical files from modification:
708
709
```python
710
PROTECTED_FILES = ["/etc/passwd", "/etc/shadow", "~/.ssh/id_rsa"]
711
712
async def file_protection_hook(input, tool_use_id, context):
713
if input["hook_event_name"] == "PreToolUse":
714
tool_name = input["tool_name"]
715
716
if tool_name in ["Write", "Edit", "MultiEdit"]:
717
file_path = input["tool_input"].get("file_path", "")
718
719
for protected in PROTECTED_FILES:
720
if protected in file_path:
721
return {
722
"decision": "block",
723
"systemMessage": f"Cannot modify protected file: {file_path}",
724
"hookSpecificOutput": {
725
"hookEventName": "PreToolUse",
726
"permissionDecision": "deny",
727
"permissionDecisionReason": "File is protected by policy"
728
}
729
}
730
731
return {}
732
733
options = ClaudeAgentOptions(
734
hooks={
735
"PreToolUse": [
736
HookMatcher(matcher="Write|Edit|MultiEdit", hooks=[file_protection_hook])
737
]
738
}
739
)
740
```
741
742
### Input Sanitization Hook
743
744
Sanitize tool inputs:
745
746
```python
747
async def input_sanitizer(input, tool_use_id, context):
748
if input["hook_event_name"] == "PreToolUse":
749
if input["tool_name"] == "Bash":
750
# Remove potentially dangerous env vars
751
tool_input = input["tool_input"].copy()
752
command = tool_input.get("command", "")
753
754
# Strip environment variable exports
755
if "export" in command.lower():
756
safe_command = command.replace("export", "# export")
757
tool_input["command"] = safe_command
758
759
return {
760
"hookSpecificOutput": {
761
"hookEventName": "PreToolUse",
762
"permissionDecision": "allow",
763
"permissionDecisionReason": "Sanitized environment exports",
764
"updatedInput": tool_input
765
}
766
}
767
768
return {}
769
770
options = ClaudeAgentOptions(
771
hooks={
772
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[input_sanitizer])]
773
}
774
)
775
```
776
777
## Important Notes
778
779
### Python Keyword Conflicts
780
781
Use underscored versions in Python code:
782
- `async_` instead of `async`
783
- `continue_` instead of `continue`
784
785
These are automatically converted to the correct field names when sent to the CLI.
786
787
### Hook Execution Order
788
789
When multiple hooks match, they execute in order defined in the hooks list.
790
791
### Error Handling
792
793
Exceptions in hooks are caught and logged but don't break execution. Always handle errors gracefully.
794
795
### Performance
796
797
Hooks are called synchronously in the agent loop. Keep hook logic fast to avoid delays.
798
799
### Hook Limitations
800
801
The Python SDK does not support SessionStart, SessionEnd, and Notification hooks due to setup limitations.
802