Send Claude Code, OpenCode, or Pi session traces to Langfuse for observability.
Runs as a Claude Code Stop hook, an OpenCode plugin, or a Pi extension — after each assistant response, it assembles conversational turns and sends them to Langfuse as structured traces with generations and tool spans.
Written in Rust for fast startup and zero runtime dependencies. The process forks after assembling the payload — the parent exits immediately while the child sends the HTTP request in the background, adding minimal latency to your workflow.
| Agent | Integration |
|---|---|
| Claude Code | Stop hook (settings.json) |
| OpenCode | Plugin (~/.config/opencode/plugins/) |
| Pi | Extension (~/.pi/agent/extensions/) |
Each turn produces:
- Trace with session grouping, input/output, and tags
- Generation span with model name and token-level cost tracking
- Tool spans for each tool call (Bash, Read, Edit, etc.) with input/output
Traces are automatically tagged with:
| Tag | Example |
|---|---|
repo:<name> |
repo:my-project |
branch:<name> |
branch:main |
user:<name> |
user:doug |
host:<hostname> |
host:codex |
os:<platform> |
os:linux |
cc-version:<ver> |
cc-version:2.1.100 (Claude Code) |
oc-version:<ver> |
oc-version:0.4.5 (OpenCode) |
pi-version:<ver> |
pi-version:1.2.0 (Pi) |
curl -sfL https://raw.githubusercontent.com/isotoma/code-trace/main/install.sh | bashThis installs the binary to ~/.local/bin/code-trace. Make sure ~/.local/bin is in your PATH.
To also install the OpenCode plugin:
curl -sfL https://raw.githubusercontent.com/isotoma/code-trace/main/install.sh | bash -s -- --opencodeTo also install the Pi extension:
curl -sfL https://raw.githubusercontent.com/isotoma/code-trace/main/install.sh | bash -s -- --picargo install --git https://github.com/isotoma/code-trace.gitgit clone https://github.com/isotoma/code-trace.git
cd code-trace
cargo build --release
cp target/release/code-trace ~/.local/bin/Add your credentials to ~/.config/code-trace/config (created by the install script):
TRACE_TO_LANGFUSE=true
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
# LANGFUSE_BASE_URL=https://cloud.langfuse.com
# CODE_TRACE_DEBUG=false
This file is read by the binary at startup and works the same regardless of which agent you're using.
Respects $XDG_CONFIG_HOME: if set, the file is read from $XDG_CONFIG_HOME/code-trace/config.
Each agent also needs the hook/plugin installed so it invokes the binary.
The install script does this automatically. If not, add to ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "code-trace"
}
]
}
]
}
}Copy plugin/opencode/code-trace.ts to your OpenCode plugins directory:
mkdir -p ~/.config/opencode/plugins/
cp plugin/opencode/code-trace.ts ~/.config/opencode/plugins/code-trace.tsOr use the install script with --opencode (see above).
Copy plugin/pi-agent/code-trace.ts to your Pi extensions directory:
mkdir -p ~/.pi/agent/extensions/
cp plugin/pi-agent/code-trace.ts ~/.pi/agent/extensions/code-trace.tsOr use the install script with --pi (see above).
Environment variables take precedence over the config file. This is useful for per-project credentials or CI environments.
| Variable | Required | Description |
|---|---|---|
TRACE_TO_LANGFUSE |
Yes | Set to true to enable tracing |
LANGFUSE_PUBLIC_KEY |
Yes | Langfuse public key |
LANGFUSE_SECRET_KEY |
Yes | Langfuse secret key |
LANGFUSE_BASE_URL |
No | Langfuse host (default: https://cloud.langfuse.com) |
CODE_TRACE_DEBUG |
No | Set to true for debug logging (alias: CC_TRACE_DEBUG) |
The CC_LANGFUSE_ prefix is also accepted for all Langfuse variables (e.g. CC_LANGFUSE_PUBLIC_KEY).
Once TRACE_TO_LANGFUSE=true is set, every session traces by default. When working with confidential data, individual sessions can be paused, inspected, and purged without touching the global flag or other concurrently-running agents.
code-trace read a Stop-hook payload on stdin and emit (default)
code-trace --on-start SessionStart handler: record session, print tracing reminder
code-trace status show tracing configuration and session counts
code-trace sessions list known sessions (most recent first)
code-trace pause [--session <id>] pause tracing for a session (default: most recent)
code-trace resume [--session <id>] resume tracing for a session (default: most recent)
code-trace purge --session <id> delete a session's Langfuse traces, transcript, and state
code-trace --version / --help
Any other invocation falls through to the stdin/emit path, so the installed Stop hook keeps working unchanged.
code-trace pause marks the most-recently-seen session as suppressed and prints which session it targeted — with parallel agents, pass --session <id> (find ids with code-trace sessions) to be explicit. A suppressed session:
- emits nothing, for all sources (Claude Code, OpenCode, Pi) — and turns that happen while paused are never traced, not deferred: the hook consumes them (the cursor advances past them) without sending, so they cannot replay after
resume. Turn numbering in Langfuse shows a gap where the pause was; - stays private for its lifetime, including across
--resume— only an explicitcode-trace resumeorpurgeclears it; - is never age-pruned from the registry while suppressed.
New sessions always trace by default. Note that pausing cannot recall a turn whose send was already forked — pause early; purge is the remediation.
First contact never uploads history. The first time code-trace sees a session (no cursor in state — e.g. it was just installed mid-session, or an old session is resumed after install), it emits only the turn that fired the hook and fast-forwards past everything earlier. Pre-install content is never traced; the skip is recorded in the log.
Known limitation: TRACE_TO_LANGFUSE=false is an on/off switch, not a privacy control — while it is off no state is touched at all, so for a session that already has a cursor, turns completed during the off period will emit when tracing is re-enabled. Use pause for "don't trace this"; it is airtight.
Wired as a Claude Code SessionStart hook, code-trace --on-start records the session and prints one line — tracing ENABLED → <host> or tracing PAUSED for this session — which Claude Code injects as agent context. It prints nothing when tracing is not configured, and never emits.
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "code-trace --on-start" }] }
]
}
}code-trace purge --session <id> removes all three copies of a session's data:
- Langfuse traces — listed via
GET /api/public/traces?sessionId=and bulk-deleted (cascades to generations and spans); - local transcript — the Claude Code transcript JSONL, if recorded;
- code-trace state — the session's registry entry and cursor.
Flags: --langfuse-only / --local-only restrict the scope; --yes skips the confirmation prompt; --transcript-path <p> purges a session traced before it was in the registry.
Caveats: deleting the transcript removes the session from Claude Code's --resume history; purge cannot un-send anything outside code-trace's ownership (shell history, model-provider logs).
- Claude Code calls the hook after each assistant response, passing a JSON payload on stdin with the
sessionIdandtranscriptPath - The binary reads new lines from the transcript JSONL file (tracking offset in
~/.local/share/code-trace/state.json) - Messages are grouped into turns (user message + assistant responses + tool results)
- A batch of Langfuse ingestion events is built (
trace-create,generation-create,span-create) - The process forks — the parent exits immediately, while the child sends the batch to the Langfuse API via HTTP and logs the result
- The OpenCode plugin hooks into the
session.idleevent after each assistant response - It fetches new messages since the last processed message (tracked per-session in
~/.local/share/code-trace/opencode_cursor.json) - Messages are assembled into turns and piped to the
code-tracebinary over stdin - The binary forks — the parent returns immediately, while the child sends the batch to the Langfuse API via HTTP and logs the result
- The Pi extension hooks into the
agent_endevent after each user prompt completes - It reads new session entries since the last processed entry (tracked per-session in
~/.local/share/code-trace/pi_agent_cursor.json) - Entries are piped to the
code-tracebinary over stdin - The binary normalises Pi's session entry format into turns and forks — the parent returns immediately, while the child sends the batch to Langfuse
State is stored in ~/.local/share/code-trace/:
state.json—{ "cursors": ..., "sessions": ... }: turn cursor per session, plus a session registry (id, source, transcript path, suppressed flag, last seen) used by the privacy CLI. Older flat-shaped files are migrated in place with cursors preserved.state.lock— blocking exclusive lock serializing all state writes; since 0.3.1 concurrent invocations (parallel agents, pause/purge commands) queue instead of racing, so apausecan no longer be lost to a concurrently-running hookopencode_cursor.json— OpenCode per-session message cursorpi_agent_cursor.json— Pi per-session entry cursorcode_trace.log— trace log
Note: State was previously stored in ~/.claude/state/. On first run, existing state is migrated automatically.
Enable debug logging with CODE_TRACE_DEBUG=true.
On first run after an update, any existing state in ~/.claude/state/code_trace_state.json is migrated to ~/.local/share/code-trace/state.json.
Two tracks, split by what only each can prove:
- Track 2 — behaviour and concurrency (
cargo test): drives the binary directly with crafted payloads against an in-process fake Langfuse (tests/support/), with tmpdir-isolated environments. Covers pause/resume/purge semantics, the purge-vs-in-flight-send window, and concurrent-invocation scenarios (tests/concurrency_test.rs). Tests marked#[ignore = "red until fix-state-locking..."]demonstrate known state-locking defects and are un-ignored by that fix. - Track 1 — the real Claude Code seam (
harness/): runs the pinned realclaudeCLI in a container with code-trace wired as hooks, a stub model API, and the same fake Langfuse as a service. Proves hook wiring, real payload shapes, and config-file discovery. Seeharness/README.md.
CODE_TRACE_SYNC_SEND=1 (tests only) makes the Langfuse send inline instead of forked, so process exit implies delivery — used for exact "nothing was sent" assertions.
MIT