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, 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:

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

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.