checkrd

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

bash
npm install checkrd @anthropic-ai/claude-agent-sdk

Quickstart

typescript
import { query, type ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import { initAsync } from "checkrd";
import { attachToOptions } from "checkrd/claude-agent-sdk";

const checkrd = await initAsync({
  policy: "policy.yaml",
  agentId: "claude-agent",
});

const options: ClaudeAgentOptions = {};
attachToOptions(options, {
  engine: checkrd.engine,
  agentId: "claude-agent",
  sink: checkrd.sink,
  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:

typescript
import {
  makePreToolUseHook,
  makePostToolUseHook,
} from "checkrd/claude-agent-sdk";

const pre = makePreToolUseHook({
  engine: checkrd.engine,
  agentId: "claude-agent",
  enforce: true,
});
const post = makePostToolUseHook({
  engine: checkrd.engine,
  agentId: "claude-agent",
});

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 eventSynthetic URL
PreToolUsehttps://claude-agent.local/tools/{tool_name}
UserPromptSubmithttps://claude-agent.local/prompts/user-prompt
yaml
agent: claude-agent
default: allow

rules:
  - name: deny-destructive-bash
    deny:
      url: "claude-agent.local/tools/Bash"
      body:
        command: "*rm -rf*|*dd if=*"

  - name: deny-secret-write
    deny:
      url: "claude-agent.local/tools/Write"
      body:
        file_path: "*.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

typescript
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 the claude-code subprocess adds a few ms. Keep hooks fast.
  • Duck-typed against the SDK shape — the HookMatcher and ClaudeAgentOptions types in the adapter are structural so a minor SDK bump doesn't force a Checkrd release.