Claude Agent SDK (TypeScript)
Add Checkrd enforcement to @anthropic-ai/claude-agent-sdk via PreToolUse / PostToolUse hooks.
Anthropic Claude Agent SDK (TypeScript)
The Claude Agent SDK exposes hooks on ClaudeAgentOptions: async functions invoked at well-defined points in the agent's run loop. Checkrd ships factory functions for the four most common events; user-supplied hooks coexist on the same matchers.
Install
npm install checkrd @anthropic-ai/claude-agent-sdkQuickstart
import { query, type ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import { initAsync, getEngine, getSink } from "checkrd";
import { attachToOptions } from "checkrd/claude-agent-sdk";
await initAsync({
agentId: "claude-agent",
apiKey: process.env.CHECKRD_API_KEY!,
});
// initAsync fetches the agent's published policy from the dashboard
// and installs it before resolving — no `policy:` argument in app code.
const options: ClaudeAgentOptions = {};
attachToOptions(options, {
engine: getEngine(),
agentId: "claude-agent",
sink: getSink(),
enforce: true,
});
for await (const msg of query({ prompt: "summarize this", options })) {
console.log(msg);
}attachToOptions adds Checkrd hooks for PreToolUse, PostToolUse, UserPromptSubmit, and Stop. Idempotent: calling it twice does not register duplicates. User-supplied hooks remain in place.
Manual wiring
If you only want one of the events, use the factory functions directly:
import { initAsync, getEngine, getSink } from "checkrd";
import type { ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import {
makePreToolUseHook,
makePostToolUseHook,
} from "checkrd/claude-agent-sdk";
// Initialize first — getEngine() / getSink() require it.
await initAsync({
agentId: "claude-agent",
apiKey: process.env.CHECKRD_API_KEY!,
});
const engine = getEngine();
const sink = getSink();
const pre = makePreToolUseHook({
engine,
agentId: "claude-agent",
sink,
enforce: true,
});
const post = makePostToolUseHook({
engine,
agentId: "claude-agent",
sink,
});
const options: ClaudeAgentOptions = {
hooks: {
PreToolUse: [{ matcher: "Bash|Write|Edit", hooks: [pre], timeout: 30 }],
PostToolUse: [{ hooks: [post] }],
},
};The matcher is a regex over tool names; leave it undefined to match every tool.
What gets enforced
| Hook event | Synthetic URL |
|---|---|
PreToolUse | https://claude-agent.local/tools/{tool_name} |
UserPromptSubmit | https://claude-agent.local/prompts/user-prompt |
default: allow
rules:
- name: deny-destructive-bash
deny:
url: "claude-agent.local/tools/Bash"
body:
- jsonpath: "$.command"
regex: "rm -rf|dd if="
- name: deny-secret-write
deny:
url: "claude-agent.local/tools/Write"
body:
- jsonpath: "$.file_path"
regex: "\\.(env|pem)$|secrets"Deny semantics
The hook returns { decision: "block", systemMessage: "<reason>" } per the SDK's documented protocol. The claude-code subprocess interprets this as "do not run the tool" and reports the message back to the agent. The model sees the rejection in its conversation history and can adapt.
Observation mode
attachToOptions(options, { engine, agentId, sink, enforce: false });The hook returns {} (no block) on deny but emits the deny telemetry event so dashboards show what would have been blocked.
Caveats
- All hooks are
async. The SDK rejects sync hooks at registration. - Hook latency adds to every tool call. The WASM engine's
evaluate()is sub-ms, but the hook IPC to theclaude-codesubprocess adds a few ms. Keep hooks fast. - Duck-typed against the SDK shape: the
HookMatcherandClaudeAgentOptionstypes in the adapter are structural so a minor SDK bump doesn't force a Checkrd release.