What Are Hooks in the Claude Agent SDK
Hooks run functions at specific points in agent execution. Understand tool hooks vs lifecycle hooks, how matchers work, and where hooks fit in the tool evaluation order.
What hooks actually are
Hooks are functions that run at specific moments in your agent's execution lifecycle. They fire automatically at designated points — before a tool runs, after a sub-agent finishes, when a session starts.
The simplest way to think about it: if permission modes and tool lists control what the agent can do, hooks control what happens around what the agent does.
That distinction matters. You cannot use a permission mode to log every file write to an audit trail. You cannot use a tool list to automatically inject context before a specific tool call. Those are hook problems.
The hook event categories
The SDK organizes hooks into two categories:
Tool hooks fire around individual tool calls. The most commonly used is preToolUse, which runs before a tool executes. Use it to allow or block specific tool calls based on their inputs, log what the agent is doing, modify tool inputs before they run, or build audit trails.
Lifecycle hooks fire at structural moments in the agent's execution. Examples:
session_start— fires once when a new session beginsuserpromptsubmit— fires each time a user sends a messagesubagent_stop— fires when a sub-agent finishes its work
Where hooks fit in the execution order
When the agent wants to use a tool, the SDK evaluates in this sequence:
1. Tool list check (is the tool available?) 2. Disallowed tools check 3. Hooks — your custom logic runs here 4. Permission rules from settings.json 5. Permission mode 6. canUseTool callback
Hooks run before permission mode and before the canUseTool callback. This matters when you are debugging. If your canUseTool callback is not firing as expected, check whether a hook is intercepting the call first.
How to configure hooks
Hooks are passed as an option to the query function:
for await (const message of query(messages(), {
model: "claude-sonnet-4-5",
hooks: {
preToolUse: {
matcher: ".*", // matches all tools
handler: async ({ toolName, input }) => {
console.log(`Tool called: ${toolName}`);
return { behavior: "allow" };
},
},
},
permissionMode: "bypassPermissions",
dangerouslyAllowBypassPermissions: true,
})) {
// handle messages
}
The matcher is a regular expression that controls which tools the hook fires for. ".*" matches everything. "bash" matches only the bash tool. "write|edit" matches write and edit. If regex feels unfamiliar, describe what you want to match to a coding agent and it will write the pattern.
The handler receives the tool name and input, then returns a behavior decision: allow, deny, or an object with modified inputs.
Matchers in practice
A few patterns you will use repeatedly:
| Intent | Matcher | |
|---|---|---|
| All tools | ".*" | |
| Only bash | "bash" | |
| Write and edit | `"write\ | edit"` |
| Web tools | `"web_search\ | web_fetch"` |
| Everything except bash | Use disallowedTools instead |
File-based hooks for lifecycle events
Some lifecycle hooks, including session_start, require the file-based configuration approach rather than the options object. This means creating a .claude/settings.json file and pointing it at a script.
{
"hooks": {
"session_start": {
"type": "command",
"command": "bun .claude/load-memory.ts"
}
}
}
Then in your query options:
settingSources: ["project"]
Without settingSources: ["project"], the settings file is ignored and the lifecycle hook never fires. This is the most common mistake when setting up lifecycle hooks. The lifecycle hooks guide covers this pattern in full.
> For operators: Hooks are where agent workflows get real guardrails. Logging tool calls, blocking writes to sensitive directories, injecting client context before specific operations — all of that is hook territory. You do not need to use hooks from day one, but once you are deploying agents that run without supervision, hooks become essential.
---
Author: FractionalSkill