checkrd

Anthropic Claude Agent SDK

Add Checkrd enforcement to the Anthropic Claude Agent SDK via PreToolUse and PostToolUse hooks.

Anthropic Claude Agent SDK (Python)

The Claude Agent SDK supports hooks at well-defined points in the agent's run loop: PreToolUse, PostToolUse, UserPromptSubmit, Stop, and several others. Checkrd's adapter wires hooks for the four most common events; user-supplied hooks coexist on the same matchers.

Install

bash
pip install 'checkrd[claude-agent-sdk]'

Quickstart

python
from claude_agent_sdk import ClaudeAgentOptions, query
from checkrd import Checkrd
from checkrd.integrations.claude_agent_sdk import attach_to_options

async def main():
    with Checkrd(policy="policy.yaml") as client:
        options = ClaudeAgentOptions()
        attach_to_options(options, client=client)

        async for msg in query(prompt="summarize this", options=options):
            print(msg)

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

python
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
from checkrd.integrations.claude_agent_sdk import (
    make_pre_tool_use_hook,
    make_post_tool_use_hook,
)

with Checkrd() as client:
    pre = make_pre_tool_use_hook(
        engine=client._runtime.engine,   # or use attach_to_options
        agent_id="my-agent",
        enforce=True,
    )
    post = make_post_tool_use_hook(
        engine=client._runtime.engine,
        agent_id="my-agent",
    )

    options = ClaudeAgentOptions(
        hooks={
            "PreToolUse": [HookMatcher(matcher="Bash|Write|Edit", hooks=[pre], timeout=30)],
            "PostToolUse": [HookMatcher(hooks=[post])],
        },
    )

The matcher argument is a regex over tool names — leave it None to match every tool.

What gets enforced

Hook eventSynthetic URLBody
PreToolUsehttps://claude-agent.local/tools/{tool_name}{"tool_input": ..., "session_id": ...}
UserPromptSubmithttps://claude-agent.local/prompts/user-prompt{"prompt": ..., "session_id": ...}

Example policy denying destructive Bash commands:

yaml
agent: claude-agent
default: allow

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

  - 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

Set enforce=False to log denies without blocking. Useful for staging new policies:

python
attach_to_options(options, client=client, 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 must be async def. The SDK rejects sync hooks at registration.
  • Python SDK has fewer events than the TS SDK. The Python SDK supports PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStop, PreCompact, Notification, SubagentStart, PermissionRequest. The TS SDK adds session lifecycle events not yet in Python.
  • Hook latency adds to every tool call. The WASM engine's evaluate() is sub-millisecond, but the hook IPC to the claude-code subprocess adds a few ms per call. Keep hooks fast.
  • session_id is the trace correlation key, not the tool_use_id. The same session can have many PreToolUse events.