Developer guide
Add a pre-action check to a Claude Agent SDK agent
A Claude Agent SDK tool call needs a PreToolUse hook if it must be checked before the tool runs.
What the check does
The check returns permit, deny, or escalate with the governing clause and a signed receipt. It is advisory, and your PreToolUse hook is what refuses execution. The hosted check evaluates the request in memory and keeps no request content.
Code
This is the audited example exactly as published under public/examples/frameworks/.
/** Claude Agent SDK PreToolUse hook that checks HANRIA before execution. */
import { query, type HookCallback, type HookCallbackMatcher, type Options } from "@anthropic-ai/claude-agent-sdk";
import { pathToFileURL } from "node:url";
const CHECK_URL = "https://check.hanria.ai/v1/check";
const MANDATE = {
schema_version: "0.2-draft",
mandate_id: "review-tool-calls",
purpose: "Permit file reads under the example directory.",
default: "deny",
clauses: [{
id: "permit-example-read",
effect: "permit",
match: { kind: ["file"], verb: ["read"], target_prefix: ["/tmp/example/"] },
}],
};
type FetchLike = typeof globalThis.fetch;
export async function checkAction(
action: Record<string, unknown>,
checkUrl = CHECK_URL,
fetchImpl: FetchLike = globalThis.fetch,
): Promise<Record<string, unknown>> {
try {
const response = await fetchImpl(checkUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ mandate: MANDATE, action }),
signal: AbortSignal.timeout(10000),
});
if (!response.ok) return { outcome: "error", reason: `HANRIA returned HTTP ${response.status}` };
const decision = await response.json() as Record<string, unknown>;
if (!["permit", "deny", "escalate", "error"].includes(String(decision.outcome))) {
return { outcome: "error", reason: "HANRIA returned an invalid decision" };
}
return decision;
} catch (error) {
return { outcome: "error", reason: `HANRIA check failed: ${String(error)}` };
}
}
export function createHanriaHook({
checkUrl = CHECK_URL,
fetchImpl = globalThis.fetch,
}: { checkUrl?: string; fetchImpl?: FetchLike } = {}): HookCallback {
return async (input) => {
if (input.hook_event_name !== "PreToolUse") {
return { continue: false, stopReason: "Expected a PreToolUse event" };
}
const toolInput = input.tool_input as Record<string, unknown>;
const action = {
schema_version: "0.1-draft",
requested_by: { agent: "claude-agent-sdk-example" },
operation: {
kind: input.tool_name === "Read" ? "file" : "tool",
verb: input.tool_name === "Read" ? "read" : input.tool_name,
target: String(toolInput.file_path ?? toolInput.path ?? input.tool_name),
},
justification: `Run ${input.tool_name} through the Claude Agent SDK.`,
};
const decision = await checkAction(action, checkUrl, fetchImpl);
if (decision.outcome === "permit") return {};
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: `HANRIA returned ${String(decision.outcome ?? "error")}: ${String(decision.reason ?? "no reason returned")}`,
},
};
};
}
export function createHookOptions(settings: { checkUrl?: string; fetchImpl?: FetchLike } = {}): Options {
const matcher: HookCallbackMatcher = { hooks: [createHanriaHook(settings)] };
return { hooks: { PreToolUse: [matcher] } };
}
export function constructQuery(prompt: string, options = createHookOptions()) {
return query({ prompt, options });
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const options = createHookOptions();
console.log(`Constructed ${options.hooks?.PreToolUse?.length ?? 0} HANRIA PreToolUse hook matcher.`);
}
Run it
Save claude_agent_sdk.ts, then run these commands from the folder where you saved the file.
npm install --no-save --save-exact @anthropic-ai/[email protected] [email protected]
npx tsx claude_agent_sdk.ts
The command constructs one PreToolUse hook matcher and does not start a query. Pass createHookOptions() to the SDK query in your application, and map each tool input to the action fields your mandate checks.
Handle every outcome
- permit: the hook returns no permission decision, so the SDK continues its normal permission flow.
- deny: the hook returns a deny permission decision before the tool runs.
- escalate: the hook returns a deny permission decision. Ask the operator for a decision outside this hook.
- error: the hook returns a deny permission decision. Treat it as deny, report the reason, and do not retry blindly.
A timeout, transport failure, non-success HTTP response, malformed response, or unknown outcome becomes error. Every result other than exact permit is refused by the hook.
References
Try the check · Verify signed receipts · OpenAPI document · Agent-facing summary