A Claude Code plugin that traces conversations to Langfuse with full fidelity — multiple LLM generations per turn, thinking blocks, token usage, and properly nested tool observations.
Session (session_id)
└── Trace: "Claude Code - Turn N"
├── Generation: "LLM Call 1/3" (model, usage, thinking in output)
│ ├── Span: "Tool: Glob" (input, output, duration_ms)
│ └── Span: "Tool: Read" (input, output, duration_ms)
├── Generation: "LLM Call 2/3" (model, usage)
│ └── Span: "Tool: Edit" (input, output, duration_ms)
└── Generation: "LLM Call 3/3" (model, usage, final response)
- Multiple generations per turn — each LLM API call gets its own generation observation
- Thinking blocks preserved — stored in the generation
output.thinkingfield - Token usage tracked —
input_tokens,output_tokens, cache metrics in metadata - Tools nested under their parent generation — shows which LLM call triggered which tool
- Streaming chunks merged — content blocks from all transcript lines with the same
message.idare concatenated - Subagent tracing — Agent tool calls and their subagent transcripts are traced
- Context compaction events — tracked via PreCompact/PostCompact hooks
- Interrupted turn recovery — turns interrupted by user escape are properly closed
/plugin marketplace add pdhoolia/langfuse-claude-code-plugin
/plugin install langfuse-tracing@langfuse-claude-code-plugin
/reload-pluginsClone the repo and build:
git clone https://github.com/pdhoolia/langfuse-claude-code-plugin.git
cd langfuse-claude-code-plugin
pnpm install
pnpm buildThen run Claude Code with the plugin directory:
claude --plugin-dir /path/to/langfuse-claude-code-pluginexport TRACE_TO_LANGFUSE=true
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...TRACE_TO_LANGFUSE=true is required to enable tracing. Without it, all hooks exit early.
| Variable | Default | Description |
|---|---|---|
TRACE_TO_LANGFUSE |
(none) | Must be true to enable tracing |
CC_LANGFUSE_PUBLIC_KEY |
falls back to LANGFUSE_PUBLIC_KEY |
Langfuse public key |
CC_LANGFUSE_SECRET_KEY |
falls back to LANGFUSE_SECRET_KEY |
Langfuse secret key |
CC_LANGFUSE_BASE_URL |
https://cloud.langfuse.com |
Langfuse API host (for self-hosted) |
CC_LANGFUSE_DEBUG |
false |
Enable debug logging |
CC_LANGFUSE_MAX_CHARS |
50000 |
Max characters before truncation |
| Hook | When | What it does |
|---|---|---|
| UserPromptSubmit | User sends a prompt | Creates the Langfuse trace eagerly; handles interrupted turn recovery |
| PreToolUse | Before tool executes | Records wall-clock start time in state |
| PostToolUse | After tool executes | Records tool end time; for Agent tools, stores deferred creation info |
| Stop | Agent finishes responding | Reads transcript, builds turns, creates nested generation/tool observations |
| StopFailure | Turn ends due to API error | Updates trace with error status |
| SubagentStop | Subagent finishes | Queues subagent transcript info for Stop to process |
| PreCompact | Before context compaction | Records compaction start time |
| PostCompact | After context compaction | Creates compaction span/trace |
| SessionEnd | Session ends | Closes interrupted turns |
- UserPromptSubmit creates a Langfuse trace eagerly (so it appears immediately) and stores the trace ID in session state.
- PreToolUse records tool start times for accurate timing.
- The Stop hook reads new lines from the JSONL transcript (line-number tracking), groups messages into turns, and creates nested observations:
- One generation per LLM API call (not one per turn)
- Tool spans nested under their parent generation
- Streaming chunks with the same
message.idmerged into a single generation
- SubagentStop queues subagent transcript paths; Stop reads and traces them.
- State is persisted to
~/.claude/state/langfuse_state.jsonwith atomic file locking.
pnpm install # Install dependencies
pnpm build # TypeScript compile + esbuild bundle (tsc && pnpm bundle)
pnpm dev # Watch mode — recompiles TypeScript on changes
pnpm test # Run tests (vitest)
pnpm lint # Lint with oxlint
pnpm lint:fix # Lint and auto-fix
pnpm format # Format with oxfmtAfter making changes, run pnpm build — the bundle/ directory contains the executable hooks.
# Enable debug logging
export CC_LANGFUSE_DEBUG=true
# View logs
tail -f ~/.claude/state/langfuse_hook.logThis plugin replaces the standalone langfuse_hook.py Python script. Key differences:
- Plugin format — no manual hook registration needed; just install the plugin
- TypeScript — compiled and bundled into self-contained ESM files
- Multi-hook architecture — 9 hooks instead of a single Stop hook
- Line-based tracking — uses line numbers instead of byte offsets
- No Python dependency — runs on Node.js (bundled with Claude Code)
If both the Python hook and the plugin run at the same time, every turn will be traced twice. Open ~/.claude/settings.json and delete the entry that invokes langfuse_hook.py from the Stop hooks array. A typical old config looks like this:
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "python3 ~/.claude/hooks/langfuse_hook.py" }
]
}
]
}Remove the python3 ~/.claude/hooks/langfuse_hook.py command. If it was the only entry in the Stop array, drop the whole Stop block. Leave any other unrelated hooks alone.
You can also delete the script itself once you no longer need it:
rm ~/.claude/hooks/langfuse_hook.pyAnd if nothing else on your system uses it, uninstall the Python SDK:
pip uninstall langfuseYour existing TRACE_TO_LANGFUSE, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_BASE_URL entries (in ~/.claude/settings.json under env, or exported in your shell) work as-is — the plugin reads the same variables. No changes needed.
/plugin marketplace add pdhoolia/langfuse-claude-code-plugin
/plugin install langfuse-tracing@langfuse-claude-code-pluginThis enables the plugin globally (every session in every directory). To scope it to a single project instead, enable it in that project's .claude/settings.json under enabledPlugins rather than the global one.
Start a new Claude Code session, send a prompt, and confirm exactly one trace per turn appears in Langfuse. Tail the hook log if anything looks off:
tail -f ~/.claude/state/langfuse_hook.logMIT