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
pip install 'checkrd[claude-agent-sdk]'Quickstart
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:
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 event | Synthetic URL | Body |
|---|---|---|
PreToolUse | https://claude-agent.local/tools/{tool_name} | {"tool_input": ..., "session_id": ...} |
UserPromptSubmit | https://claude-agent.local/prompts/user-prompt | {"prompt": ..., "session_id": ...} |
Example policy denying destructive Bash commands:
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:
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 theclaude-codesubprocess adds a few ms per call. Keep hooks fast. session_idis the trace correlation key, not the tool_use_id. The same session can have manyPreToolUseevents.