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

DataCaptured asNotes
User prompttrace inputtext; image inputs leave a block summary in metadata
Each LLM API callgeneration observationplan (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 observationtool: bash (git) #N — key info in the name, full args in the input
Subagents (Agent / Task tool)subtreetool (1 subagent) #Nsubagent container → the child’s own plan/tool/response steps, parsed from the child transcript; child usage rolls up into the parent trace. Recursive.
Token usageusage_details on generationAnthropic-style keys (input / output / cache_read_input_tokens / cache_creation_input_tokens); attached once per API call, so totals don’t inflate
Model namemodel on generationused by Litefuse for cost computation
API errors / retries / rate limitsevent observation, level=ERRORfrom Claude Code’s system rows
Context compactioncontext compaction eventexplains sudden input-token drops on the next call
Tool errors (is_error=true)tool observation, level=ERRORwith a status-message preview
Interrupted turnsroot span level=WARNINGa turn that never reached a final response
Session groupingtrace session_idClaude Code session UUID
User identitytrace user_id$LITEFUSE_USER_ID, falls back to the OS username
Environmental contexttrace 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 didplan (n tools) #N when it requested tools, response for the final text answer, think #N for thinking-only steps — never after which model ran (that’s the model attribute).
  • One step counter per agent container. #N is a single chronological sequence shared by generations and tools; each subagent container restarts at #1. A tool’s agent_plan_step metadata points at the agent_step_index of the generation that requested it.
  • Subagent subtrees. When the model delegates via the Agent (formerly Task) tool, the hook locates the child transcript under <session>/subagents/agent-<id>.jsonl and 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 timestamp fields, 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 isMeta rows 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 text block), 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 an agent_ 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 python3 works, 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.py

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

  1. Download the new script over the old one (back up first if you’ve customized it).
  2. In settings.json, change the Stop hook command to plain python3 "$HOME"/.claude/hooks/litefuse_hook.py and remove the SubagentStop entry if present.
  3. Optionally rename the env keys to LITEFUSE_* (the LANGFUSE_* names keep working as a fallback) and delete TRACE_TO_LANGFUSE (no longer read; tracing is on whenever keys are present).
  4. 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.

VariableRequiredDescription
LITEFUSE_PUBLIC_KEYYesLitefuse project public key (pk-lf-...).
LITEFUSE_SECRET_KEYYesLitefuse project secret key (sk-lf-...).
LITEFUSE_BASE_URLNoDefaults to https://litefuse.cloud. Alias: LITEFUSE_HOST.
LITEFUSE_TRACING_ENVIRONMENTNoLitefuse environment for emitted traces. Defaults to production; use development for experiments so they don’t pollute production dashboards.
LITEFUSE_USER_IDNoOverrides the trace user_id. Falls back to the OS username, then hostname.
LITEFUSE_EXTRA_TARGETSNoJSON array of extra targets ([{"publicKey", "secretKey", "baseUrl", "environment"}]) to double-write traces to (e.g. self-hosted + cloud).
LITEFUSE_TRACEPARENTNoW3C 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_DISABLEDNoSet to "true" to switch the hook off without uninstalling.
CLAUDE_CODE_LITEFUSE_DEBUGNoSet to "true" for verbose hook logging.
CLAUDE_CODE_LITEFUSE_MAX_CHARSNoTruncation 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:

  1. Reads new bytes from the session transcript since the last offset (state in ~/.claude/state/litefuse_state.json, keyed by sha256(session_id::transcript_path)).
  2. Assembles rows into turns. Real user messages start turns; tool_result rows, system rows, and harness-injected isMeta rows attach to the in-flight turn.
  3. Defers the last turn when it’s still in progress (its final assistant row isn’t a text block) — the offset rewinds and the next firing re-evaluates. A per-session emitted_user_uuids set prevents double emission on rewind.
  4. Merges assistant rows into API calls by message.id, numbers steps, resolves tool results, recursively expands subagent transcripts.
  5. 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.log

Resources

Was this page helpful?