Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Intercept and control agent behavior with hooks changedagent-sdk/hooks

Nearest release: v2.1.302, published 8 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 17 Oct 2026 01:27 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.

Upstream edited
Recorded here
Lines+570added
Lines−596removed
From line 12 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits18to this page, all time

The whole hunk

from line 12, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 12
1212 
1313## How hooks work
1414 
15<Steps>
16 <Step title="An event fires">
17 Something happens during agent execution and the SDK fires an event: a tool is about to be called (`PreToolUse`), a tool returned a result (`PostToolUse`), a subagent started or stopped, the agent is idle, or execution finished. See the [full list of events](#available-hooks).
18 </Step>
19 
20 <Step title="The SDK collects registered hooks">
21 The SDK checks for hooks registered for that event type. This includes callback hooks you pass in `options.hooks` and shell command hooks from settings files when the corresponding [`settingSources`](/docs/en/agent-sdk/typescript#settingsource) or [`setting_sources`](/docs/en/agent-sdk/python#settingsource) entry is enabled, which it is for default `query()` options.
22 </Step>
23 
24 <Step title="Matchers filter which hooks run">
25 If a hook has a [`matcher`](#matchers) pattern (like `"Write|Edit"`), the SDK tests it against the event's target (for example, the tool name). Hooks without a matcher run for every event of that type.
26 </Step>
27 
28 <Step title="Callback functions execute">
29 Each matching hook's [callback function](#callback-functions) receives input about what's happening: the tool name, its arguments, the session ID, and other event-specific details.
30 </Step>
31 
32 <Step title="Your callback returns a decision">
33 After performing any operations (logging, API calls, validation), your callback returns an [output object](#outputs) that tells the agent what to do: allow the operation, block it, modify the input, or inject context into the conversation.
34 </Step>
35</Steps>
15**1. An event fires**
16 
17Something happens during agent execution and the SDK fires an event: a tool is about to be called (`PreToolUse`), a tool returned a result (`PostToolUse`), a subagent started or stopped, the agent is idle, or execution finished. See the [full list of events](#available-hooks).
18 
19**2. The SDK collects registered hooks**
20 
21The SDK checks for hooks registered for that event type. This includes callback hooks you pass in `options.hooks` and shell command hooks from settings files when the corresponding [`settingSources`](/docs/en/agent-sdk/typescript#settingsource) or [`setting_sources`](/docs/en/agent-sdk/python#settingsource) entry is enabled, which it is for default `query()` options.
22 
23**3. Matchers filter which hooks run**
24 
25If a hook has a [`matcher`](#matchers) pattern (like `"Write|Edit"`), the SDK tests it against the event's target (for example, the tool name). Hooks without a matcher run for every event of that type.
26 
27**4. Callback functions execute**
28 
29Each matching hook's [callback function](#callback-functions) receives input about what's happening: the tool name, its arguments, the session ID, and other event-specific details.
30 
31**5. Your callback returns a decision**
32 
33After performing any operations (logging, API calls, validation), your callback returns an [output object](#outputs) that tells the agent what to do: allow the operation, block it, modify the input, or inject context into the conversation.
3634 
3735This example registers a `PreToolUse` hook (step 1) with a `"Write|Edit"` matcher (step 3) so the callback fires for the Write and Edit tools. When triggered, the callback receives the tool's input (step 4), checks if the file path targets a `.env` file, and returns `permissionDecision: "deny"` to block the operation (step 5):
3836 
39<CodeGroup>
40 ```python Python theme={null}
41 import asyncio
42 from claude_agent_sdk import (
43 AssistantMessage,
44 ClaudeSDKClient,
45 ClaudeAgentOptions,
46 HookMatcher,
47 ResultMessage,
48 )
49 
50 
51 # Define a hook callback that receives tool call details
52 async def protect_env_files(input_data, tool_use_id, context):
53 # Extract the file path from the tool's input arguments
54 file_path = input_data["tool_input"].get("file_path", "")
55 file_name = file_path.split("/")[-1]
56 
57 # Block the operation if targeting a .env file
58 if file_name.lower() == ".env":
59 return {
60 "hookSpecificOutput": {
61 "hookEventName": input_data["hook_event_name"],
62 "permissionDecision": "deny",
63 "permissionDecisionReason": "Cannot modify .env files",
64 }
65 }
66 
67 # Return empty object to allow the operation
68 return {}
69 
70 
71 async def main():
72 options = ClaudeAgentOptions(
73 hooks={
74 # Register the hook for PreToolUse events
75 # The matcher filters to only Write and Edit tool calls
76 "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
77 }
78 )
79 
80 async with ClaudeSDKClient(options=options) as client:
81 await client.query("Create a .env file with the standard local development database configuration")
82 async for message in client.receive_response():
83 # Filter for assistant and result messages
84 if isinstance(message, (AssistantMessage, ResultMessage)):
85 print(message)
86 
87 
88 asyncio.run(main())
89 ```
90 
91 ```typescript TypeScript theme={null}
92 import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
93 
94 // Define a hook callback with the HookCallback type
95 const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
96 // Cast input to the specific hook type for type safety
97 const preInput = input as PreToolUseHookInput;
98 
99 // Cast tool_input to access its properties (typed as unknown in the SDK)
100 const toolInput = preInput.tool_input as Record<string, unknown>;
101 const filePath = toolInput?.file_path as string;
102 const fileName = filePath?.split("/").pop();
103 
104 // Block the operation if targeting a .env file
105 if (fileName?.toLowerCase() === ".env") {
106 return {
107 hookSpecificOutput: {
108 hookEventName: preInput.hook_event_name,
109 permissionDecision: "deny",
110 permissionDecisionReason: "Cannot modify .env files"
37```python Python theme={null}
38import asyncio
39from claude_agent_sdk import (
40 AssistantMessage,
41 ClaudeSDKClient,
42 ClaudeAgentOptions,
43 HookMatcher,
44 ResultMessage,
45)
46 
47 
48# Define a hook callback that receives tool call details
49async def protect_env_files(input_data, tool_use_id, context):
50 # Extract the file path from the tool's input arguments
51 file_path = input_data["tool_input"].get("file_path", "")
52 file_name = file_path.split("/")[-1]
53 
54 # Block the operation if targeting a .env file
55 if file_name.lower() == ".env":
56 return {
57 "hookSpecificOutput": {
58 "hookEventName": input_data["hook_event_name"],
59 "permissionDecision": "deny",
60 "permissionDecisionReason": "Cannot modify .env files",
61 }
11162 }
112 };
63 
64 # Return empty object to allow the operation
65 return {}
66 
67 
68async def main():
69 options = ClaudeAgentOptions(
70 hooks={
71 # Register the hook for PreToolUse events
72 # The matcher filters to only Write and Edit tool calls
73 "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
74 }
75 )
76 
77 async with ClaudeSDKClient(options=options) as client:
78 await client.query("Create a .env file with the standard local development database configuration")
79 async for message in client.receive_response():
80 # Filter for assistant and result messages
81 if isinstance(message, (AssistantMessage, ResultMessage)):
82 print(message)
83 
84 
85asyncio.run(main())
86```
87 
88```typescript TypeScript theme={null}
89import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
90 
91// Define a hook callback with the HookCallback type
92const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
93 // Cast input to the specific hook type for type safety
94 const preInput = input as PreToolUseHookInput;
95 
96 // Cast tool_input to access its properties (typed as unknown in the SDK)
97 const toolInput = preInput.tool_input as Record<string, unknown>;
98 const filePath = toolInput?.file_path as string;
99 const fileName = filePath?.split("/").pop();
100 
101 // Block the operation if targeting a .env file
102 if (fileName?.toLowerCase() === ".env") {
103 return {
104 hookSpecificOutput: {
105 hookEventName: preInput.hook_event_name,
106 permissionDecision: "deny",
107 permissionDecisionReason: "Cannot modify .env files"
108 }
109 };
110 }
111 
112 // Return empty object to allow the operation
113 return {};
114};
115 
116for await (const message of query({
117 prompt: "Create a .env file with the standard local development database configuration",
118 options: {
119 hooks: {
120 // Register the hook for PreToolUse events
121 // The matcher filters to only Write and Edit tool calls
122 PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
113123 }
114 
115 // Return empty object to allow the operation
116 return {};
117 };
118 
119 for await (const message of query({
120 prompt: "Create a .env file with the standard local development database configuration",
121 options: {
122 hooks: {
123 // Register the hook for PreToolUse events
124 // The matcher filters to only Write and Edit tool calls
125 PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
126 }
127 }
128 })) {
129 // Filter for assistant and result messages
130 if (message.type === "assistant" || message.type === "result") {
131 console.log(message);
132 }
133 }
134 ```
135</CodeGroup>
124 }
125})) {
126 // Filter for assistant and result messages
127 if (message.type === "assistant" || message.type === "result") {
128 console.log(message);
129 }
130}
131```
136132 
137133The hook denies a Write or Edit call whose `file_path` ends in `/.env`, in any letter case. On Windows, `file_path` uses backslashes, such as `C:\project\.env`, and this hook allows a Write or Edit call to `C:\project\.env`.
138134 
139<Warning>
140 The `Write|Edit` matcher limits this hook to the Write and Edit tools. If Claude writes `.env` with a Bash command, the hook doesn't run. To block Bash writes to a file where [the sandbox runs](/docs/en/sandboxing), see the sandbox configuration for [Python](/docs/en/agent-sdk/python#sandbox-configuration) or [TypeScript](/docs/en/agent-sdk/typescript#sandbox-configuration).
141</Warning>
135> [!WARNING]
136>
137> The `Write|Edit` matcher limits this hook to the Write and Edit tools. If Claude writes `.env` with a Bash command, the hook doesn't run. To block Bash writes to a file where [the sandbox runs](/docs/en/sandboxing), see the sandbox configuration for [Python](/docs/en/agent-sdk/python#sandbox-configuration) or [TypeScript](/docs/en/agent-sdk/typescript#sandbox-configuration).
142138 
143139## Available hooks
144140 
from line 180
184180 
185181To configure a hook, pass it in the `hooks` field of your agent options (`ClaudeAgentOptions` in Python, the `options` object in TypeScript). This snippet assumes you have already defined a hook callback, like `protect_env_files` in Python or `protectEnvFiles` in TypeScript from the example above:
186182 
187<CodeGroup>
188 ```python Python theme={null}
189 options = ClaudeAgentOptions(
190 hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
191 )
192 
193 async with ClaudeSDKClient(options=options) as client:
194 await client.query("Your prompt")
195 async for message in client.receive_response():
196 print(message)
197 ```
198 
199 ```typescript TypeScript theme={null}
200 for await (const message of query({
201 prompt: "Your prompt",
202 options: {
203 hooks: {
204 PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
205 }
183```python Python theme={null}
184options = ClaudeAgentOptions(
185 hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
186)
187 
188async with ClaudeSDKClient(options=options) as client:
189 await client.query("Your prompt")
190 async for message in client.receive_response():
191 print(message)
192```
193 
194```typescript TypeScript theme={null}
195for await (const message of query({
196 prompt: "Your prompt",
197 options: {
198 hooks: {
199 PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
206200 }
207 })) {
208 console.log(message);
209 }
210 ```
211</CodeGroup>
201 }
202})) {
203 console.log(message);
204}
205```
212206 
213207The `hooks` option is a dictionary in Python or an object in TypeScript, where:
214208 
from line 247
253247 
254248Return `{}` to allow the operation without changes. SDK callback hooks use the same JSON output format as [Claude Code shell command hooks](/docs/en/hooks#json-output), which documents every field and event-specific option. For the SDK type definitions, see the [TypeScript](/docs/en/agent-sdk/typescript#synchookjsonoutput) and [Python](/docs/en/agent-sdk/python#synchookjsonoutput) SDK references.
255249 
256<Note>
257 When multiple `PreToolUse` hooks return different [decisions](/docs/en/hooks#pretooluse-decision-control), `deny` takes priority over `defer`, which takes priority over `ask`, which takes priority over `allow`.
258</Note>
250> [!NOTE]
251>
252> When multiple `PreToolUse` hooks return different [decisions](/docs/en/hooks#pretooluse-decision-control), `deny` takes priority over `defer`, which takes priority over `ask`, which takes priority over `allow`.
259253 
260254#### Asynchronous output
261255 
262256By default, the agent waits for your hook to return before proceeding. If your hook performs a side effect, such as logging or sending a webhook, and doesn't need to influence the agent's behavior, you can return an async output instead. This tells the agent to continue immediately without waiting for the hook to finish. In this snippet, `send_to_logging_service` in Python and `sendToLoggingService` in TypeScript stand in for any logging function you define:
263257 
264<CodeGroup>
265 ```python Python theme={null}
266 async def async_hook(input_data, tool_use_id, context):
267 # Start a background task, then return immediately
268 asyncio.create_task(send_to_logging_service(input_data))
269 return {"async_": True, "asyncTimeout": 30000}
270 ```
271 
272 ```typescript TypeScript theme={null}
273 const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
274 // Start a background task, then return immediately
275 sendToLoggingService(input).catch(console.error);
276 return { async: true, asyncTimeout: 30000 };
277 };
278 ```
279</CodeGroup>
258```python Python theme={null}
259async def async_hook(input_data, tool_use_id, context):
260 # Start a background task, then return immediately
261 asyncio.create_task(send_to_logging_service(input_data))
262 return {"async_": True, "asyncTimeout": 30000}
263```
264 
265```typescript TypeScript theme={null}
266const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
267 // Start a background task, then return immediately
268 sendToLoggingService(input).catch(console.error);
269 return { async: true, asyncTimeout: 30000 };
270};
271```
280272 
281273| Field | Type | Description |
282274| - | - | - |
283275| `async` | `true` | Signals async mode. The agent proceeds without waiting. In Python, use `async_` to avoid the reserved keyword. |
284276| `asyncTimeout` | `number` | Optional timeout in milliseconds for the background operation |
285277 
286<Note>
287 Async outputs can't block, modify, or inject context into the operation since the agent has already moved on. Use them only for side effects like logging, metrics, or notifications.
288</Note>
278> [!NOTE]
279>
280> Async outputs can't block, modify, or inject context into the operation since the agent has already moved on. Use them only for side effects like logging, metrics, or notifications.
289281 
290282## Examples
291283 
from line 287
295287 
296288This example intercepts Write tool calls and rewrites the `file_path` argument to prepend `/sandbox`. The callback returns `updatedInput` with the modified path and `permissionDecision: 'allow'` to auto-approve the rewritten operation:
297289 
298<CodeGroup>
299 ```python Python theme={null}
300 async def redirect_to_sandbox(input_data, tool_use_id, context):
301 if input_data["hook_event_name"] != "PreToolUse":
302 return {}
303 
304 if input_data["tool_name"] == "Write":
305 original_path = input_data["tool_input"].get("file_path", "")
306 return {
307 "hookSpecificOutput": {
308 "hookEventName": input_data["hook_event_name"],
309 "permissionDecision": "allow",
310 "updatedInput": {
311 **input_data["tool_input"],
312 "file_path": f"/sandbox{original_path}",
313 },
314 }
315 }
316 return {}
317 ```
318 
319 ```typescript TypeScript theme={null}
320 const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
321 if (input.hook_event_name !== "PreToolUse") return {};
322 
323 const preInput = input as PreToolUseHookInput;
324 const toolInput = preInput.tool_input as Record<string, unknown>;
325 if (preInput.tool_name === "Write") {
326 const originalPath = toolInput.file_path as string;
327 return {
328 hookSpecificOutput: {
329 hookEventName: preInput.hook_event_name,
330 permissionDecision: "allow",
331 updatedInput: {
332 ...toolInput,
333 file_path: `/sandbox${originalPath}`
334 }
290```python Python theme={null}
291async def redirect_to_sandbox(input_data, tool_use_id, context):
292 if input_data["hook_event_name"] != "PreToolUse":
293 return {}
294 
295 if input_data["tool_name"] == "Write":
296 original_path = input_data["tool_input"].get("file_path", "")
297 return {
298 "hookSpecificOutput": {
299 "hookEventName": input_data["hook_event_name"],
300 "permissionDecision": "allow",
301 "updatedInput": {
302 **input_data["tool_input"],
303 "file_path": f"/sandbox{original_path}",
304 },
305 }
335306 }
336 };
337 }
338 return {};
339 };
340 ```
341</CodeGroup>
342 
343<Note>
344 Pair `updatedInput` with `permissionDecision: 'allow'` to auto-approve the modified input, or `permissionDecision: 'ask'` to show it to the user. If you omit `permissionDecision`, the modified input still applies and flows through the normal permission evaluation. With `'defer'`, `updatedInput` is ignored. Always return a new object rather than mutating the original `tool_input`.
345</Note>
307 return {}
308```
309 
310```typescript TypeScript theme={null}
311const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
312 if (input.hook_event_name !== "PreToolUse") return {};
313 
314 const preInput = input as PreToolUseHookInput;
315 const toolInput = preInput.tool_input as Record<string, unknown>;
316 if (preInput.tool_name === "Write") {
317 const originalPath = toolInput.file_path as string;
318 return {
319 hookSpecificOutput: {
320 hookEventName: preInput.hook_event_name,
321 permissionDecision: "allow",
322 updatedInput: {
323 ...toolInput,
324 file_path: `/sandbox${originalPath}`
325 }
326 }
327 };
328 }
329 return {};
330};
331```
332 
333> [!NOTE]
334>
335> Pair `updatedInput` with `permissionDecision: 'allow'` to auto-approve the modified input, or `permissionDecision: 'ask'` to show it to the user. If you omit `permissionDecision`, the modified input still applies and flows through the normal permission evaluation. With `'defer'`, `updatedInput` is ignored. Always return a new object rather than mutating the original `tool_input`.
346336 
347337To confirm the redirect, set the prefix to a path you can write to, such as `./sandbox` or `/tmp/sandbox` (macOS doesn't allow creating a root-level `/sandbox` directory), then ask the agent to write a file: the Write tool's result in the message stream names the path with your sandbox prefix rather than the one Claude requested.
348338 
from line 344
354344* `permissionDecisionReason` tells the model why.
355345* `systemMessage` shows the user what happened.
356346 
357<CodeGroup>
358 ```python Python theme={null}
359 async def block_etc_writes(input_data, tool_use_id, context):
360 file_path = input_data["tool_input"].get("file_path", "")
361 
362 if file_path.startswith("/etc"):
363 return {
364 # Top-level field: message shown to the user
365 "systemMessage": "Remember: system directories like /etc are protected.",
366 # hookSpecificOutput: block the operation
367 "hookSpecificOutput": {
368 "hookEventName": input_data["hook_event_name"],
369 "permissionDecision": "deny",
370 "permissionDecisionReason": "Writing to /etc is not allowed",
371 },
372 }
373 return {}
374 ```
375 
376 ```typescript TypeScript theme={null}
377 const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
378 const preInput = input as PreToolUseHookInput;
379 const toolInput = preInput.tool_input as Record<string, unknown>;
380 const filePath = toolInput?.file_path as string;
381 
382 if (filePath?.startsWith("/etc")) {
383 return {
384 // Top-level field: message shown to the user
385 systemMessage: "Remember: system directories like /etc are protected.",
386 // hookSpecificOutput: block the operation
387 hookSpecificOutput: {
388 hookEventName: preInput.hook_event_name,
389 permissionDecision: "deny",
390 permissionDecisionReason: "Writing to /etc is not allowed"
347```python Python theme={null}
348async def block_etc_writes(input_data, tool_use_id, context):
349 file_path = input_data["tool_input"].get("file_path", "")
350 
351 if file_path.startswith("/etc"):
352 return {
353 # Top-level field: message shown to the user
354 "systemMessage": "Remember: system directories like /etc are protected.",
355 # hookSpecificOutput: block the operation
356 "hookSpecificOutput": {
357 "hookEventName": input_data["hook_event_name"],
358 "permissionDecision": "deny",
359 "permissionDecisionReason": "Writing to /etc is not allowed",
360 },
391361 }
392 };
362 return {}
363```
364 
365```typescript TypeScript theme={null}
366const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
367 const preInput = input as PreToolUseHookInput;
368 const toolInput = preInput.tool_input as Record<string, unknown>;
369 const filePath = toolInput?.file_path as string;
370 
371 if (filePath?.startsWith("/etc")) {
372 return {
373 // Top-level field: message shown to the user
374 systemMessage: "Remember: system directories like /etc are protected.",
375 // hookSpecificOutput: block the operation
376 hookSpecificOutput: {
377 hookEventName: preInput.hook_event_name,
378 permissionDecision: "deny",
379 permissionDecisionReason: "Writing to /etc is not allowed"
380 }
381 };
382 }
383 return {};
384};
385```
386 
387To confirm the block, register the callback under `PreToolUse` with a `Write|Edit` matcher and ask the agent to create a file under `/etc`: the Write tool's result in the message stream contains `Writing to /etc is not allowed`, and no file is created.
388 
389### Auto-approve specific tools
390 
391By default, the agent may prompt for permission before using certain tools. This example auto-approves read-only filesystem tools (Read, Glob, Grep) by returning `permissionDecision: 'allow'`, letting them run without user confirmation, apart from reads from [network paths](/docs/en/permissions#network-paths), while leaving all other tools subject to normal permission checks:
392 
393```python Python theme={null}
394async def auto_approve_read_only(input_data, tool_use_id, context):
395 if input_data["hook_event_name"] != "PreToolUse":
396 return {}
397 
398 read_only_tools = ["Read", "Glob", "Grep"]
399 if input_data["tool_name"] in read_only_tools:
400 return {
401 "hookSpecificOutput": {
402 "hookEventName": input_data["hook_event_name"],
403 "permissionDecision": "allow",
404 "permissionDecisionReason": "Read-only tool auto-approved",
405 }
406 }
407 return {}
408```
409 
410```typescript TypeScript theme={null}
411const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
412 if (input.hook_event_name !== "PreToolUse") return {};
413 
414 const preInput = input as PreToolUseHookInput;
415 const readOnlyTools = ["Read", "Glob", "Grep"];
416 if (readOnlyTools.includes(preInput.tool_name)) {
417 return {
418 hookSpecificOutput: {
419 hookEventName: preInput.hook_event_name,
420 permissionDecision: "allow",
421 permissionDecisionReason: "Read-only tool auto-approved"
422 }
423 };
424 }
425 return {};
426};
427```
428 
429### Register multiple hooks
430 
431When an event fires, the matching hooks run in parallel. For permission decisions, the most restrictive result applies. One hook's `deny` blocks the tool call. Because completion order is non-deterministic, write each hook to act independently rather than relying on another hook having run first.
432 
433The example below registers three independent checks, each with an [omitted matcher](/docs/en/hooks#matcher-patterns). The hook names in it, such as `audit_logger` in Python or `auditLogger` in TypeScript, stand in for callbacks you define:
434 
435```python Python theme={null}
436options = ClaudeAgentOptions(
437 hooks={
438 "PreToolUse": [
439 HookMatcher(hooks=[authorization_check]),
440 HookMatcher(hooks=[input_validator]),
441 HookMatcher(hooks=[audit_logger]),
442 ]
393443 }
394 return {};
395 };
396 ```
397</CodeGroup>
398 
399To confirm the block, register the callback under `PreToolUse` with a `Write|Edit` matcher and ask the agent to create a file under `/etc`: the Write tool's result in the message stream contains `Writing to /etc is not allowed`, and no file is created.
400 
401### Auto-approve specific tools
402 
403By default, the agent may prompt for permission before using certain tools. This example auto-approves read-only filesystem tools (Read, Glob, Grep) by returning `permissionDecision: 'allow'`, letting them run without user confirmation, apart from reads from [network paths](/docs/en/permissions#network-paths), while leaving all other tools subject to normal permission checks:
404 
405<CodeGroup>
406 ```python Python theme={null}
407 async def auto_approve_read_only(input_data, tool_use_id, context):
408 if input_data["hook_event_name"] != "PreToolUse":
409 return {}
410 
411 read_only_tools = ["Read", "Glob", "Grep"]
412 if input_data["tool_name"] in read_only_tools:
413 return {
414 "hookSpecificOutput": {
415 "hookEventName": input_data["hook_event_name"],
416 "permissionDecision": "allow",
417 "permissionDecisionReason": "Read-only tool auto-approved",
418 }
419 }
420 return {}
421 ```
422 
423 ```typescript TypeScript theme={null}
424 const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
425 if (input.hook_event_name !== "PreToolUse") return {};
426 
427 const preInput = input as PreToolUseHookInput;
428 const readOnlyTools = ["Read", "Glob", "Grep"];
429 if (readOnlyTools.includes(preInput.tool_name)) {
430 return {
431 hookSpecificOutput: {
432 hookEventName: preInput.hook_event_name,
433 permissionDecision: "allow",
434 permissionDecisionReason: "Read-only tool auto-approved"
435 }
436 };
437 }
438 return {};
439 };
440 ```
441</CodeGroup>
442 
443### Register multiple hooks
444 
445When an event fires, the matching hooks run in parallel. For permission decisions, the most restrictive result applies. One hook's `deny` blocks the tool call. Because completion order is non-deterministic, write each hook to act independently rather than relying on another hook having run first.
446 
447The example below registers three independent checks, each with an [omitted matcher](/docs/en/hooks#matcher-patterns). The hook names in it, such as `audit_logger` in Python or `auditLogger` in TypeScript, stand in for callbacks you define:
448 
449<CodeGroup>
450 ```python Python theme={null}
451 options = ClaudeAgentOptions(
452 hooks={
453 "PreToolUse": [
454 HookMatcher(hooks=[authorization_check]),
455 HookMatcher(hooks=[input_validator]),
456 HookMatcher(hooks=[audit_logger]),
457 ]
458 }
459 )
460 ```
461 
462 ```typescript TypeScript theme={null}
463 const options = {
464 hooks: {
465 PreToolUse: [
466 { hooks: [authorizationCheck] },
467 { hooks: [inputValidator] },
468 { hooks: [auditLogger] }
469 ]
470 }
471 };
472 ```
473</CodeGroup>
444)
445```
446 
447```typescript TypeScript theme={null}
448const options = {
449 hooks: {
450 PreToolUse: [
451 { hooks: [authorizationCheck] },
452 { hooks: [inputValidator] },
453 { hooks: [auditLogger] }
454 ]
455 }
456};
457```
474458 
475459### Filter with multi-tool matchers
476460 
from line 464
480464* A regex (`^mcp__`) triggers `mcp_audit_hook` for any MCP tool whose name starts with `mcp__`.
481465* An omitted matcher triggers `global_logger` without filtering by tool name.
482466 
483<CodeGroup>
484 ```python Python theme={null}
485 options = ClaudeAgentOptions(
486 hooks={
487 "PreToolUse": [
488 # Match file modification tools
489 HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
490 # Match all MCP tools
491 HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
492 # Match everything (no matcher)
493 HookMatcher(hooks=[global_logger]),
494 ]
495 }
496 )
497 ```
498 
499 ```typescript TypeScript theme={null}
500 const options = {
467```python Python theme={null}
468options = ClaudeAgentOptions(
469 hooks={
470 "PreToolUse": [
471 # Match file modification tools
472 HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
473 # Match all MCP tools
474 HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
475 # Match everything (no matcher)
476 HookMatcher(hooks=[global_logger]),
477 ]
478 }
479)
480```
481 
482```typescript TypeScript theme={null}
483const options = {
484 hooks: {
485 PreToolUse: [
486 // Match file modification tools
487 { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
488 
489 // Match all MCP tools
490 { matcher: "^mcp__", hooks: [mcpAuditHook] },
491 
492 // Match everything (no matcher)
493 { hooks: [globalLogger] }
494 ]
495 }
496};
497```
498 
499### Track subagent activity
500 
501Use `SubagentStop` hooks to monitor when subagents finish their work. See the full input type in the [TypeScript](/docs/en/agent-sdk/typescript#hookinput) and [Python](/docs/en/agent-sdk/python#hookinput) SDK references. This example logs a summary each time a subagent completes:
502 
503```python Python theme={null}
504async def subagent_tracker(input_data, tool_use_id, context):
505 # Log subagent details when it finishes
506 print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
507 print(f" Transcript: {input_data['agent_transcript_path']}")
508 print(f" Tool use ID: {tool_use_id}")
509 print(f" Stop hook active: {input_data.get('stop_hook_active')}")
510 return {}
511 
512 
513options = ClaudeAgentOptions(
514 hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
515)
516```
517 
518```typescript TypeScript theme={null}
519import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";
520 
521const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
522 // Cast to SubagentStopHookInput to access subagent-specific fields
523 const subInput = input as SubagentStopHookInput;
524 
525 // Log subagent details when it finishes
526 console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
527 console.log(` Transcript: ${subInput.agent_transcript_path}`);
528 console.log(` Tool use ID: ${toolUseID}`);
529 console.log(` Stop hook active: ${subInput.stop_hook_active}`);
530 return {};
531};
532 
533const options = {
534 hooks: {
535 SubagentStop: [{ hooks: [subagentTracker] }]
536 }
537};
538```
539 
540To confirm the hook fires, register the callback and ask the agent to delegate a small task to a subagent, such as listing the files in the current directory: when the subagent finishes, the callback prints the `[SUBAGENT] Completed:` lines with the subagent's ID and transcript path.
541 
542### Make HTTP requests from hooks
543 
544Hooks can perform asynchronous operations like HTTP requests. Catch errors inside your hook instead of letting them propagate.
545 
546This example sends a webhook after each tool completes, logging which tool ran and when. The hook catches errors from a failed webhook:
547 
548```python Python theme={null}
549import asyncio
550import json
551import urllib.request
552from datetime import datetime
553 
554 
555def _send_webhook(tool_name):
556 """Synchronous helper that POSTs tool usage data to an external webhook."""
557 data = json.dumps(
558 {
559 "tool": tool_name,
560 "timestamp": datetime.now().isoformat(),
561 }
562 ).encode()
563 req = urllib.request.Request(
564 "https://api.example.com/webhook",
565 data=data,
566 headers={"Content-Type": "application/json"},
567 method="POST",
568 )
569 urllib.request.urlopen(req)
570 
571 
572async def webhook_notifier(input_data, tool_use_id, context):
573 # Only fire after a tool completes (PostToolUse), not before
574 if input_data["hook_event_name"] != "PostToolUse":
575 return {}
576 
577 try:
578 # Run the blocking HTTP call in a thread to avoid blocking the event loop
579 await asyncio.to_thread(_send_webhook, input_data["tool_name"])
580 except Exception as e:
581 # Log the error but don't raise
582 print(f"Webhook request failed: {e}")
583 
584 return {}
585```
586 
587```typescript TypeScript theme={null}
588import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
589 
590const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
591 // Only fire after a tool completes (PostToolUse), not before
592 if (input.hook_event_name !== "PostToolUse") return {};
593 
594 try {
595 await fetch("https://api.example.com/webhook", {
596 method: "POST",
597 headers: { "Content-Type": "application/json" },
598 body: JSON.stringify({
599 tool: (input as PostToolUseHookInput).tool_name,
600 timestamp: new Date().toISOString()
601 }),
602 // Pass signal so the request cancels if the hook times out
603 signal
604 });
605 } catch (error) {
606 // Handle cancellation separately from other errors
607 if (error instanceof Error && error.name === "AbortError") {
608 console.log("Webhook request cancelled");
609 }
610 // Don't re-throw
611 }
612 
613 return {};
614};
615 
616// Register as a PostToolUse hook
617for await (const message of query({
618 prompt: "Refactor the auth module",
619 options: {
501620 hooks: {
502 PreToolUse: [
503 // Match file modification tools
504 { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
505 
506 // Match all MCP tools
507 { matcher: "^mcp__", hooks: [mcpAuditHook] },
508 
509 // Match everything (no matcher)
510 { hooks: [globalLogger] }
511 ]
621 PostToolUse: [{ hooks: [webhookNotifier] }]
512622 }
513 };
514 ```
515</CodeGroup>
516 
517### Track subagent activity
518 
519Use `SubagentStop` hooks to monitor when subagents finish their work. See the full input type in the [TypeScript](/docs/en/agent-sdk/typescript#hookinput) and [Python](/docs/en/agent-sdk/python#hookinput) SDK references. This example logs a summary each time a subagent completes:
520 
521<CodeGroup>
522 ```python Python theme={null}
523 async def subagent_tracker(input_data, tool_use_id, context):
524 # Log subagent details when it finishes
525 print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
526 print(f" Transcript: {input_data['agent_transcript_path']}")
527 print(f" Tool use ID: {tool_use_id}")
528 print(f" Stop hook active: {input_data.get('stop_hook_active')}")
529 return {}
530 
531 
532 options = ClaudeAgentOptions(
533 hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
534 )
535 ```
536 
537 ```typescript TypeScript theme={null}
538 import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";
539 
540 const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
541 // Cast to SubagentStopHookInput to access subagent-specific fields
542 const subInput = input as SubagentStopHookInput;
543 
544 // Log subagent details when it finishes
545 console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
546 console.log(` Transcript: ${subInput.agent_transcript_path}`);
547 console.log(` Tool use ID: ${toolUseID}`);
548 console.log(` Stop hook active: ${subInput.stop_hook_active}`);
549 return {};
550 };
551 
552 const options = {
553 hooks: {
554 SubagentStop: [{ hooks: [subagentTracker] }]
555 }
556 };
557 ```
558</CodeGroup>
559 
560To confirm the hook fires, register the callback and ask the agent to delegate a small task to a subagent, such as listing the files in the current directory: when the subagent finishes, the callback prints the `[SUBAGENT] Completed:` lines with the subagent's ID and transcript path.
561 
562### Make HTTP requests from hooks
563 
564Hooks can perform asynchronous operations like HTTP requests. Catch errors inside your hook instead of letting them propagate.
565 
566This example sends a webhook after each tool completes, logging which tool ran and when. The hook catches errors from a failed webhook:
567 
568<CodeGroup>
569 ```python Python theme={null}
570 import asyncio
571 import json
572 import urllib.request
573 from datetime import datetime
574 
575 
576 def _send_webhook(tool_name):
577 """Synchronous helper that POSTs tool usage data to an external webhook."""
578 data = json.dumps(
579 {
580 "tool": tool_name,
581 "timestamp": datetime.now().isoformat(),
582 }
583 ).encode()
584 req = urllib.request.Request(
585 "https://api.example.com/webhook",
586 data=data,
587 headers={"Content-Type": "application/json"},
588 method="POST",
589 )
590 urllib.request.urlopen(req)
591 
592 
593 async def webhook_notifier(input_data, tool_use_id, context):
594 # Only fire after a tool completes (PostToolUse), not before
595 if input_data["hook_event_name"] != "PostToolUse":
596 return {}
597 
598 try:
599 # Run the blocking HTTP call in a thread to avoid blocking the event loop
600 await asyncio.to_thread(_send_webhook, input_data["tool_name"])
601 except Exception as e:
602 # Log the error but don't raise
603 print(f"Webhook request failed: {e}")
604 
605 return {}
606 ```
607 
608 ```typescript TypeScript theme={null}
609 import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
610 
611 const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
612 // Only fire after a tool completes (PostToolUse), not before
613 if (input.hook_event_name !== "PostToolUse") return {};
614 
615 try {
616 await fetch("https://api.example.com/webhook", {
617 method: "POST",
618 headers: { "Content-Type": "application/json" },
619 body: JSON.stringify({
620 tool: (input as PostToolUseHookInput).tool_name,
621 timestamp: new Date().toISOString()
622 }),
623 // Pass signal so the request cancels if the hook times out
624 signal
625 });
626 } catch (error) {
627 // Handle cancellation separately from other errors
628 if (error instanceof Error && error.name === "AbortError") {
629 console.log("Webhook request cancelled");
630 }
631 // Don't re-throw
632 }
633 
634 return {};
635 };
636 
637 // Register as a PostToolUse hook
638 for await (const message of query({
639 prompt: "Refactor the auth module",
640 options: {
641 hooks: {
642 PostToolUse: [{ hooks: [webhookNotifier] }]
643 }
644 }
645 })) {
646 console.log(message);
647 }
648 ```
649</CodeGroup>
623 }
624})) {
625 console.log(message);
626}
627```
650628 
651629To confirm the hook fires, point the webhook URL at an endpoint you can watch and send a prompt that uses a tool: the hook sends a POST with the tool name and timestamp after each tool completes.
652630 
from line 641
663641 
664642This example forwards every notification to a Slack channel. It requires a [Slack incoming webhook URL](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/), which you create by adding an app to your Slack workspace and enabling incoming webhooks:
665643 
666<CodeGroup>
667 ```python Python theme={null}
668 import asyncio
669 import json
670 import urllib.request
671 
672 from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
673 
674 
675 def _send_slack_notification(message):
676 """Synchronous helper that sends a message to Slack via incoming webhook."""
677 data = json.dumps({"text": f"Agent status: {message}"}).encode()
678 req = urllib.request.Request(
679 "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
680 data=data,
681 headers={"Content-Type": "application/json"},
682 method="POST",
683 )
684 urllib.request.urlopen(req)
685 
686 
687 async def notification_handler(input_data, tool_use_id, context):
688 try:
689 # Run the blocking HTTP call in a thread to avoid blocking the event loop
690 await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
691 except Exception as e:
692 print(f"Failed to send notification: {e}")
693 
694 # Return empty object. Notification hooks don't modify agent behavior
695 return {}
696 
697 
698 async def main():
699 options = ClaudeAgentOptions(
700 hooks={
701 # Register the hook for Notification events (no matcher needed)
702 "Notification": [HookMatcher(hooks=[notification_handler])],
703 },
704 )
705 
706 async with ClaudeSDKClient(options=options) as client:
707 await client.query("Analyze this codebase")
708 async for message in client.receive_response():
709 print(message)
710 
711 
712 asyncio.run(main())
713 ```
714 
715 ```typescript TypeScript theme={null}
716 import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
717 
718 // Define a hook callback that sends notifications to Slack
719 const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
720 // Cast to NotificationHookInput to access the message field
721 const notification = input as NotificationHookInput;
722 
723 try {
724 // POST the notification message to a Slack incoming webhook
725 await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
726 method: "POST",
727 headers: { "Content-Type": "application/json" },
728 body: JSON.stringify({
729 text: `Agent status: ${notification.message}`
730 }),
731 // Pass signal so the request cancels if the hook times out
732 signal
733 });
734 } catch (error) {
735 if (error instanceof Error && error.name === "AbortError") {
736 console.log("Notification cancelled");
737 } else {
738 console.error("Failed to send notification:", error);
739 }
644```python Python theme={null}
645import asyncio
646import json
647import urllib.request
648 
649from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
650 
651 
652def _send_slack_notification(message):
653 """Synchronous helper that sends a message to Slack via incoming webhook."""
654 data = json.dumps({"text": f"Agent status: {message}"}).encode()
655 req = urllib.request.Request(
656 "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
657 data=data,
658 headers={"Content-Type": "application/json"},
659 method="POST",
660 )
661 urllib.request.urlopen(req)
662 
663 
664async def notification_handler(input_data, tool_use_id, context):
665 try:
666 # Run the blocking HTTP call in a thread to avoid blocking the event loop
667 await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
668 except Exception as e:
669 print(f"Failed to send notification: {e}")
670 
671 # Return empty object. Notification hooks don't modify agent behavior
672 return {}
673 
674 
675async def main():
676 options = ClaudeAgentOptions(
677 hooks={
678 # Register the hook for Notification events (no matcher needed)
679 "Notification": [HookMatcher(hooks=[notification_handler])],
680 },
681 )
682 
683 async with ClaudeSDKClient(options=options) as client:
684 await client.query("Analyze this codebase")
685 async for message in client.receive_response():
686 print(message)
687 
688 
689asyncio.run(main())
690```
691 
692```typescript TypeScript theme={null}
693import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
694 
695// Define a hook callback that sends notifications to Slack
696const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
697 // Cast to NotificationHookInput to access the message field
698 const notification = input as NotificationHookInput;
699 
700 try {
701 // POST the notification message to a Slack incoming webhook
702 await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
703 method: "POST",
704 headers: { "Content-Type": "application/json" },
705 body: JSON.stringify({
706 text: `Agent status: ${notification.message}`
707 }),
708 // Pass signal so the request cancels if the hook times out
709 signal
710 });
711 } catch (error) {
712 if (error instanceof Error && error.name === "AbortError") {
713 console.log("Notification cancelled");
714 } else {
715 console.error("Failed to send notification:", error);
740716 }
741 
742 // Return empty object. Notification hooks don't modify agent behavior
743 return {};
744 };
745 
746 // Register the hook for Notification events (no matcher needed)
747 for await (const message of query({
748 prompt: "Analyze this codebase",
749 options: {
750 hooks: {
751 Notification: [{ hooks: [notificationHandler] }]
752 }
717 }
718 
719 // Return empty object. Notification hooks don't modify agent behavior
720 return {};
721};
722 
723// Register the hook for Notification events (no matcher needed)
724for await (const message of query({
725 prompt: "Analyze this codebase",
726 options: {
727 hooks: {
728 Notification: [{ hooks: [notificationHandler] }]
753729 }
754 })) {
755 console.log(message);
756 }
757 ```
758</CodeGroup>
730 }
731})) {
732 console.log(message);
733}
734```
759735 
760736When a `Notification` event fires, the hook posts the notification's `message`, prefixed with `Agent status:`, to the channel your webhook targets.
761737 
from line 808
832808 
833809`SessionStart` and `SessionEnd` can be registered as SDK callback hooks in TypeScript, but aren't available in the Python SDK because its `HookEvent` type omits them. In Python, they are only available as [shell command hooks](/docs/en/hooks#hook-events) defined in settings files such as `.claude/settings.json`. Which settings files your SDK application loads depends on [`setting_sources`](/docs/en/agent-sdk/python#settingsource) or [`settingSources`](/docs/en/agent-sdk/typescript#settingsource). If you set that option, include the source that holds the hooks:
834810 
835<CodeGroup>
836 ```python Python theme={null}
837 options = ClaudeAgentOptions(
838 setting_sources=["project"], # Loads .claude/settings.json including hooks
839 )
840 ```
841 
842 ```typescript TypeScript theme={null}
843 const options = {
844 settingSources: ["project"] // Loads .claude/settings.json including hooks
845 };
846 ```
847</CodeGroup>
811```python Python theme={null}
812options = ClaudeAgentOptions(
813 setting_sources=["project"], # Loads .claude/settings.json including hooks
814)
815```
816 
817```typescript TypeScript theme={null}
818const options = {
819 settingSources: ["project"] // Loads .claude/settings.json including hooks
820};
821```
848822 
849823To run initialization logic as a Python SDK callback instead, use the first message from `client.receive_response()` as your trigger.
850824 
851825 
Feedback