Skip to main content
Version: 0.25.0

Your own hooks

A hook is a function the CLI calls around every tool call. The kit's guardrails are hooks; yours can be too. Use one when a rule must hold whatever the model decides.

Adding hooks next to the kit's​

buildSessionOptions() returns plain options: add your hooks after the kit's, never instead of them.

import type { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

// Deny any WebFetch outside an allow list of domains.
const onlyTrustedSites: HookCallback = async (input) => {
const pre = input as PreToolUseHookInput;
if (pre.tool_name !== "WebFetch") return {};
const url = new URL(String((pre.tool_input as { url?: string }).url));
if (["docs.example.com", "wiki.example.com"].includes(url.hostname)) return {};
return {
hookSpecificOutput: {
hookEventName: pre.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: `Only docs.example.com and wiki.example.com can be fetched, not ${url.hostname}.`,
},
};
};

const { options } = await buildSessionOptions(config, runDir, spec);
const session: Options = {
...options,
hooks: {
...options.hooks,
PreToolUse: [...(options.hooks?.PreToolUse ?? []), { hooks: [onlyTrustedSites] }],
},
};

The hook types come from the SDK (@anthropic-ai/claude-agent-sdk); install it as a dev dependency for the types, or type the input yourself.

With a session opener, do it inside the opener:

await runChatInk(async (run) => {
const built = await buildSessionOptions(config, run.dir, spec, { run });
return { ...built, options: withMyHooks(built.options) };
}, { runsDir });

What a PreToolUse hook can return​

ReturnEffect
{}No decision: the call goes on (to the next hook).
permissionDecision: "deny" with a reasonThe call is denied; the model gets the reason and can try something else. Write the reason for the model: say what's allowed instead.
permissionDecision: "allow"The call is approved.
updatedInput: { … }The call runs with this input instead (the kit's foreground gate does this).
continue: falseThe turn stops after this call (the step gate's Stop).

Hooks run for subagents' calls too. agent_id in the input is set only for calls made inside a subagent.

Auditing with PostToolUse​

const audit: HookCallback = async (input) => {
const post = input as PostToolUseHookInput;
await appendFile(auditLog, `${new Date().toISOString()} ${post.tool_name}\n`);
return {};
};

hooks: { ...options.hooks, PostToolUse: [...(options.hooks?.PostToolUse ?? []), { hooks: [audit] }] },

The kit already logs every call and result to transcript.jsonl, redacted; see Transcript.

Reusing the kit's hooks​

Every guardrail is exported as a hook factory: createFileScopeGate(), createStepGate(), createSubagentTypeGate(), createSubagentBashGate(), createSubagentForegroundGate(), createPlanGate(), createTranscriptLogger(). Use them to assemble a session without buildSessionOptions(), or in tests.