Requesty
Back|SEP '26AGENTS / INTEGRATIONS
22 MIN READ|

Claude Agent SDK Hooks: TypeScript and Python Examples

Last updated

The Claude Agent SDK gives your application the tools, agent loop, and context management behind Claude Code. Install @anthropic-ai/claude-agent-sdk for TypeScript or claude-agent-sdk for Python. Its hooks run your code at lifecycle events: check a tool before execution, rewrite its input, or record its outcome. Anthropic's SDK overview and hook guide describe these surfaces.

Below: one reusable runner, seven paired examples, the current event table, and the permission, timeout, and accounting details that determine how your agent behaves.

Install the Claude Agent SDK

The SDK runs the Claude Code binary. With the Anthropic Client SDK, you write the loop yourself or use its beta tool runner. For other frameworks, see our agent SDK comparison.

API baseline: TypeScript 0.3.285 and Python 0.2.162, checked September 27, 2026.

  1. 1
    Install one package

    Create and enter a new disposable lab directory. The quickstart requires Node.js 18 or later for TypeScript, or Python 3.10 or later for Python.

    Shell
    npm init -y
    npm pkg set type=module
    npm install @anthropic-ai/claude-agent-sdk
    npm install --save-dev tsx typescript @types/node

    Most installations bundle the CLI. Python source distributions and TypeScript installs omitting optional dependencies have binary-installation exceptions.

  2. 2
    Set authentication

    For direct Anthropic access:

    Shell
    export ANTHROPIC_API_KEY="your-anthropic-api-key"

    The SDK reads the process environment; it does not load .env automatically. Requesty users should use the gateway setup below instead.

  3. 3
    Create disposable file fixtures

    Run this macOS/Linux setup in the new lab directory:

    Shell
    mkdir workspace outside-demo
    printf 'Hook demonstration workspace.\n' > workspace/README.md
    printf 'placeholder\n' > workspace/edit-demo.txt

    Run every example from this directory. Reset fixtures and start a fresh session when switching recipes.

1. Block dangerous Bash patterns

A matcher selects Bash calls; the callback checks tool_input.command and returns a PreToolUse denial. The DELETE_DEMO marker lets you exercise this policy with harmless echo.

On Windows without Git Bash, change Bash to PowerShell in the runner, matcher, callback tool-name check, and prompt. The marker exercise works with either tool; the dangerous-command patterns below target Bash syntax.

Save the hook module for your language:

hooks.ts
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
const patterns = [
  /DELETE_DEMO/, /\brm\s+-rf\b/, /(?:^|\s)sudo(?:\s|$)/,
  /\bcurl\b[\s\S]*\|\s*(?:sh|bash)\b/,
];
export const blockCommand: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse" || input.tool_name !== "Bash") return {};
  const args = input.tool_input as Record<string, unknown>;
  const command = typeof args.command === "string" ? args.command : "";
  if (!patterns.some((pattern) => pattern.test(command))) return {};
  console.log({ tool_use_id: input.tool_use_id, decision: "deny" });
  return { hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "Command matched the application's block list.",
  } };
};
export const hooks: NonNullable<Options["hooks"]> = {
  PreToolUse: [{ matcher: "Bash", timeout: 5, hooks: [blockCommand] }],
};

Shared runner: use this for every recipe

Save this runner once. Later recipes replace the hook module and change only tools and prompt, except the usage recipe's result logger. The runners disable filesystem settings sources and ambient MCP configuration for these demonstrations.

agent.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { hooks } from "./hooks.js";
 
const tools: string[] = ["Bash"];
const prompt = "Use Bash to run exactly: echo DELETE_DEMO. If denied, explain without retrying.";
 
for await (const message of query({
  prompt,
  options: {
    cwd: process.cwd(),
    tools,
    allowedTools: tools,
    permissionMode: "dontAsk",
    settingSources: [],
    strictMcpConfig: true,
    maxTurns: 6,
    hooks,
  },
})) {
  if (message.type !== "result") continue;
  console.log({ subtype: message.subtype, is_error: message.is_error,
    terminal_reason: message.terminal_reason });
  if (message.subtype === "success") console.log(message.result);
}

Run npx tsx agent.ts.

Look for the callback's deny record and tool-use ID. No record means you should check whether a valid Bash call occurred.

tools sets availability; allowedTools / allowed_tools pre-approves. Pre-approval still passes through PreToolUse. dontAsk denies calls requiring a permission prompt. For human approval, use default, remove the reviewed tool from pre-approval, and provide canUseTool / can_use_tool; auto-approved calls bypass that callback.

Hook events and the agent lifecycle

The runtime invokes your callback; the model does not choose whether to follow it. PostToolUse records success; PostToolUseFailure records executed failure. Permission denials are separate, and invalid arguments or unknown tools can fail before hooks. EndConversation skips PreToolUse and PostToolUse.

Agent SDK hook timeline: UserPromptSubmit, then PreToolUse (allow, deny, ask or rewrite input), the tool runs, PostToolUse or PostToolUseFailure, repeating for each tool call, then Stop (block to keep working) and the result message with usage and cost.
Check tools before execution; read cost estimates and usage from the result message.

The SDK event list, TypeScript union, and Python union establish this September 27, 2026 matrix. Can block? identifies event-specific callback decisions, excluding universal termination requests and shell exit codes.

EventTSPythonCan block?
PreToolUseYesYesTool execution
PostToolUseYesYesNo, tool already ran
PostToolUseFailureYesYesNo, failure already occurred
PostToolBatchYesNoNext model call
NotificationYesYesNo
UserPromptSubmitYesYesPrompt
UserPromptExpansionYesNoCommand/MCP expansion
SessionStartYesNoNo
SessionEndYesNoNo
StopYesYesCompletion, resumes work
StopFailureYesNoNo
SubagentStartYesYesNo
SubagentStopYesYesCompletion, resumes work
PreCompactYesYesCompaction
PostCompactYesNoNo
PreModelSwitchYesNoRequested switch
PostModelSwitchYesNoNo
PermissionRequestYesYesTool permission
PermissionDeniedYesNoNo, auto denial occurred
SetupYesNoNo
TeammateIdleYesNoNo callback veto
TaskCreatedYesNoCreation
TaskCompletedYesNoNo callback veto
ElicitationYesNoDecline/cancel MCP input
ElicitationResultYesNoDecline/cancel MCP response
ConfigChangeYesNoApply change, except policy
WorktreeCreateYesNoPath-return contract
WorktreeRemoveYesNoNo callback veto
InstructionsLoadedYesNoNo
CwdChangedYesNoNo
FileChangedYesNoNo
DirectoryAddedYesNoNo
MessageDisplayYesNoNo, display only

Python cannot register session callbacks. TypeScript supports them, but a Windows streaming-input report on 0.3.273 describes missing invocations. Put setup before the query and cleanup in finally or a context manager. Stop excludes interrupts and API errors.

Notification in SDK sessions covers pending approval through canUseTool and elicitation notifications. MessageDisplay receives completed assistant text in delta in SDK queries; display replacement leaves transcripts and model context unchanged.

Matchers, inputs, and return values

Match the event target, then inspect its input

Matcher grammar is shared with the CLI, including for Python:

MatcherMeaning for tool events
BashExact name
Write|Edit or Write,EditExact-name list
^Edit$Whole-name JavaScript regex
^mcp__ or mcp__memory__.*MCP-name regex
Omitted, empty, or *All targets

Strings containing only letters, digits, underscores, hyphens, spaces, commas, and pipes use exact matching/list syntax. Other characters select unanchored JavaScript regex; FileChanged and StopFailure have narrower rules. Bare mcp__memory is an exact name, not a prefix.

Tool matchers inspect names, not paths or commands. Stop and UserPromptSubmit ignore matchers; subagent events match agent type, Notification matches notification type, and PreCompact matches manual or auto.

Callback signatures

See the TypeScript hook types and Python contract for all fields.

TypeScript
// HookCallback signature:
// (input: HookInput, toolUseID: string | undefined,
//  options: { signal: AbortSignal }) => Promise<HookJSONOutput>

Shared inputs include hook_event_name, session_id, transcript_path, and cwd. PreToolUse, PostToolUse, and PostToolUseFailure add tool_name, tool_input, and tool_use_id. Success adds tool_response; failure adds error. PermissionRequest provides the tool name and input but no tool_use_id. Prompt submission provides prompt; Stop provides stop_hook_active.

Narrow the event before accessing fields. TypeScript tool input is unknown. Tool-use IDs correlate records; optional agent_id identifies subagent tool activity. Python exposes fewer optional fields, and its context signal is currently None.

Permission decisions and context

PreToolUse decisions belong under hookSpecificOutput, with hookEventName: "PreToolUse":

ReturnEffect
Empty objectNo decision/change; normal permissions apply
permissionDecision: "deny"Prevent this tool call
permissionDecision: "allow"Approve subject to deny/ask rules and special restrictions
permissionDecision: "ask"Send to permission handling; supply approval UX
permissionDecision: "defer"Persisted single-call handoff
updatedInputReplace the entire input; preserve unchanged fields
additionalContextAdd model context

Decision priority is deny, defer, ask, allow. defer ignores updatedInput; multi-call batches ignore defer and follow normal permissions. A rewrite without a permission decision preserves permission evaluation.

systemMessage is user-facing. Use additionalContext for the model. Prompt hooks cannot replace the prompt. PostToolUse.updatedToolOutput replaces output before the model sees it; built-in output must match the tool schema. Context/message strings have a 10,000-character cap, after which the runtime supplies a file path and preview.

Stop with decision: "block" requests continued work. continue: false, or Python continue_: False, requests termination on events honoring it; neither undoes side effects.

PreToolUse deny prevents execution; PostToolUse context follows execution; Stop block resumes work; Stop continue false requests termination.
The same word, block, means different things at different events.

Matching callbacks run in parallel. Combine ordered validation and transformation in one callback.

2. Audit tool calls and outcomes

Runner: tools ['Read']; prompt Use Read to read workspace/README.md, then summarize it. For an executed-failure exercise, use ['Bash'] and Use Bash to run exactly: exit 1. Report the failure without retrying.

Log metadata on all three tool events. Serialize appends and keep secrets out of the records.

hooks.ts
import { appendFile } from "node:fs/promises";
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
let queue: Promise<void> = Promise.resolve();
const audit: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse" &&
      input.hook_event_name !== "PostToolUse" &&
      input.hook_event_name !== "PostToolUseFailure") return {};
  const line = JSON.stringify({
    event: input.hook_event_name, session_id: input.session_id,
    agent_id: input.agent_id ?? null,
    tool_name: input.tool_name, tool_use_id: input.tool_use_id,
  }) + "\n";
  const write = queue.then(() => appendFile("./audit.jsonl", line));
  queue = write.catch(() => {});
  try { await write; } catch (error) { console.error(error); }
  return {};
};
export const hooks: NonNullable<Options["hooks"]> = {
  PreToolUse: [{ timeout: 5, hooks: [audit] }],
  PostToolUse: [{ timeout: 5, hooks: [audit] }],
  PostToolUseFailure: [{ timeout: 5, hooks: [audit] }],
};

Correlate by tool-use ID, not file order. Log guard denials separately and inspect result permission-denial data. This logger continues on storage failures; production audits need a durable sink outside agent-writable files. Python cancellation does not stop the worker-thread append; use a dedicated writer and drain it on shutdown.

3. Redact a known secret from tool input

Runner: tools ['Read', 'Write', 'Edit']; prompt Write workspace/redact-demo.txt containing DEMO_SECRET_123. Then Read workspace/edit-demo.txt and Edit placeholder to DEMO_SECRET_123.

Replace Write.content and Edit.new_string, preserving Edit.old_string as the exact-match target.

hooks.ts
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
const redact: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse") return {};
  const args = input.tool_input as Record<string, unknown>;
  const field = input.tool_name === "Write" ? "content" :
    input.tool_name === "Edit" ? "new_string" : null;
  if (!field || typeof args[field] !== "string") return {};
  const after = (args[field] as string).split("DEMO_SECRET_123").join("[REDACTED]");
  if (after === args[field]) return {};
  return { hookSpecificOutput: {
    hookEventName: "PreToolUse", updatedInput: { ...args, [field]: after },
    additionalContext: "A known secret was removed from this write.",
  } };
};
export const hooks: NonNullable<Options["hooks"]> = {
  PreToolUse: [{ matcher: "Write|Edit", timeout: 5, hooks: [redact] }],
};

Inspect both files. Read before Edit in the same conversation, keeping the replacement target unambiguous, for portable behavior across models. The fake token demonstrates one rewrite: it does not remove prior model exposure, transcripts, Bash writes, or network exfiltration. No permission decision is returned.

4. Restrict writes to an allowed directory

Runner: tools ['Write']; prompt Write workspace/allowed-demo.txt containing demo. In a fresh run, use Write outside-demo/blocked-demo.txt containing demo. If denied, do not retry. Both targets remain in the disposable lab.

File tools supply absolute native paths. This compact policy resolves the existing parent, uses component containment, and rejects final symlinks, including dangling links. Create workspace before importing the module.

hooks.ts
import path from "node:path";
import { lstat, realpath } from "node:fs/promises";
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
const root = await realpath("./workspace");
function inside(target: string) {
  const rel = path.relative(root, target);
  return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
}
async function permitted(file: string) {
  try {
    if (!path.isAbsolute(file) || file.split(/[\\/]/).includes("..") || /[\\/]$/.test(file)) return false;
    if (!inside(await realpath(path.dirname(file)))) return false;
    let entry;
    try { entry = await lstat(file); }
    catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT"; }
    return entry.isFile() && !entry.isSymbolicLink() && inside(await realpath(file));
  } catch { return false; }
}
const guard: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse") return {};
  const args = input.tool_input as Record<string, unknown>;
  if (typeof args.file_path === "string" && await permitted(args.file_path)) return {};
  return { hookSpecificOutput: {
    hookEventName: "PreToolUse", permissionDecision: "deny",
    permissionDecisionReason: "Write must stay inside the allowed workspace.",
  } };
};
export const hooks: NonNullable<Options["hooks"]> = {
  PreToolUse: [{ matcher: "Write|Edit", timeout: 5, hooks: [guard] }],
};

The containment logic uses Node path operations, lstat, and Python strict resolution. A sibling such as workspace-other is outside the root. Parents must exist; inspection errors deny the call.

These checks are not atomic authorization: paths can change afterward, hard links and mounts need separate controls, and Bash/NotebookEdit/MCP writes require their own policies. Combine the check with OS isolation.

5. Add context when a prompt is submitted

Runner: tools []; prompt What environment and test command apply to this workspace? Inspect the printed answer for staging and the configured command.

additionalContext attaches trusted application facts alongside the prompt.

hooks.ts
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
const context: HookCallback = async (input) => {
  if (input.hook_event_name !== "UserPromptSubmit") return {};
  return { hookSpecificOutput: {
    hookEventName: "UserPromptSubmit",
    additionalContext: "Environment: staging. Test command: python3 -m unittest discover -s tests.",
  } };
};
export const hooks: NonNullable<Options["hooks"]> = {
  UserPromptSubmit: [{ timeout: 5, hooks: [context] }],
};

Enforce staging through credentials and permissions. Static conventions fit CLAUDE.md; hooks suit changing application state. Keep untrusted external content separate from trusted instructions.

6. Check completion without a Stop loop

Runner: tools ['Read', 'Write']; prompt Read workspace/README.md and summarize it in your answer. Start without workspace/SUMMARY.md; the hook requests that missing artifact.

Stop blocking continues the agent. Check stop_hook_active to avoid repeating the same request indefinitely.

hooks.ts
import { readFile } from "node:fs/promises";
import path from "node:path";
import type { HookCallback, Options } from "@anthropic-ai/claude-agent-sdk";
 
const summary = path.resolve("workspace/SUMMARY.md");
export async function summaryReady() {
  try { return (await readFile(summary, "utf8")).trim().length > 0; }
  catch { return false; }
}
const requireSummary: HookCallback = async (input) => {
  if (input.hook_event_name !== "Stop" || input.stop_hook_active) return {};
  if (await summaryReady()) return {};
  return { decision: "block", reason: `Write a nonempty summary to ${summary} before finishing.` };
};
export const hooks: NonNullable<Options["hooks"]> = {
  Stop: [{ timeout: 5, hooks: [requireSummary] }],
};

Import summaryReady alongside hooks. After the runner's loop, add:

TypeScript
if (!(await summaryReady())) throw new Error("Summary missing or empty");

The follow-up path permits stopping, so the host checks again before accepting the artifact. Also check result status: interrupts, API errors, and turn limits bypass normal completion. Nonempty is one requirement; validate content quality separately. Hook code runs in your host process, outside the Bash tool's sandbox.

7. Track usage from result messages

Runner: tools ['Read', 'Glob', 'Grep']; prompt Read workspace/README.md and summarize it without changing files. Replace the hook module below, import its logger alongside hooks, and call the logger inside the runner's result branch.

The cost-tracking API exposes these fields on results, not Stop inputs:

hooks.ts
import type { SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
 
export const hooks = {};
export function logUsage(message: SDKResultMessage) {
  console.log({
    session_id: message.session_id,
    estimated_cost_usd: message.total_cost_usd,
    model_usage: message.modelUsage,
  });
}

Use logUsage(message) in the result branch. Optional estimated budget: add maxBudgetUsd: 1 to the runner's options.

FieldScope
usageMain loop; per turn in streaming input
modelUsage / model_usageMain loop, subagents, compaction, other query-pipeline calls; some helpers excluded
total_cost_usdCumulative client estimate, not billed charges

Read the latest cumulative snapshot, not the sum of snapshots. Resumed/forked sessions restore earlier totals; the budget still measures the current call's spend. Deduplicate API message IDs for per-step input/cache tokens, and use final result counts for output: assistant-message output tokens are placeholders. Python accounting fields can be None; errors still carry costs, while crashes can leave incomplete totals.

The $1 option uses client pricing estimates; the crossing response has already consumed tokens. Gateway model IDs and changed prices can cause estimate drift. Use Requesty accounting for routed charges.

Async hooks and timeouts

An async callback is awaited by default. Background work requires starting a task and returning async: true, or Python async_: True; it cannot subsequently enforce a decision. Retain tasks and flush them during shutdown.

Timeout settingUnit
Matcher timeoutSeconds
Async output asyncTimeoutMilliseconds

Current defaults: 600 seconds for most callbacks, 30 for prompt/model-switch events, 10 for MessageDisplay, and a 1.5-second shutdown budget for SessionEnd. Set application timeouts explicitly.

SDK callback timeouts block PreToolUse tools and UserPromptSubmit prompts. PostToolUse keeps the result; Stop supplies no decision. Settings command/HTTP/MCP PreToolUse timeouts instead proceed through normal permissions.

Catch dependency failures and return an explicit policy outcome. Deny when a required security check fails; log and continue for optional telemetry. Pass TypeScript's signal to cancellable work; implement Python cancellation separately.

Test callbacks before live sessions

Keep recipe 1's hook module. These fixtures call the policy without executing a command:

hooks.test.ts
import assert from "node:assert/strict";
import { blockCommand } from "./hooks.js";
 
const output = await blockCommand({
  hook_event_name: "PreToolUse", session_id: "test", cwd: process.cwd(),
  transcript_path: "/tmp/fixture.jsonl", tool_name: "Bash",
  tool_input: { command: "echo DELETE_DEMO" }, tool_use_id: "test-tool",
}, "test-tool", { signal: new AbortController().signal });
assert("hookSpecificOutput" in output &&
  output.hookSpecificOutput?.hookEventName === "PreToolUse");
assert.equal(output.hookSpecificOutput.permissionDecision, "deny");

Run the fixture, then type-check the three files with explicit NodeNext settings:

Shell
npx tsx hooks.test.ts
npx tsc --noEmit --strict --target ES2022 --module NodeNext \
  --moduleResolution NodeNext --types node hooks.ts agent.ts hooks.test.ts

Use new Write filenames or reset files before another session. Assert actual files and callback decisions, not the agent's prose. Record runtime and SDK versions with your results.

Agent SDK hook test checklist

0 of 7 done

Why your Claude Agent SDK hook is not firing

SymptomCauseFix
No invocationWrong event spellingUse PreToolUse, not pre-tool-use
Some tools missingMatcher filters themRemove matcher; inspect metadata
MCP tools missingIncomplete nameMatch fully qualified mcp__ name
No tool eventNo valid tool callCheck tool availability and validation
Python session hook absentUnsupported callback eventUse host lifecycle/settings hook
Rewrite ignoredWrong nestingInclude hookSpecificOutput.hookEventName
Input fields disappearRewrite replaces whole objectCopy unchanged fields
Approval callback skippedTool pre-approvedPut invariants in PreToolUse
Conflicting decisionsParallel or settings hooksInspect every active handler
Follow-up callback failsOlder callback plumbingUpdate SDK and bundled/custom CLI

See official troubleshooting. September fixes in Python 0.2.160 and TypeScript 0.3.284 address follow-up callbacks after background subagents. Check the SDK's CLI, not just the shell executable.

includeHookEvents: true / include_hook_events=True adds lifecycle messages. Some event families omit parts of that stream; log callback entry and decisions independently.

SDK callbacks versus Claude Code settings hooks

If you use the Claude Code CLI rather than building your own agent, settings hooks are the right tool. Our guide to 10 practical Claude Code hooks has copy-paste recipes.

QuestionSDK callbacksSettings shell hooks
Defined inAgent optionsSettings JSON
HandlerApplication functionExecutable/command
InputCallback argumentJSON on stdin
OutputReturned objectExit status/stdout JSON
Matcher fieldsmatcher, hooks, timeoutHandler type, command

Keep shell-handler fields out of SDK matcher objects. For scripts and exit-code examples, see the official Claude Code hook guide.

Current query() loads user, project, and local settings by default. Disable them with settingSources: [] or setting_sources=[]; use ["project"] to load project shell hooks. Empty sources do not erase environment variables or managed policies. Both Python query() and ClaudeSDKClient support callbacks.

Route Agent SDK model requests through Requesty

Point the SDK at Requesty with ANTHROPIC_BASE_URL=https://router.requesty.ai and your key in ANTHROPIC_AUTH_TOKEN. You get model choice across 600+ models, configured fallbacks, a monthly spend cap per agent key, and per-agent cost analytics through our Agent SDK integration.

Assume REQUESTY_API_KEY holds your Requesty key:

Shell
export ANTHROPIC_BASE_URL="https://router.requesty.ai"
export ANTHROPIC_AUTH_TOKEN="$REQUESTY_API_KEY"
export ANTHROPIC_MODEL="anthropic/claude-opus-5-5"
export ANTHROPIC_CUSTOM_HEADERS="X-Requesty-Agent: repo-reviewer
X-Requesty-Environment: staging"

Use the root URL and the key without Bearer. The model ID appears in our model catalog. Anthropic's ANTHROPIC_CUSTOM_HEADERS uses newline-separated Name: Value entries; Requesty captures those analytics tags and strips them before provider forwarding. Keep secrets and personal data out of tags.

When switching from a cloud route, clear its provider-selection variables. ANTHROPIC_AUTH_TOKEN has higher precedence than ANTHROPIC_API_KEY. For per-query env, TypeScript replaces inherited environment, so spread process.env; Python merges.

Application hooks control tools; Claude Code sends model requests through Requesty to providers. Files, Bash, and MCP use a separate path. SDK estimates and gateway accounting are separate.
Requesty governs model requests. Local tool operations follow their own paths.

Use a key per top-level agent application for separate monthly caps. Caps block new requests; they do not cancel in-flight work. Launch headers tag the enclosing application; hook agent_id provides individual subagent tool attribution.

Create a fallback policy with your chosen models/providers and use its policy/name as the model. Test the chain's tool calling, streaming, context, and required features. Gateway fallback does not imply an SDK PostModelSwitch event.

Requesty
Control tools and model spend

Keep hook policies and final validation in your application. Add routed model accounting, agent tags, and per-key caps with the Agent SDK integration docs.

Start with one callback policy, test its returned decision, then verify the disposable-workspace outcome. Add accounting and shared spend controls once the tool behavior is clear.

Sources

References checked September 27, 2026.

Frequently asked questions
What is the Claude Agent SDK?
The Claude Agent SDK is a TypeScript and Python library that runs the Claude Code binary, giving your application its agent loop, tools, and context management. The Anthropic Client SDK instead exposes model API calls and a beta tool runner.
How do Claude Agent SDK hooks work?
Register callbacks under lifecycle events such as PreToolUse. A matcher selects targets, the host invokes your callback, and its returned object controls the event. Callbacks are awaited by default.
Does Python query() support Claude Agent SDK hooks?
Yes. Both query() and ClaudeSDKClient support hooks in the current Python API. Older README text saying hooks require ClaudeSDKClient does not describe the current API.
Are Agent SDK hooks the same as Claude Code hooks?
They share lifecycle events and much of the output contract, but SDK hooks are application callbacks rather than settings.json shell handlers. Settings-based hooks also run in SDK sessions when their settings sources are enabled.
Why is my Claude Agent SDK hook not firing?
Check the case-sensitive event name, matcher target, language support, and installed SDK plus bundled CLI version. A prompt that produces no valid tool call will not trigger a tool hook, and validation failures can happen before hooks run.
Can hooks block commands or change tool inputs?
PreToolUse can deny a tool call or replace its arguments using updatedInput. PostToolUse runs after execution and cannot undo its side effects. Pattern checks do not replace sandboxing.
Can I track cost with a Stop hook?
Read total_cost_usd and per-model usage from SDK result messages. Stop inputs are not the cost accounting surface. SDK dollar totals are client-side estimates rather than authoritative billing data.
Related reading

Start building with Requesty

One line of code. 600+ models. Full control.

Speak to founders