Anthropic Agent SDK TypeScript hooks: interfaces reference
You add hooks in the Claude Agent SDK for TypeScript through one option on query(): hooks, typed Partial<Record<HookEvent, HookCallbackMatcher[]>>. Each callback is a HookCallback. It gets a typed HookInput and returns a HookJSONOutput that can allow, deny, rewrite or annotate the operation. The package is @anthropic-ai/claude-agent-sdk. The SDK used to be called the Claude Code SDK (Anthropic has a migration guide), and people still search for it as the “Anthropic Agent SDK”. All three names mean the same library.
The types below are copied from the official TypeScript SDK reference and the hooks guide, both read on 2026-11-07. The SDK ships often. If a field is missing from your version, those pages’ version notes say when it arrived.
The hook event list
HookEvent in the TypeScript reference is this union:
type HookEvent =
| "PreToolUse"
| "PostToolUse"
| "PostToolUseFailure"
| "PostToolBatch"
| "Notification"
| "UserPromptSubmit"
| "UserPromptExpansion"
| "SessionStart"
| "SessionEnd"
| "Stop"
| "StopFailure"
| "SubagentStart"
| "SubagentStop"
| "PreCompact"
| "PostCompact"
| "PreModelSwitch"
| "PostModelSwitch"
| "PermissionRequest"
| "PermissionDenied"
| "Setup"
| "TeammateIdle"
| "TaskCreated"
| "TaskCompleted"
| "Elicitation"
| "ElicitationResult"
| "ConfigChange"
| "DirectoryAdded"
| "WorktreeCreate"
| "WorktreeRemove"
| "InstructionsLoaded"
| "CwdChanged"
| "FileChanged"
| "MessageDisplay";
The ones most people need, with what fires them according to the hooks guide table:
| Event | Fires when | Can it change the outcome? |
|---|---|---|
PreToolUse |
A tool call is requested | Yes: allow, deny, ask, defer, rewrite input |
PostToolUse |
A tool returned a result | Yes: add context, replace the output |
PostToolUseFailure |
A tool call failed | Adds context |
PostToolBatch |
A batch of tool calls resolves, before the next model call | Adds context |
UserPromptSubmit |
A prompt is submitted, including a turn Claude Code starts on its own | Yes: add context or block |
PermissionRequest |
A tool call needs a permission decision | Yes: allow or deny |
PermissionDenied |
Auto mode denies a tool call | Can ask for a retry |
Stop / SubagentStop |
The agent or a subagent stops | Adds context or keeps the agent going |
SubagentStart |
A subagent starts | Adds context |
SessionStart / SessionEnd |
Session begins / ends | SessionStart adds context |
PreCompact / PostCompact |
Conversation compaction starts / finishes | No |
Notification |
Agent status messages | No |
PostToolBatch, UserPromptExpansion, MessageDisplay, StopFailure, PostCompact, PermissionDenied, SessionStart, SessionEnd, Setup and the remaining events in the union are marked TypeScript-only in that table. The guide notes that Python’s HookEvent omits SessionStart and SessionEnd, so Python apps can only get them as shell command hooks from settings files.
The callback signature
type HookCallback = (
input: HookInput, // Union of all hook input types
toolUseID: string | undefined,
options: { signal: AbortSignal }
) => Promise<HookJSONOutput>;
interface HookCallbackMatcher {
matcher?: string;
hooks: HookCallback[];
timeout?: number; // Timeout in seconds for all hooks in this matcher
onFailure?: "continue" | "block"; // "block" blocks the action when a callback fails
}
toolUseID correlates the PreToolUse and PostToolUse events for the same call. signal fires when the hook times out, so pass it to any fetch you make.
Input shapes
Every input extends BaseHookInput:
type BaseHookInput = {
session_id: string;
transcript_path: string;
cwd: string;
prompt_id?: string;
permission_mode?: string;
effort?: { level: string };
agent_id?: string;
agent_type?: string;
};
agent_id and agent_type are populated when the hook fires inside a subagent. The tool and prompt events add these fields:
type PreToolUseHookInput = BaseHookInput & {
hook_event_name: "PreToolUse";
tool_name: string;
tool_input: unknown;
tool_use_id: string;
mcp_server?: McpServerProvenance;
};
type PostToolUseHookInput = BaseHookInput & {
hook_event_name: "PostToolUse";
tool_name: string;
tool_input: unknown;
tool_response: unknown;
tool_use_id: string;
duration_ms?: number;
mcp_server?: McpServerProvenance;
};
type PostToolUseFailureHookInput = BaseHookInput & {
hook_event_name: "PostToolUseFailure";
tool_name: string;
tool_input: unknown;
tool_use_id: string;
error: string;
is_interrupt?: boolean;
duration_ms?: number;
mcp_server?: McpServerProvenance;
};
type UserPromptSubmitHookInput = BaseHookInput & {
hook_event_name: "UserPromptSubmit";
prompt: string;
session_title?: string;
};
type PermissionRequestHookInput = BaseHookInput & {
hook_event_name: "PermissionRequest";
tool_name: string;
tool_input: unknown;
permission_suggestions?: PermissionUpdate[];
mcp_server?: McpServerProvenance;
};
And the lifecycle events:
type SessionStartHookInput = BaseHookInput & {
hook_event_name: "SessionStart";
source: "startup" | "resume" | "clear" | "compact" | "fork";
agent_type?: string;
model?: string;
session_title?: string;
};
type SessionEndHookInput = BaseHookInput & {
hook_event_name: "SessionEnd";
reason: ExitReason; // String from EXIT_REASONS array
};
type StopHookInput = BaseHookInput & {
hook_event_name: "Stop";
stop_hook_active: boolean;
last_assistant_message?: string;
background_tasks?: BackgroundTaskSummary[];
session_crons?: SessionCronSummary[];
};
type SubagentStopHookInput = BaseHookInput & {
hook_event_name: "SubagentStop";
stop_hook_active: boolean;
agent_id: string;
agent_transcript_path: string;
agent_type: string;
last_assistant_message?: string;
background_tasks?: BackgroundTaskSummary[];
session_crons?: SessionCronSummary[];
};
type PreCompactHookInput = BaseHookInput & {
hook_event_name: "PreCompact";
trigger: "manual" | "auto";
custom_instructions: string | null;
};
type NotificationHookInput = BaseHookInput & {
hook_event_name: "Notification";
message: string;
title?: string;
notification_type: string;
};
tool_input is unknown. Cast it to Record<string, unknown> (or to the per-tool input type, such as BashInput) before reading fields; every example in the official guide does the same.
Output shape: how a hook blocks, rewrites or adds context
type HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput;
type AsyncHookJSONOutput = {
async: true;
asyncTimeout?: number;
};
SyncHookJSONOutput has top-level fields (continue, suppressOutput, stopReason, decision, systemMessage, reason, and a CLI-only terminalSequence) plus a hookSpecificOutput union keyed by hookEventName. The variants for the common events:
hookSpecificOutput?:
| {
hookEventName: "PreToolUse";
permissionDecision?: "allow" | "deny" | "ask" | "defer";
permissionDecisionReason?: string;
updatedInput?: Record<string, unknown>;
additionalContext?: string;
}
| {
hookEventName: "UserPromptSubmit";
additionalContext?: string;
sessionTitle?: string;
suppressOriginalPrompt?: boolean;
}
| {
hookEventName: "PostToolUse";
additionalContext?: string;
classifierContext?: string;
updatedToolOutput?: unknown;
updatedMCPToolOutput?: unknown; // deprecated
}
| {
hookEventName: "PermissionRequest";
decision:
| { behavior: "allow"; updatedInput?: Record<string, unknown>; updatedPermissions?: PermissionUpdate[] }
| { behavior: "deny"; message?: string; interrupt?: boolean };
}
// ...one variant per event; see the TypeScript reference for the full list
The rules that matter, from the hooks guide:
- Allow with no change: return
{}. - Block a tool:
PreToolUsewithpermissionDecision: "deny".permissionDecisionReasontells the model why;systemMessagetells the user. - Rewrite input: return
updatedInputinsidehookSpecificOutput, never at the top level. Pair it with"allow"to auto-approve the rewritten call or"ask"to show it to the user. OmitpermissionDecisionand the rewritten input still goes through normal permission evaluation. With"defer",updatedInputis ignored. - Add context:
additionalContextreaches the model.systemMessagereaches the user, not the model. - Several hooks disagree: matching hooks run in parallel, and
denybeatsdefer, which beatsask, which beatsallow. - Async output (
{ async: true }) lets the agent move on without waiting. It cannot block, modify or inject context.asyncTimeoutis in milliseconds.
Registering hooks in query() and matchers
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Refactor the auth module",
options: {
hooks: {
PreToolUse: [
{ matcher: "Bash", hooks: [denyDangerousBash], onFailure: "block" },
{ matcher: "Write|Edit", hooks: [rewriteWrites] },
{ matcher: "^mcp__", hooks: [mcpAudit] },
],
PostToolUse: [{ hooks: [logEveryCall] }],
},
},
})) {
console.log(message);
}
Matchers follow the settings-file matcher rules: a pipe-separated list such as Write|Edit is an exact list, and a pattern such as ^mcp__ is a regex. For tool events the matcher tests the tool name only, never arguments, so filter on tool_input.file_path inside the callback. MCP tools are named mcp__<server>__<action>. No matcher means every occurrence of the event. Stop ignores matchers.
Timeouts and failure semantics
From the Hook timeout section: when timeout is unset, the default is 600 seconds for most events, 30 seconds for UserPromptSubmit, PreModelSwitch and PostModelSwitch, and 10 seconds for MessageDisplay. SessionEnd runs under a shutdown budget of 1.5 seconds by default.
When a callback times out, its output is discarded and the session continues. Then:
PreToolUse: the tool call is blocked, and the tool result tells Claude the hook did not respond.PostToolUse/PostToolUseFailure: the tool result is kept.UserPromptSubmit: the prompt is blocked with a message naming the hook.Stop/SubagentStop: counts as no decision.
A callback that throws is different. On most events the action goes ahead and your app receives an SDKInformationalMessage tagged hook_callback_failed. So a crashing PreToolUse guard fails open into the normal permission flow. A callback that returns nothing also counts as failing. Set onFailure: "block" on the matcher to fail closed instead (TypeScript SDK v0.3.317 or later, per the guide). Stop, SubagentStop, TaskCompleted and TeammateIdle ignore it.
Hooks vs settings.json hooks vs canUseTool
Settings-file hooks. The SDK runs your options.hooks callbacks and also the shell command hooks from settings files whose source is enabled in settingSources, which default query() options enable. Both use the same JSON output format. If you pass settingSources: [], the user, project and local settings files stop loading, hooks included; the TypeScript reference notes that endpoint-managed policy loads regardless.
canUseTool. Its type is:
type CanUseTool = (
toolName: string,
input: Record<string, unknown>,
options: { signal: AbortSignal; toolUseID: string; requestId: string; decisionReason?: string; /* ... */ }
) => Promise<PermissionResult | null>;
The permissions page puts hooks first and canUseTool last: hooks, deny rules, ask rules, permission mode, allow rules, then canUseTool. A call approved by allowedTools, acceptEdits or bypassPermissions never reaches canUseTool. A PreToolUse hook runs for those calls too, and a hook deny applies even in bypassPermissions. A hook allow does not skip deny or ask rules.
Our April Agent SDK reference once said there was no post-call hook. It now carries the correction. The current hooks guide lists PostToolUse, PostToolUseFailure and PostToolBatch, so use those for checks on output.
Three working examples
Deny a command. If the check throws, the call is blocked.
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const denyDangerousBash: HookCallback = async (input) => {
const pre = input as PreToolUseHookInput;
const command = String((pre.tool_input as Record<string, unknown>)?.command ?? "");
if (/\bgit\s+push\b.*--force/.test(command)) {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Force-push is blocked in this app",
},
};
}
return {};
};
const options = {
hooks: { PreToolUse: [{ matcher: "Bash", hooks: [denyDangerousBash], onFailure: "block" as const }] },
};
A regex on the command string is a convenience, not a boundary: git push -f or a script that pushes slips past it.
Log every call. Async, so it never slows the loop.
import { HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const logEveryCall: HookCallback = async (input, toolUseID) => {
const post = input as PostToolUseHookInput;
const line = JSON.stringify({
at: new Date().toISOString(),
session: post.session_id,
agent: post.agent_id ?? "main",
tool: post.tool_name,
toolUseID,
ms: post.duration_ms,
});
appendToLog(line).catch(console.error); // your sink
return { async: true, asyncTimeout: 30000 };
};
Rewrite input. Redirect writes into a scratch directory, mirroring the official example.
import { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const rewriteWrites: HookCallback = async (input) => {
if (input.hook_event_name !== "PreToolUse") return {};
const pre = input as PreToolUseHookInput;
const toolInput = pre.tool_input as Record<string, unknown>;
if (pre.tool_name !== "Write") return {};
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
updatedInput: { ...toolInput, file_path: `./scratch${toolInput.file_path as string}` },
},
};
};
Return a new object; do not mutate tool_input.
Limits to know
- Matchers see tool names only. A
Write|Editguard does not run when Claude writes the same file withBash. The guide says so and points to the sandbox for that case. - A throwing callback lets the call through unless you set
onFailure: "block". - Hooks may not fire when the agent hits
maxTurns, because the session ends first. - A hook
allowis not absolute. Deny rules still apply, and a hook cannot approve recognized critical-path removals such asrm -rf ~. - Callbacks live in your Node process. They know the session, not the person behind it. Your log is only as durable as the place you write it.
- Recursion. A
UserPromptSubmithook that spawns subagents can loop if those subagents trigger the same hook.
ACP’s Claude Code install adds PreToolUse and PostToolUse hooks through the ACP plugin. On an older Claude Code without plugin support, it writes them to ~/.claude/settings.json instead (details). An SDK app that loads the settings sources may run them like any other settings hook. We have not tested that under the SDK. If you set a custom env, pass {...process.env} so the hook keeps HOME and its credentials. For tool-use loops on the raw Anthropic SDK, the Anthropic Agent SDK integration wraps your handlers instead.
On the security side (tool boundaries, sandboxing, where a policy check sits), see Claude Agent SDK security architecture and tool integration.
Frequently asked questions
What are the Anthropic Agent SDK TypeScript hooks interfaces?
A hook is a HookCallback: (input: HookInput, toolUseID: string | undefined, options: { signal: AbortSignal }) => Promise<HookJSONOutput>. You register it inside a HookCallbackMatcher (matcher?, hooks, timeout?, onFailure?) under an event name in the hooks option of query(). The package is @anthropic-ai/claude-agent-sdk.
What are the Anthropic Agent SDK UserPromptSubmit hook details?
UserPromptSubmitHookInput adds hook_event_name: "UserPromptSubmit", prompt: string and an optional session_title to the base input. The callback can return hookSpecificOutput.additionalContext to inject context, or block the prompt. Its default timeout is 30 seconds, and a timeout blocks the prompt while the session continues.
How do Anthropic Agent SDK hooks PreToolUse and PostToolUse differ?
PreToolUse runs before the tool and can return permissionDecision (allow, deny, ask, defer) plus updatedInput. PostToolUse runs after, receives tool_response and duration_ms, and can append additionalContext or replace the result with updatedToolOutput. The toolUseID argument correlates the two.
What lifecycle hooks does the Anthropic Agent SDK have?
In TypeScript: SessionStart, SessionEnd, Setup, UserPromptSubmit, Stop, StopFailure, SubagentStart, SubagentStop, PreCompact, PostCompact, Notification, and more. Several are TypeScript-only; Python’s HookEvent omits SessionStart and SessionEnd.
Is a hook the same as canUseTool in the Agent SDK?
No. A PreToolUse hook runs first, for every matching call, including calls that allow rules or bypassPermissions approve. canUseTool runs last and only when nothing earlier resolved the call.
Where to read more
- Claude Agent SDK TypeScript reference
- Intercept and control agent behavior with hooks
- Configure permissions
- Claude Code hooks reference (matcher patterns, JSON output)
- Agent SDK TypeScript changelog
Running coding agents? ACP records each tool call its hook sees and makes the dangerous shell commands (a force-push, destructive SQL, curl | sh) wait for you. One command, free up to 5 agents:
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex