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 required.

The hook parses Claude Code’s session transcript (a JSONL file under ~/.claude/projects/) and emits one Litefuse trace per user turn, with separate observations for each LLM call (thinking / text / tool decision) and each tool execution.

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 block summary
Each thinking / text / tool_use block from the assistantgeneration observationone observation per JSONL row, real timestamps
Tool executions (input + output)tool observationwith a structured toolUseResult summary per tool type
Token usage (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens)usage_details on generationattached only to the last row of each Anthropic message, 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
Tool errors (is_error=true)tool observation, level=ERRORwith status_message preview
Session groupingtrace session_idClaude Code session UUID
User identitytrace user_id$LITEFUSE_USER_ID, falls back to getpass.getuser()
Claude Code versiontrace releasefrom JSONL version field
Environmental contexttrace metadatacwd, git branch, entrypoint, permission mode, sidechain flag
Per-observation identitymetadatauuid, parentUuid, requestId, message.id — JSONL field names preserved verbatim

Trace structure

A typical multi-tool turn produces a trace shaped like:

Claude Code - Turn 7                       (root span, AS_ROOT, trace headers)
├── user message                           (event — carries the user prompt)
├── Thinking (#1)                          (generation)
├── Text response (#2)                     (generation)
├── Decision to call tool: Bash (#3)       (generation, usage_details here)
├── Tool call: Bash (#3)                   (tool)
├── Thinking (#4)                          (generation)
├── Decision to call tool: Edit (#5)       (generation, usage_details here)
├── Tool call: Edit (#5)                   (tool)
└── Final response (#6)                    (generation)

Design notes:

  • user message event opens every turn. Instantaneous event-typed observation carrying the user’s prompt as input. Pairs visually with the closing Final response (#N) generation so the trace timeline reads top-to-bottom as a clean opening/closing structure.
  • Per-row fidelity: each JSONL row becomes its own observation. thinking, text, and tool_use blocks of the same Anthropic message are NOT merged.
  • Unique span names: the (#N) step suffix keeps observation names unique within a turn so Litefuse’s graph view stays linear even when the same tool is called multiple times.
  • Real timestamps: span start/end come from the JSONL timestamp field, not the hook’s wall clock — the Litefuse timeline reflects when things actually happened, including cold-start and queue latency.
  • No double-counted tokens: every JSONL row of a multi-row message carries the same usage block; the hook attaches usage_details only to the last row.
  • 1ms gap: consecutive sibling spans are separated by 1 ms so Litefuse’s graph view doesn’t treat boundary-touching observations as parallel branches.
  • Namespaced metadata under claude_code.*. Each Hermes-or-JSONL-specific field lives under that single key (e.g. claude_code.uuid, claude_code.requestId, claude_code.service_tier). Litefuse-standard fields (sessionId, userId, tags, release, usageDetails) stay at the top level. Sparse: fields that aren’t present in the source JSONL row are absent from metadata entirely, not padded with null.
  • In-progress turns are deferred. Claude Code’s Stop hook can fire while the agent is still mid-loop (between tool dispatches). If a turn’s last assistant row is anything other than a text block (e.g., a tool_use awaiting the next tool result), the hook defers emission — it rewinds its byte offset and waits for the next Stop firing, by which time the closing text “Final response” has usually landed. A per-session emitted_user_uuids set ensures previously-emitted turns are not re-emitted on the rewind. Trade-off: if the agent ever ends a turn without a final text response (e.g., killed mid-tool-loop), that turn won’t get a trace.

Quick Start

Prerequisites

  • Python ≥ 3.10 (Langfuse SDK v4 requirement). Check with python3 --version. macOS’s system python3 is usually 3.9 — install a newer one via Homebrew (brew install python@3.13, or any other 3.10+ minor) and make sure it’s first on PATH as python3.
  • A Litefuse project at https://litefuse.cloud with public + secret keys.

Create a virtualenv and install Langfuse v4

Isolate the hook’s dependencies in a dedicated venv:

python3 -m venv ~/.claude/hooks/.venv
~/.claude/hooks/.venv/bin/pip install --upgrade pip
~/.claude/hooks/.venv/bin/pip install 'langfuse>=4,<5'

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": {
    "TRACE_TO_LANGFUSE": "true",
    "LANGFUSE_PUBLIC_KEY": "pk-lf-xxx",
    "LANGFUSE_SECRET_KEY": "sk-lf-xxx",
    "LANGFUSE_BASE_URL": "https://litefuse.cloud"
  },
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$HOME\"/.claude/hooks/.venv/bin/python3 \"$HOME\"/.claude/hooks/litefuse_hook.py"
          }
        ]
      }
    ]
  }
}

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 N turns in X.XXs (session=...)"

Open the project in Litefuse — each user message becomes one trace with the structure shown above.

Environment variables

VariableRequiredDescription
TRACE_TO_LANGFUSEYesSet to "true" to enable the hook. Any other value short-circuits the script.
LANGFUSE_PUBLIC_KEYYesLitefuse project public key (pk-lf-...).
LANGFUSE_SECRET_KEYYesLitefuse project secret key (sk-lf-...).
LANGFUSE_BASE_URLNoDefaults to https://cloud.langfuse.com. Set to https://litefuse.cloud (or your self-hosted URL).
LITEFUSE_USER_IDNoOverrides the trace user_id. Falls back to getpass.getuser() then hostname.
CC_LANGFUSE_DEBUGNoSet to "true" for verbose hook logging.
CC_LANGFUSE_MAX_CHARSNoTruncation threshold (in characters) for span inputs/outputs. Default 1000000 (~1MB of text).

Trace metadata reference

All Claude-Code-specific fields are namespaced under claude_code.* in observation metadata. JSONL field names are preserved verbatim (camelCase / snake_case as they appear in the original transcript). Litefuse-standard fields (sessionId, userId, tags, release, usageDetails) stay at the top level via OTel trace attributes — they’re not duplicated under claude_code.

Trace root span — claude_code.* (from the user message row):

  • uuid, parentUuid, promptId, isMeta, origin — row identity
  • cwd, gitBranch, version, entrypoint, userType, isSidechain, permissionMode — Claude Code environmental context
  • sessionId, turn_number, transcript_path, models_used, source — derived
  • user_text_truncated, user_text_orig_len — set when the user prompt is truncated
  • user_content_text_blocks, user_content_image_blocks, user_content_image_media_types — set when the user sent images

user message event — claude_code.*:

  • uuid, parentUuid, promptId — same identity fields as the trace root
  • user_content_*, user_text_* — same content / truncation summaries as the trace root

Generation observation — claude_code.* (from the assistant row):

  • uuid, parentUuid, requestId — JSONL row identity
  • id (Anthropic message id), stop_reason, stop_sequence, stop_details — Anthropic message fields
  • service_tier, speed, inference_geo, iterations — Anthropic usage metadata
  • cache_creation (ephemeral_1h / ephemeral_5m breakdown), server_tool_use — informational; NOT summed into usage_details
  • diagnostics — Anthropic’s cache_miss_reason etc.; useful when cache_creation_input_tokens is unexpectedly large
  • context_management, container — Anthropic-side, when present
  • step_index, is_last_in_message — derived
  • assistant_text_truncated, assistant_text_orig_len, … — only when the text was truncated

Tool observation — claude_code.*:

  • name, tool_use_id — from the tool_use block
  • uuid, parentUuid, sourceToolAssistantUUID — from the user row carrying the tool_result
  • is_error — from the tool_result block
  • toolUseResult — flat summary; fields vary by tool type:
    • Bash: command, stdout_len, stderr_len, has_stderr, interrupted
    • WebFetch / WebSearch: code, codeText, bytes
    • Task (subagent): agentId, agentType
    • BashOutput: backgroundTaskId, assistantAutoBackgrounded
    • Glob / Grep / Read: appliedLimit, appliedOffset, itemCount
  • input_truncated, input_orig_len, output_truncated, output_orig_len — truncation info

Event observation — claude_code.* (from system rows — API errors, retries, hook outcomes):

  • All system row fields preserved verbatim: subtype, level, cause, error, uuid, parentUuid, stopReason, toolUseID, hookCount, hookErrors, hookInfos, retryAttempt, maxRetries, retryInMs, preventedContinuation, hasOutput
  • The stop_hook_summary subtype is intentionally filtered out (records this script’s own execution).

How it works

On every Stop hook firing the script does:

  1. Reads new bytes from the session’s JSONL transcript since the last offset (state cached in ~/.claude/state/langfuse_state.json, keyed by sha256(session_id::transcript_path)).
  2. Parses rows; deduplicates by uuid (the transcript occasionally repeats rows on session resume).
  3. Groups rows into turns: each real user message starts a turn; subsequent assistant rows + tool_result rows + non-summary system rows attach to it.
  4. Checks each turn’s user_msg.uuid against the per-session emitted_user_uuids set (also cached in state). Already-emitted turns are skipped — protects against double-emission when the offset rewinds for deferral (see step 6).
  5. Evaluates the LAST turn’s completeness: if its final assistant row contains a text block (= the Claude Code “Final response” closing marker), the turn is complete and emits. Otherwise — typically because the agent’s still mid-loop, with the latest assistant row being a tool_use awaiting the next iteration — the hook defers.
  6. On defer: rewind offset to its pre-read value so the next Stop firing re-reads the same bytes. By then the missing text row has usually landed and the turn becomes emittable. The emitted_user_uuids set keeps the earlier completed turns from being re-emitted during this rewind.
  7. For each completed turn, emits a Litefuse trace via Langfuse SDK v4’s OTel layer, using explicit start/end timestamps from the JSONL so the timeline reflects when each block actually arrived.
  8. Saves new offset + emitted_user_uuids so the next hook invocation continues from where this one stopped.

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 Claude Code’s main loop.

Troubleshooting

No traces appear in Litefuse. Tail ~/.claude/state/litefuse_hook.log. An empty log means the hook isn’t running — confirm TRACE_TO_LANGFUSE=true is set and settings.json’s command path is correct (the venv’s python3 plus the hook script).

Hook log says Langfuse client init failed. The venv’s Python doesn’t have a working langfuse install. Re-run:

~/.claude/hooks/.venv/bin/pip install --upgrade 'langfuse>=4,<5'

Hook log says Missing session_id or transcript_path; exiting. Claude Code didn’t pass the expected payload on stdin. Confirm you’re on a Claude Code version that emits Stop hook payloads (see Claude Code hooks).

Token totals look wrong. The hook attaches usage_details only to the LAST row of each Anthropic message — verify by inspecting individual generation observations in Litefuse: only Decision to call tool: X (#N) / Final response (#N) rows carry token counts; Thinking (#N) / Text response (#N) from the same message do not.

A turn never appears as a trace. Look at the hook log — if you see Processed N turns, deferred 1 (in-progress), the latest turn is being held back because its last assistant row isn’t a text block. The next Stop firing will re-evaluate it. If the turn was killed mid-tool-loop (no text closing row will ever land), the deferral becomes permanent and no trace is emitted. Send any follow-up message in Claude Code and the deferred turn will be re-checked: if there’s now a newer user message after it, the dedup-on-followup path emits it regardless of completeness (a past turn followed by a fresh user message is “as complete as it’ll ever be”).

Test the hook manually:

TRACE_TO_LANGFUSE=true \
LANGFUSE_PUBLIC_KEY="pk-lf-..." \
LANGFUSE_SECRET_KEY="sk-lf-..." \
LANGFUSE_BASE_URL="https://litefuse.cloud" \
~/.claude/hooks/.venv/bin/python3 ~/.claude/hooks/litefuse_hook.py
# Errors appear in the log file.

Enable debug logging:

# Add to settings.json env block:
"CC_LANGFUSE_DEBUG": "true"

Resources

Was this page helpful?