Claude Code Tracing with Litefuse
Claude Code is Anthropic’s terminal-based coding agent. This integration uses Claude Code’s Stop hook to send each conversation turn to Litefuse — no Claude Code source changes, no SDK, no virtualenv. The hook is a single Python file with zero dependencies (standard library only) that ships spans straight to Litefuse’s OTLP endpoint.
The hook parses Claude Code’s session transcript (a JSONL file under ~/.claude/projects/) and emits one Litefuse trace per user turn: one generation per LLM API call, one tool observation per tool execution, and a full subtree for every subagent delegation.
For AI — automated install
If you’re chatting with Claude Code right now, paste this prompt and the agent will handle the whole install end-to-end:
Read https://litefuse.ai/SKILL.md and follow the instructions to install and configure Litefuse for Claude Code.
The skill will ask for your Litefuse API keys (or walk you through signing up if you don’t have an account yet), then configure everything in place. For step-by-step manual setup instead, continue below.
What gets captured
| Data | Captured as | Notes |
|---|---|---|
| User prompt | trace input | text; image inputs leave a block summary in metadata |
| Each LLM API call | generation observation | plan (n tools) #N / response / think #N, named after what the model did; thinking / text / tool_use block structure preserved in the output |
| Tool executions (input + output) | tool observation | tool: bash (git) #N — key info in the name, full args in the input |
Subagents (Agent / Task tool) | subtree | tool (1 subagent) #N → subagent container → the child’s own plan/tool/response steps, parsed from the child transcript; child usage rolls up into the parent trace. Recursive. |
| Token usage | usage_details on generation | Anthropic-style keys (input / output / cache_read_input_tokens / cache_creation_input_tokens); attached once per API call, so totals don’t inflate |
| Model name | model on generation | used by Litefuse for cost computation |
| API errors / retries / rate limits | event observation, level=ERROR | from Claude Code’s system rows |
| Context compaction | context compaction event | explains sudden input-token drops on the next call |
Tool errors (is_error=true) | tool observation, level=ERROR | with a status-message preview |
| Interrupted turns | root span level=WARNING | a turn that never reached a final response |
| Session grouping | trace session_id | Claude Code session UUID |
| User identity | trace user_id | $LITEFUSE_USER_ID, falls back to the OS username |
| Environmental context | trace metadata (agent_*) | cwd, git branch, Claude Code version, permission mode, turn stats |
Trace structure
A turn that delegates to a subagent produces a trace shaped like this (real example):
Claude Code — Turn 4 (AGENT root span, trace headers)
├── plan (1 tool) #1 (generation — usage, real latency)
├── tool (1 subagent) #2 (tool — the delegation as seen by the parent)
│ └── subagent (AGENT container — parsed from the child transcript)
│ ├── plan (2 tools) #1 (child-local numbering restarts at #1)
│ ├── tool: bash (wc) #2
│ ├── tool: read (architecture.md) #3
│ └── subagent response (generation — the child's final answer)
├── plan (1 tool) #3
├── tool: bash (ls) #4
└── response (generation — the final answer, ends the turn)Design notes:
- One generation per LLM API call. Claude Code splits one Anthropic response across multiple JSONL rows (one per content block); the hook merges them back by
message.id. Generations are named after what the model did —plan (n tools) #Nwhen it requested tools,responsefor the final text answer,think #Nfor thinking-only steps — never after which model ran (that’s themodelattribute). - One step counter per agent container.
#Nis a single chronological sequence shared by generations and tools; each subagent container restarts at#1. A tool’sagent_plan_stepmetadata points at theagent_step_indexof the generation that requested it. - Subagent subtrees. When the model delegates via the
Agent(formerlyTask) tool, the hook locates the child transcript under<session>/subagents/agent-<id>.jsonland emits the full three-level subtree. The delegation tool span deliberately wraps the container: tool-span duration − container duration = the real overhead of delegating (child runtime startup, result collection and bookkeeping) — typically a few seconds. - Real timestamps. Span start/end come from the JSONL
timestampfields, not the hook’s wall clock. Consecutive same-timestamp siblings are spaced 1 ms apart so the graph view stays linear. - Harness-injected user rows are not turns. Skill expansions, auto-continue messages, and other
isMetarows do not split the trace — they continue the in-flight turn and feed the next generation’s input. - In-progress turns are deferred. If the Stop hook fires mid-loop (the last assistant row isn’t a final
textblock), the turn is held back and re-evaluated on the next firing — no half trace is ever sent. - Deterministic IDs. Trace and span IDs are derived from the session ID and row UUIDs, so an accidental re-send upserts instead of duplicating.
- Flat
agent_*metadata. All integration fields live at the metadata top level with anagent_prefix (agent_step_index,agent_plan_step,agent_duration_ms…) — the same keys as every other Litefuse agent integration, so one dashboard query works across all of them.
When do traces appear?
The hook runs when the Stop hook fires — i.e. at the end of each turn. Everything in the turn (including subagent subtrees) uploads as one batch at that point; nothing is visible mid-turn. This is a deliberate difference from event-driven integrations like Pi, which send each observation the moment it ends — a transcript-parsing hook only gets to run when Claude Code invokes it.
Quick Start
Prerequisites
- Python ≥ 3.8 — any
python3works, including macOS’s system Python. The hook has zero third-party dependencies: no SDK, no virtualenv, no pip install. - A Litefuse project at https://litefuse.cloud with public + secret keys.
Download the hook script
mkdir -p ~/.claude/hooks
curl -fsSL https://litefuse.ai/integrations/claude-code/litefuse_hook.py \
-o ~/.claude/hooks/litefuse_hook.py
chmod +x ~/.claude/hooks/litefuse_hook.pyThe source is also browseable at the same URL — feel free to read it before deploying.
Configure ~/.claude/settings.json
Add the Stop hook and Litefuse credentials:
{
"env": {
"LITEFUSE_PUBLIC_KEY": "pk-lf-xxx",
"LITEFUSE_SECRET_KEY": "sk-lf-xxx",
"LITEFUSE_BASE_URL": "https://litefuse.cloud"
},
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME\"/.claude/hooks/litefuse_hook.py"
}
]
}
]
}
}Do not register a SubagentStop hook — subagent subtrees are emitted by the parent session’s Stop hook.
For per-project use, put the same env block in <project>/.claude/settings.local.json instead.
Verify
Send a message in Claude Code, then watch the hook log:
tail -f ~/.claude/state/litefuse_hook.log
# Expected: "processed 1 turn(s), N span(s) in X.XXs (session=...)"Open the project in Litefuse — each user message becomes one trace with the structure shown above.
Upgrading from v1
The previous version of this hook used the Langfuse Python SDK inside a virtualenv at ~/.claude/hooks/.venv. v2 needs none of that:
- Download the new script over the old one (back up first if you’ve customized it).
- In
settings.json, change the Stop hook command to plainpython3 "$HOME"/.claude/hooks/litefuse_hook.pyand remove theSubagentStopentry if present. - Optionally rename the env keys to
LITEFUSE_*(theLANGFUSE_*names keep working as a fallback) and deleteTRACE_TO_LANGFUSE(no longer read; tracing is on whenever keys are present). - Optionally
rm -rf ~/.claude/hooks/.venv— nothing uses it anymore.
Turn numbering and the byte-offset state carry over unchanged (~/.claude/state/litefuse_state.json). Note that v2 renames observations (plan (n tools) #N instead of Decision to call tool: …, lowercase tool: bash … #N instead of Tool call: Bash (#N)) and flattens metadata to agent_* keys — update any saved dashboard filters.
Environment variables
LITEFUSE_* takes precedence; the equivalent LANGFUSE_* names are accepted as an ecosystem-compatible fallback.
| Variable | Required | Description |
|---|---|---|
LITEFUSE_PUBLIC_KEY | Yes | Litefuse project public key (pk-lf-...). |
LITEFUSE_SECRET_KEY | Yes | Litefuse project secret key (sk-lf-...). |
LITEFUSE_BASE_URL | No | Defaults to https://litefuse.cloud. Alias: LITEFUSE_HOST. |
LITEFUSE_TRACING_ENVIRONMENT | No | Litefuse environment for emitted traces. Defaults to production; use development for experiments so they don’t pollute production dashboards. |
LITEFUSE_USER_ID | No | Overrides the trace user_id. Falls back to the OS username, then hostname. |
LITEFUSE_EXTRA_TARGETS | No | JSON array of extra targets ([{"publicKey", "secretKey", "baseUrl", "environment"}]) to double-write traces to (e.g. self-hosted + cloud). |
LITEFUSE_TRACEPARENT | No | W3C traceparent (00-<traceId>-<spanId>-01). When another agent’s collector spawned this Claude Code process as a subagent, set this to make every turn join the parent’s trace as a subagent container instead of creating its own traces. |
CLAUDE_CODE_LITEFUSE_DISABLED | No | Set to "true" to switch the hook off without uninstalling. |
CLAUDE_CODE_LITEFUSE_DEBUG | No | Set to "true" for verbose hook logging. |
CLAUDE_CODE_LITEFUSE_MAX_CHARS | No | Truncation threshold (in characters) for span inputs/outputs. Default 1000000. |
Metadata reference
All integration fields are flat top-level metadata keys with an agent_ prefix (shared across Litefuse agent integrations). Fields absent from the source transcript are omitted entirely, never padded with null.
Trace root: agent_turn_number, agent_session_id, agent_cwd, agent_model, agent_provider, agent_api_calls, agent_tool_calls, agent_steps, agent_message_count, agent_duration_ms, agent_git_branch, agent_claude_code_version, agent_permission_mode, agent_entrypoint, agent_transcript_path, agent_user_uuid, agent_prompt_id; agent_image_blocks / agent_image_media_types when the prompt contains images; agent_prompt_truncated / agent_final_text_truncated (+ _orig_len) on truncation.
Generation: agent_turn_number, agent_step_index, agent_provider, agent_stop_reason, agent_message_id, agent_request_id, agent_api_duration_ms, agent_tool_call_count, agent_thinking_chars, agent_service_tier, truncation markers.
Tool: agent_turn_number, agent_step_index, agent_plan_step (join key: tool.agent_plan_step == generation.agent_step_index), agent_tool_name (original casing, e.g. Bash), agent_tool_call_id, agent_duration_ms, agent_is_error, agent_details (flat per-tool-type summary of toolUseResult: exit info for Bash, HTTP status for WebFetch, item counts for Grep/Glob, agent id/type for delegations), truncation markers.
Subagent container: agent_subagent: true, agent_subagent_id, agent_subagent_type, agent_subagent_description, plus that run’s agent_api_calls / agent_tool_calls / agent_steps / agent_duration_ms.
How it works
On every Stop hook firing the script:
- Reads new bytes from the session transcript since the last offset (state in
~/.claude/state/litefuse_state.json, keyed bysha256(session_id::transcript_path)). - Assembles rows into turns. Real user messages start turns;
tool_resultrows,systemrows, and harness-injectedisMetarows attach to the in-flight turn. - Defers the last turn when it’s still in progress (its final assistant row isn’t a
textblock) — the offset rewinds and the next firing re-evaluates. A per-sessionemitted_user_uuidsset prevents double emission on rewind. - Merges assistant rows into API calls by
message.id, numbers steps, resolves tool results, recursively expands subagent transcripts. - Sends everything as OTLP/HTTP JSON to
<base_url>/api/public/otel/v1/traces(batched under the endpoint’s body-size limit, Basic auth, 10 s timeout). Trace headers ride on every span.
The hook is fail-open: any unexpected error is logged to ~/.claude/state/litefuse_hook.log and the script exits 0, so it never blocks or slows Claude Code.
Troubleshooting
No traces appear in Litefuse. Tail ~/.claude/state/litefuse_hook.log. An empty log means the hook isn’t running — check the command path in settings.json. A send failed: line means keys or network: verify LITEFUSE_PUBLIC_KEY / LITEFUSE_SECRET_KEY / LITEFUSE_BASE_URL.
The latest turn is missing. Look for deferred 1 (in-progress) in the log — the turn hadn’t reached its final response when the hook fired. It will be emitted on the next Stop firing.
A trace ends in a tool span with a WARNING root. That turn genuinely never produced a final text answer (killed or interrupted mid tool-loop). The WARNING status message says so; it is not a collection error.
The subagent container is shorter than its parent tool span. Expected — the difference is the real cost of delegating (child startup + result collection). The container duration matches the harness’s own totalDurationMs to within milliseconds.
Cost shows 0. Litefuse computes cost from the model name; make sure your Litefuse project has a price entry matching the model (e.g. claude-opus-4-8) under Settings → Models.
Test the hook manually (uses a development environment so production stays clean):
echo '{"session_id":"manual-test","transcript_path":"'$HOME'/.claude/projects/<proj>/<session>.jsonl"}' | \
LITEFUSE_PUBLIC_KEY="pk-lf-..." \
LITEFUSE_SECRET_KEY="sk-lf-..." \
LITEFUSE_BASE_URL="https://litefuse.cloud" \
LITEFUSE_TRACING_ENVIRONMENT="development" \
CLAUDE_CODE_LITEFUSE_DEBUG=true \
python3 ~/.claude/hooks/litefuse_hook.py
tail ~/.claude/state/litefuse_hook.logResources
- Claude Code hooks documentation
- Litefuse Cloud
- Hook script source:
litefuse_hook.py