Set it up
Recommended path: The Toolcaise Python SDK, with Claude Agent SDK hooks. You need an agent credential from Dashboard, Agents: one credential per agent.
1. Install the Toolcaise SDK
pip install claude-agent-sdk
curl -fsSLO https://toolcaise.com/downloads/toolcaise_agent.py # then: from toolcaise_agent import Toolcaise2. Wrap the session, report its usage, and gate tools with hooks
The Claude Agent SDK makes its model calls inside the Claude Code process it starts, so a client wrapper cannot see them. Report the usage from the result message instead, and use hooks for tools. A PreToolUse hook that waits for approval must have a timeout long enough for a person to answer.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher, ResultMessage
from toolcaise_agent import Toolcaise
toolcaise = Toolcaise()
async def main():
async with toolcaise.run("repo-triage") as run_id:
async def require_approval(input_data, tool_use_id, context):
decision = "deny"
try:
await toolcaise.aguarded_action(
"Run a shell command",
{"tool": input_data["tool_name"], "input": input_data["tool_input"]},
lambda approved: None,
run_id=run_id,
)
decision = "allow"
except Exception:
pass # Denied, expired or Toolcaise unreachable: fail closed.
return {"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": decision,
"permissionDecisionReason": "Decided in Toolcaise",
}}
async def record_tool(input_data, tool_use_id, context):
toolcaise.event("claude.tool_used", "Used " + input_data["tool_name"], run_id=run_id)
return {}
options = ClaudeAgentOptions(hooks={
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[require_approval], timeout=3600)],
"PostToolUse": [HookMatcher(hooks=[record_tool])],
})
async with toolcaise.span("claude session", kind="model"):
async with ClaudeSDKClient(options=options) as claude:
await claude.query("Label the open issues in this repository")
async for message in claude.receive_response():
if isinstance(message, ResultMessage):
usage = message.usage or {}
toolcaise.update_span(
model=next(iter(message.model_usage or {}), None),
input_tokens=sum(usage.get(key) or 0 for key in (
"input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens")),
output_tokens=usage.get("output_tokens"),
reported_cost_usd=message.total_cost_usd,
)
asyncio.run(main())Then run the agent as usual. Its runs appear in the dashboard as soon as their first report arrives.
What Toolcaise receives
| How it arrives | |
|---|---|
| Runs | One run per session, completed, failed or canceled. |
| Usage | One model span for the session with the input and output tokens and the cost from the result message. These are the Claude Agent SDK's own figures: its usage leaves out subagents, and its cost is a client-side estimate. |
| Tool calls | An event per tool call, carrying only the tool's name. |
| Decisions | A person approves or denies each gated tool call. The approval binds the exact tool input; Toolcaise receives its digest, never the input. |
What this path does not give you:
- A span per model call: they happen inside the Claude Code process.
Everything here is reported by your agent or its framework and stored as agent-reported. Prompts, completions and tool arguments are never read: the OpenTelemetry endpoint reads only an allow-list of attributes, and the SDK sends metadata only.
Other ways
Measured, with limits: Toolcaise Connect
Run the agent under toolcaise-connect run. The connector points ANTHROPIC_BASE_URL at a local gateway, which the Claude Code process the SDK starts honours, measures every model call itself, and can stop the run on a token, cost or time limit. We have not run the Claude Agent SDK under Connect.
toolcaise-connect run --agent "Repo triage" --max-runtime 30m --max-total-tokens 2000000 -- python triage.pyNot recommended yet: Claude Code's OpenTelemetry traces
With CLAUDE_CODE_ENABLE_TELEMETRY=1, CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER=otlp, the Claude Code process exports trace spans (beta). Their token counts and tool names use attribute names Toolcaise does not read, so Toolcaise would show model calls by model name only, and tools as unnamed steps.
Caveats
- In TypeScript, query() returns the same result message (usage, total_cost_usd, modelUsage) and accepts the same hooks. options.env replaces the environment rather than adding to it, so spread process.env into it.
- Hook callbacks here capture run_id explicitly rather than relying on the current run following the SDK's internal tasks.
What was checked
Checked on 2026-09-22 against claude-agent-sdk 0.2.157 (its ResultMessage and hook types) and Anthropic's hooks, cost-tracking and LLM gateway documentation. Not run end to end.
Reference: SDKs, HTTP API, and the OpenTelemetry endpoint's limits and answers.