# Anthropic Agent SDK TypeScript hooks: interfaces reference

Every hook event in the Claude Agent SDK for TypeScript, with the exact HookCallback, input and output types, matchers, timeouts and working examples.

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](https://code.claude.com/docs/en/agent-sdk/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](https://code.claude.com/docs/en/agent-sdk/typescript) and the [hooks guide](https://code.claude.com/docs/en/agent-sdk/hooks), 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:

```typescript
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](https://code.claude.com/docs/en/agent-sdk/hooks#available-hooks):

| 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

```typescript
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`:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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:

```typescript
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:** `PreToolUse` with `permissionDecision: "deny"`. `permissionDecisionReason` tells the model why; `systemMessage` tells the user.
- **Rewrite input:** return `updatedInput` inside `hookSpecificOutput`, never at the top level. Pair it with `"allow"` to auto-approve the rewritten call or `"ask"` to show it to the user. Omit `permissionDecision` and the rewritten input still goes through normal permission evaluation. With `"defer"`, `updatedInput` is ignored.
- **Add context:** `additionalContext` reaches the model. `systemMessage` reaches the user, not the model.
- **Several hooks disagree:** matching hooks run in parallel, and `deny` beats `defer`, which beats `ask`, which beats `allow`.
- **Async output** (`{ async: true }`) lets the agent move on without waiting. It cannot block, modify or inject context. `asyncTimeout` is in milliseconds.

## Registering hooks in `query()` and matchers

```typescript
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](https://code.claude.com/docs/en/hooks#matcher-patterns): 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](https://code.claude.com/docs/en/agent-sdk/hooks#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:

```typescript
type CanUseTool = (
  toolName: string,
  input: Record<string, unknown>,
  options: { signal: AbortSignal; toolUseID: string; requestId: string; decisionReason?: string; /* ... */ }
) => Promise<PermissionResult | null>;
```

The [permissions page](https://code.claude.com/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) 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](/blog/anthropic-agent-sdk-governance-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.

```typescript
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.

```typescript
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](https://code.claude.com/docs/en/agent-sdk/hooks#modify-tool-input).

```typescript
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|Edit` guard does not run when Claude writes the same file with `Bash`. 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 `allow` is not absolute.** Deny rules still apply, and a hook cannot approve recognized critical-path removals such as `rm -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 `UserPromptSubmit` hook 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](/integrations/claude-code)). 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](/integrations/anthropic-agent-sdk) 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](/blog/claude-agent-sdk-security-architecture-tool-integration).

## Frequently asked questions



## Where to read more

- [Claude Agent SDK TypeScript reference](https://code.claude.com/docs/en/agent-sdk/typescript)
- [Intercept and control agent behavior with hooks](https://code.claude.com/docs/en/agent-sdk/hooks)
- [Configure permissions](https://code.claude.com/docs/en/agent-sdk/permissions)
- [Claude Code hooks reference (matcher patterns, JSON output)](https://code.claude.com/docs/en/hooks)
- [Agent SDK TypeScript changelog](https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md)
