Export completed Codex CLI turns and completed Claude Code turns to Langfuse.
This is a small machine-level companion for people using Codex heavily across many repositories. Install it once on a Linux workstation and it watches Codex's local rollout files, then sends one clean Langfuse batch for each completed turn with the prompt, final answer, tool calls, command output, patch diffs, token usage, timing, and file-change metadata.
It is intentionally not a wrapper around codex. Codex runs normally; a systemd --user service exports completed observations and finalizes completed turns in the background.
- Tested with Codex CLI
0.146.0. - Claude Code support is implemented for explicit transcript export and Stop hook queueing.
- Built with Go
1.26.0. - Uses Codex rollout JSONL files under
~/.codex/sessions/. - Uses explicit Claude Code transcript JSONL paths supplied by
--provider claude --pathor Claude Code hook input. - Uses Langfuse OTLP ingestion at
/api/public/otel/v1/traces. - Supports Linux user services through
systemd --user. - Licensed under Apache-2.0.
Codex rollout JSONL and Claude Code transcript JSONL are not stable public APIs. Codex and Claude export are production-ready for workstation use after the local live checks in TESTING.md pass. Rerun CHECK-001 after Claude Code upgrades that change transcript shape.
Codex already records useful local session data. This exporter turns that data into Langfuse traces that are easier to review, search, and audit.
You can inspect:
- what prompt was given
- what final answer came back
- which shell commands ran
- command status, exit code, duration, and output
- patches and changed files
- MCP, web search, and deferred tool-search calls when Codex records them
- whether verification commands ran and whether any failed
- token usage when Codex records it
This is useful for reviewing LLM coding sessions, debugging failed agent work, sharing traces with a team, and keeping an operational record of important Codex turns.
git clone https://github.com/kirilligum/codex-langfuse-tracer.git
cd codex-langfuse-tracerThe exporter reads one Langfuse project key pair from ~/.codex/config.toml. Use one of these two setups.
Use this when Langfuse only listens on your workstation, for example http://localhost:3000. Langfuse still expects a project public key and secret key for ingestion, but they do not need to protect a hosted service. For a fresh local self-hosted instance, add fixed local-only keys to the Langfuse service environment before first startup:
LANGFUSE_INIT_ORG_ID=local
LANGFUSE_INIT_ORG_NAME=Local
LANGFUSE_INIT_PROJECT_ID=codex-local
LANGFUSE_INIT_PROJECT_NAME=Codex-Local
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-local-codex-tracer
LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-local-codex-tracerSet the same local-only values in ~/.codex/config.toml:
[mcp_servers.langfuse.env]
LANGFUSE_HOST = "http://localhost:3000"
LANGFUSE_PUBLIC_KEY = "pk-lf-local-codex-tracer"
LANGFUSE_SECRET_KEY = "sk-lf-local-codex-tracer"Do not use these fixed values for a shared, public, or remotely reachable Langfuse instance. If your local Langfuse project already exists, create or copy a project API key in the local Langfuse UI instead of relying on first-start initialization.
If the local Langfuse UI should be reachable from another device on a private network such as Tailscale, generate unique project keys instead of using the fixed local-only defaults above. Set Langfuse's NEXTAUTH_URL to the URL users open in the browser, for example http://100.x.y.z:3000. Leaving NEXTAUTH_URL at http://localhost:3000 can make sign-in redirect to the wrong machine.
For a private single-user local instance, create the initial UI user through Langfuse's init environment and disable future signups:
NEXTAUTH_URL=http://100.x.y.z:3000
LANGFUSE_INIT_USER_EMAIL=admin@local.langfuse
LANGFUSE_INIT_USER_NAME=Local Admin
LANGFUSE_INIT_USER_PASSWORD=<strong-local-password>
AUTH_DISABLE_SIGNUP=trueIf your Langfuse Docker Compose file does not pass AUTH_DISABLE_SIGNUP through to the langfuse-web container, add it to that service's environment or to a local Compose override. AUTH_DISABLE_SIGNUP=true prevents new accounts; it does not remove users that already exist.
Use this for Langfuse Cloud or any self-hosted Langfuse instance reachable by other machines. Create a real project API key pair in the target Langfuse project: open the project, go to Project Settings -> API Keys, create a new API key, and copy both the public key and secret key. Langfuse documents project API keys in project settings and self-hosted first-start key seeding through LANGFUSE_INIT_PROJECT_PUBLIC_KEY and LANGFUSE_INIT_PROJECT_SECRET_KEY.
For hosted self-hosted deployments that need deterministic first-start provisioning, generate unique keys instead of using the local-only defaults:
printf 'LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-%s\n' "$(openssl rand -hex 16)"
printf 'LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-%s\n' "$(openssl rand -hex 32)"Then add the hosted credentials to ~/.codex/config.toml:
[mcp_servers.langfuse.env]
LANGFUSE_HOST = "https://cloud.langfuse.com"
LANGFUSE_PUBLIC_KEY = "pk-lf-..."
LANGFUSE_SECRET_KEY = "sk-lf-..."Host examples:
- Langfuse Cloud US:
https://us.cloud.langfuse.com - Langfuse Cloud EU/default:
https://cloud.langfuse.com - Local self-hosted Langfuse:
http://localhost:3000
If your config already has a [mcp_servers.langfuse] block for Langfuse MCP, keep it. The exporter only reads the env values.
Workspace identity needs no additional configuration. The exporter always maps the repository and export-time branch to Langfuse Environment and the Linux runtime hostname to Langfuse User, as described in How It Works. Identity fields are not configurable.
Protect the config file if it contains hosted or shared-instance API keys:
chmod 600 ~/.codex/config.tomlOlder watcher state is intentionally incompatible and is not migrated. When upgrading a machine with an older state file, stop the existing watcher and perform this one-time destructive reset before installing:
systemctl --user stop codex-langfuse-watch.service
rm -- ~/.codex/langfuse-export-state.jsonRemoving the file discards processed IDs, queued requests, the scan watermark, and pending score retries. There is no compatibility state, backup path, or migration command.
./install.shThe installer builds the Go binary, syncs Langfuse model pricing from the configured project, installs the user service, reloads systemd, enables the service, and restarts it. It is the only required service-start step. Langfuse must already be reachable at LANGFUSE_HOST, and the project key pair in ~/.codex/config.toml must already authenticate to that Langfuse instance. If model pricing sync fails, the installer stops before installing the codex-langfuse-watch.service unit.
Useful preflight checks after setting equivalent shell variables:
curl -fsS "$LANGFUSE_HOST/api/public/health"
curl -fsS -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" "$LANGFUSE_HOST/api/public/models?page=1&limit=1" >/dev/nullInstalled files:
~/.codex/bin/codex-langfuse-exporter
~/.config/systemd/user/codex-langfuse-watch.service
~/.codex/langfuse-export-state.json
The version 3 state file records processed trace IDs, score retries, and queued hook requests so normal watcher runs do not resend successful observation batches. The installer starts the watcher, which creates fresh version 3 state when no state file exists; recently modified session files can then be exported again.
If you want the user service to run even when you are logged out, enable lingering for your Linux user:
loginctl enable-linger "$USER"Check the service:
systemctl --user status codex-langfuse-watch.serviceRun the built-in diagnostics:
~/.codex/bin/codex-langfuse-exporter --doctorRun a tiny Codex turn:
codex exec --model gpt-5.4-mini -c model_reasoning_effort=low --sandbox read-only --skip-git-repo-check "Reply exactly: langfuse-smoke-test"Open Langfuse, go to Tracing, and search for langfuse-smoke-test or codex.turn.transcript.
Expected trace shape:
- root observation:
codex.agent - main generation:
codex.transcript - tool calls:
codex.tool.*
Claude Code support can be checked with an explicit sanitized transcript path:
~/.codex/bin/codex-langfuse-exporter --provider claude --path <transcript.jsonl>Claude Code hooks can call the same binary in hook mode. The hook queues work only; the existing watch service drains the queued transcript and performs the Langfuse export.
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.codex/bin/codex-langfuse-exporter --claude-hook --quiet"
}
]
}
]
}
}- Linux with
systemd --user - Go
1.26.0or compatible - Codex CLI installed and writing rollout files under
~/.codex/sessions/ - A Langfuse project with public and secret API keys
- Network access from your workstation to the Langfuse host
This repo does not install Codex or Langfuse.
There is one automatic export path:
codex-langfuse-watch.serviceruns~/.codex/bin/codex-langfuse-exporter --watch.- The exporter polls
~/.codex/sessions/forrollout-*.jsonlevery five seconds by default. - The watcher considers only completed turns with non-empty canonical input and output from Codex
response_itemmessages. - One export sends
codex.agent,codex.transcript, and all child observations in a single batch, then creates deterministic trace-level scores. - Processed traces, score retries, and queue requests are saved in
~/.codex/langfuse-export-state.json.
Incomplete turns remain local until Codex records completion. It does not stream tokens or partial assistant text, and it does not export partial tool observations.
The version 3 state document uses processed_trace_ids, pending_scores[trace_id] for a successful span export awaiting scores, and queue. A successful span batch stores its resolved environment before score submission. A score failure retries scores with that environment and does not re-export the trace. Successful scores move the trace to processed_trace_ids and remove its pending entry.
Delivery is at-least-once to the currently configured Langfuse target. Known OTLP and score failures do not advance their checkpoints and retry on a later scan. A timeout after remote acceptance, or process termination between remote acceptance and local checkpoint persistence, can produce a duplicate on retry. The exporter does not query Langfuse to reconcile ambiguous acknowledgements and does not synchronize targets.
During catch-up after an outage, the watcher waits one configured poll interval between turn export attempts so the Langfuse ingestion and score queues receive bounded load.
The service is independent of the shell and Codex launch path. It covers codex, co, codex exec, and codex resume as long as Codex writes rollout files under ~/.codex/sessions/.
Install one watcher on every workstation and configure every watcher with the same shared Langfuse URL and project key pair. A stable URL lets the machine serving Langfuse change without rewriting producer configuration. Each workstation keeps its own watcher state and source transcripts; do not copy or merge watcher state files between machines.
The released exporter does not move Cloudflare tunnels, synchronize Langfuse databases, or reconcile traces between different Langfuse backends. Do not rsync live PostgreSQL, ClickHouse, object-store, or Docker volume data as a substitute. The accepted design is a single external gateway-promotion command plus deterministic trace-level reconciliation from each producer's local source corpus.
Gateway promotion and reconciliation are planned but are not supported commands in the current release. The current state, ownership boundaries, exact intended UX, implementation phases, release gates, and recovery limitations are recorded in the multi-machine tracing gateway handoff. In particular, an offline producer's unsent traces cannot be reconstructed until its local source files are available again.
Claude Code support uses one automatic trigger path:
- Claude Code sends a Stop hook payload containing
session_id,transcript_path,cwd, andhook_event_name. codex-langfuse-exporter --claude-hookvalidates the hook JSON and appends one queue request to~/.codex/langfuse-export-state.json.codex-langfuse-watch.servicedrains the queue, parses the explicit transcript path, exports completed Claude turns, and records processed trace IDs in the same state file.
There is no Claude directory discovery in this release. The exporter does not edit Claude settings; add the hook command yourself when you want automatic Claude exports.
Native Codex OTEL is intentionally not part of this setup. It emits low-level runtime spans such as streaming, socket, dispatch, and server internals that are not useful for reviewing prompts, answers, terminal output, tool calls, token usage, or file changes.
If your ~/.codex/config.toml has a native [otel] section and Langfuse shows noisy spans such as handle_responses, receiving, socket reader, or serve_inner, remove that section so this exporter is the single tracing path.
The exporter sends these observations when Codex records the data locally:
codex.agent: logical root observation with canonical input and output.codex.transcript: generation observation with the same canonical input and output, model name, and token usage.codex.message.commentary: assistant progress updates shown in the CLI.codex.reasoning.summary: visible reasoning summaries when Codex records a non-empty summary.codex.tool.command: shell command input and terminal output.codex.tool.file_change: patch input, changed files, and unified diffs.codex.tool.mcp: MCP invocation and result.codex.tool.web_search: web search query/action metadata.codex.tool.tool_search: deferred-tool discovery calls and results.
Tool observations use Langfuse's tool observation type. The transcript uses generation.
Claude Code support emits:
- trace name:
claude.turn.transcript claude.agent: logical root observation with canonical input and output.claude.transcript: generation observation with the same canonical input and output, model name, and token usage when Claude records it.claude.tool.command: Bash tool input, output, status, and command metadata.claude.tool.file_change: file-writing tool metadata when Claude records structured path fields.claude.tool.mcp: MCP invocation and result when Claude records structured MCP tool names.claude.tool.generic: bounded metadata and redacted input/output for other Claude tools.
Claude thinking blocks are omitted, including redacted or encrypted thinking-like blocks. Visible assistant text, final answers, tool input, tool output, and metadata strings use the shared redaction and truncation path.
<provider>.tool.file_change metadata includes:
changed_filesadded_filesmodified_filesdeleted_filesmoved_filesfile_change_typeschanged_file_count
The root trace carries compact provider insight metadata for table scanning. Codex writes codex_insight; Claude writes claude_insight.
tool_countcommand_countfailed_command_countfile_change_tool_countchanged_file_countchanged_extensionstouched_test_filesverification_command_countverification_statuslast_verification_commandlast_verification_statusoutcomeoutcomesnavigation<kind>_command_countfor each command kind<tool_family>_tool_countfor each tool family
verification_status is one of not_applicable, not_run, passed, or failed. Full changed_files stays on <provider>.tool.file_change; root metadata only carries compact file-impact summaries.
outcome and outcomes are deterministic, code-derived labels such as no_changes, changed_files, docs_only, tests_touched, verification_not_run, verification_passed, verification_failed, failed_command, and install_or_network_used. They do not use LLM judgment and do not claim the task was semantically solved.
Workspace metadata includes the exact cwd and, when cwd is inside an attached Git worktree, the export-time branch as git_branch. The exporter maps the worktree-root folder and export-time branch to langfuse.environment in the form repository-folder--branch-<hash>. Repository and branch text is normalized to lowercase Langfuse-safe characters, the value is capped at 40 characters, and every Git environment ends with the first six lowercase hexadecimal SHA-256 characters of the raw repository folder, a NUL separator, and raw branch. A detached HEAD uses detached; its git_branch metadata remains omitted. Non-Git, missing, unreadable, or timed-out working directories use default.
Every exported span sets langfuse.user.id to the trimmed, non-empty Linux runtime hostname captured once when the export process starts. This is machine identity, not a human account or workspace path. The same persisted Environment is used for the span export and deterministic scores even if the checked-out branch changes before a score retry.
Navigation metadata is always-on. A read-only trace means navigation contains files:read_only, which only means no observed local file changes in the exported turn. It does not mean no network activity, no install command, or no external API call. Counts remain the metric representation. <provider>_insight.navigation, for example codex_insight.navigation or claude_insight.navigation, is the canonical low-cardinality navigation field that trace tags project into Langfuse's tag UI.
Cost tracking uses Langfuse's model and usage handling. The exporter sends langfuse.observation.model.name and langfuse.observation.usage_details on provider transcript observations; it does not multiply tokens locally or emit cost_details. Configure pricing in Langfuse model definitions so Langfuse calculates input, output, total, and cost columns from the canonical model and usage values.
Langfuse calculates cost. The built-in pricing sync creates source-backed model definitions for supported Codex/OpenAI models and current Claude models: Opus 4.7, Sonnet 4.6, and Haiku 4.5. Claude Code subscription billing is separate from Anthropic API token pricing; these definitions are for Langfuse trace cost columns when Claude records compatible model and usage details.
install.sh runs ~/.codex/bin/codex-langfuse-exporter --sync-model-pricing --quiet before restarting codex-langfuse-watch.service. The same setup can be run directly:
~/.codex/bin/codex-langfuse-exporter --sync-model-pricingThe built-in pricing catalog is source-dated from https://openai.com/api/pricing/ on 2026-05-02 and covers gpt-5.5, gpt-5.4, and gpt-5.4-mini. It also covers gpt-5.3-codex-spark using the official gpt-5.3-codex pricing from https://developers.openai.com/api/docs/models/gpt-5.3-codex on 2026-05-02. It covers claude-opus-4-7, claude-sonnet-4-6, and claude-haiku-4-5-20251001 using Claude Opus 4.7, Claude Sonnet 4.6, and Claude Haiku 4.5 pricing from https://platform.claude.com/docs/en/about-claude/pricing on 2026-05-05. OpenAI/Codex usage keys are input, input_cached_tokens, output, output_reasoning_tokens, and total; Claude usage keys are input, cache_creation_input_tokens, cache_read_input_tokens, output, and total. Child token buckets are subtracted from parent input/output buckets to avoid double counting. For Claude Code traces, a small input count can be correct when most prompt context is reported in cache_creation_input_tokens and cache_read_input_tokens.
When provider pricing changes or a supported coding agent emits a new model name, update internal/langfuse/models.go and its catalog tests in the same change. Do not add fallback local cost multiplication.
Langfuse calculates cost during ingestion. Existing rows are not backfilled automatically; use an explicit re-export for old sessions after model pricing is synced:
~/.codex/bin/codex-langfuse-exporter --session-id <session-id> --no-verify<provider>.tool.command metadata includes:
command_kindstatusexit_codeduration_msfailure_type
command_kind uses a fixed enum: test, build, lint, format, git, read, search, install, systemd, network, or other.
<provider>.tool.mcp metadata includes:
mcp_servermcp_tool
MCP metadata is derived only from observed structured MCP events. Configured but unused MCP servers are not exported as usage. Exact MCP tools such as issues/list stay in observation metadata and are not trace tags.
Trace tags are emitted through langfuse.trace.tags. The built-in tag contract is the active provider's insight navigation values plus observed mcp:<server> values. That means every navigation value, including files:changed, command:other, tool:command, tool:file_change, tool:mcp, and verification:not_run, is available as a trace tag, and an observed MCP server such as GitHub also adds mcp:github. Tags are sorted, unique, lowercase, and never contain exact MCP tool names, prompts, outputs, cwd, file paths, session IDs, or trace IDs.
Source-level custom tags can be added as compiled Go rules in internal/agenttrace/tagrules.go; see internal/agenttrace/TAG_RULES.md for examples such as emitting a fixed tag when the user's request contains a known string. Custom tags are code changes, not runtime configuration.
The exporter also creates deterministic trace-level Langfuse scores after each successful trace export:
verification_runverification_passedhad_failed_commandhad_file_changeschanged_testsdocs_onlychanged_file_countoutcome
These scores are idempotent on re-export and use only parsed trace metadata. They do not make extra LLM calls. The eight score events are submitted in one Langfuse ingestion batch; trace export continues to use only the OTLP trace endpoint.
Use trace tags for turn-level navigation and observation filters for individual tool calls.
Trace tags are the primary reusable trace filters:
files:changedfiles:read_onlycommand:<kind>for each observedcommand_kindtool:<family>for each observed tool familyverification:<status>mcp:<server>for each observed MCP server
Common tag filters include command:search, command:read, command:network, command:install, tool:web_search, and verification:failed.
Outcome filters include outcome:no_changes, outcome:changed_files, outcome:docs_only, outcome:verification_not_run, outcome:verification_passed, outcome:verification_failed, outcome:failed_command, and outcome:install_or_network_used.
Use mcp_server and mcp_tool on <provider>.tool.mcp observations when you need exact MCP call details.
Observation filters use observation metadata:
Observations: command search:Name equals codex.tool.commandandMetadata command_kind equals searchObservations: command read:Name equals codex.tool.commandandMetadata command_kind equals readObservations: command network:Name equals codex.tool.commandandMetadata command_kind equals networkObservations: command install:Name equals codex.tool.commandandMetadata command_kind equals installObservations: failed commands:Name equals codex.tool.commandandMetadata failure_type equals nonzero_exitObservations: file changes:Name equals codex.tool.file_changeObservations: web search:Name equals codex.tool.web_search
After install.sh restarts codex-langfuse-watch.service, future watcher exports include these tags and MCP metadata automatically. Existing Langfuse rows are not automatically backfilled; use an explicit re-export command when old rows need the new fields.
The watcher is the normal production path. Manual export is for explicit backfill or debugging.
Export the latest local Codex session:
~/.codex/bin/codex-langfuse-exporter --latestExport a known Codex session:
~/.codex/bin/codex-langfuse-exporter --session-id <SESSION_ID>Export a specific rollout file:
~/.codex/bin/codex-langfuse-exporter --path ~/.codex/sessions/YYYY/MM/DD/rollout-....jsonlExplicit re-export for backfill uses the same command shape:
~/.codex/bin/codex-langfuse-exporter --path <rollout.jsonl> --no-verifySkip post-export verification:
~/.codex/bin/codex-langfuse-exporter --latest --no-verifyManual exports print a trace URL when the Langfuse project can be resolved:
exported trace=<trace-id> status=200
verified trace=<trace-id> input=true output=true
trace_url=http://localhost:3000/project/<project-id>/traces/<trace-id>
Use JSON output when another Codex turn, script, or CI job needs to consume the result:
~/.codex/bin/codex-langfuse-exporter --latest --jsonThe command emits one JSON object per exported turn:
{"provider":"codex","session_file":"...","turn_id":"...","trace_id":"...","trace_url":"...","status":200,"verified_input":true,"verified_output":true}Codex can use trace_url in final summaries, issue comments, or handoff notes without scraping human-readable logs.
Run the doctor command when exports do not appear in Langfuse or before sharing a demo:
~/.codex/bin/codex-langfuse-exporter --doctorIt checks:
- config loading and configured host
- Langfuse health endpoint
- Langfuse API authentication
- project id lookup for trace URLs
- watch state queue length
codex-langfuse-watch.serviceactivity- recent watcher export errors
- loopback host binding problems such as
localhost:3000refusing connections
Machine-readable output is available for scripts:
~/.codex/bin/codex-langfuse-exporter --doctor --jsonThis exporter can send prompt text, assistant text, tool inputs, command output, and diffs to Langfuse. Do not enable it where Codex sessions may contain secrets, customer data, tax data, banking data, card data, private legal data, or other sensitive material unless exporting that data is intentionally approved.
The exporter redacts several common token/key patterns, but redaction is a last line of defense, not a security boundary.
Do not pass API keys directly in process command-line arguments. Process-list commands can expose those arguments, and their output can then appear in a trace. Prefer private environment files, credential files, or the owning tool's secret store.
The exporter does not emit:
- hidden chain-of-thought or encrypted reasoning content
- native Codex runtime spans
- byte-for-byte TUI recordings
- per-file observation fanout
- inferred "model context" observations
- local cost calculations or
cost_details
Input and output are stored on the root and generation observations using Langfuse v4 observation fields: langfuse.observation.input and langfuse.observation.output. Empty observation input/output fields are omitted. Deprecated trace-level input/output fields are not emitted.
Verification reads Langfuse's /api/public/v2/observations endpoint, selects the single logical root observation, and compares its raw input/output values with the sanitized turn. It does not read deprecated trace objects or v1 observation rows.
Check the service:
systemctl --user status codex-langfuse-watch.serviceCheck service logs:
journalctl --user -u codex-langfuse-watch.service -n 100 --no-pagerConfirm the exporter binary exists:
test -x ~/.codex/bin/codex-langfuse-exporterFind a local session file for a prompt:
rg -l "some prompt text" ~/.codex/sessionsCommon failure modes:
- Missing Langfuse credentials in
~/.codex/config.toml. - Wrong Langfuse host for the project keys.
- Codex reports that the
langfuseMCP server closed duringinitialize:langfuse-mcp==0.10.0still uses the MCP Python SDK v1 API, so launch it with the testedmcp>=1.28,<2constraint shown inexamples/codex-config.toml. An unconstraineduvx langfuse-mcpcan resolve the incompatible MCP SDK v2. ./install.shfails withconnect: connection refused: Langfuse is not running atLANGFUSE_HOST, or the host URL points at the wrong machine or port../install.shfails withLangfuse model list /api/public/models failed with HTTP 401: the configured public/secret key pair is not valid for the Langfuse instance atLANGFUSE_HOST. Seed the sameLANGFUSE_INIT_PROJECT_PUBLIC_KEYandLANGFUSE_INIT_PROJECT_SECRET_KEYbefore first startup, or create/copy a project key pair from the Langfuse UI and update~/.codex/config.toml.systemctl --user status codex-langfuse-watch.servicesays the unit is not found after./install.sh: the installer likely failed before the service install step. Fix the Langfuse reachability or authentication error and rerun./install.sh.- Browser sign-in redirects to
localhost: the Langfuse server'sNEXTAUTH_URLis still set tohttp://localhost:3000. Set it to the actual browser URL, such as a Tailscale URL, and recreate thelangfuse-webcontainer. - A browser on Windows cannot reach a Tailscale IP that works from WSL: Tailscale may be running only inside WSL. Run Tailscale on the Windows host too, or open the browser inside the same WSL network environment.
- Native Codex OTEL still enabled, causing noisy duplicate traces.
- Claude Code transcript exists but no trace appears because the
Stophook is not installed in Claude settings. Use~/.codex/bin/codex-langfuse-exporter --provider claude --path <transcript.jsonl>for explicit backfill, or add the documented--claude-hook --quietcommand to Claude'sStophook for future automatic exports. - Watch state already marked a historical turn as processed.
- Langfuse ingestion delay. Wait a few seconds and refresh the UI.
- Empty Input/Output on unrelated observations. Select
codex.transcript.
ccgram is not required by this exporter. If ccgram is installed separately and its bot uses the __main__ window in a tmux session named ccgram, it can remain stopped to conserve workstation memory and be started only when needed.
Start the bot in its existing control window:
tmux send-keys -t ccgram:@0 'ccgram run' EnterStop the bot without closing the tmux session or its other windows:
tmux send-keys -t ccgram:@0 C-cUse tmux list-windows -t ccgram first if the control window might not be @0.
Run the local suite:
go test ./... -count=1Run focused parser, contract, watcher, exporter, and fuzz checks from TESTING.md.
The repo uses a manifest-driven contract corpus:
testdata/manifest.json
testdata/sources/<provider>/*.jsonl
testdata/golden/*.normalized.json
Keep that as the single behavioral contract. Do not add a second fixture registry.
This exporter is provider-neutral after parsing. Codex, Claude Code, and future coding agents such as Gemini CLI, OpenCode, Goose, or another local agent must converge on the same internal contract:
source transcript/log -> internal/<provider>trace -> agenttrace.Turn -> tracecontract.Trace -> langfuse.EmitSpans
Add a new coding agent by extending the existing path:
- Add a parser package named
internal/<provider>tracethat reads that agent's local transcript or log format and returns[]agenttrace.Turn. - Keep provider-specific logic in that parser only. Shared redaction, token usage, trace IDs, insight rollups, trace tags, and Langfuse projection stay in
internal/agenttrace,internal/tracecontract, andinternal/langfuse. - Add the provider constant in
internal/agenttrace/model.goand profile names ininternal/agenttrace/profile.go, including trace name, observation names, metadata prefix, and insight metadata key. - Register the parser once in
internal/providers/providers.go.cmd/codex-langfuse-exporter,internal/watch, and contract tests must call the provider registry instead of importing the provider parser directly. - Add sanitized fixtures under
testdata/sources/<provider>/*.jsonl, add entries totestdata/manifest.json, and add normalized expectations undertestdata/golden. - Run
go test ./test -run TestGoldenTraceContract -count=1so the new provider is held to the same normalized Langfuse contract as Codex and Claude. - If the provider has a stable completion hook, add a small hook package that validates the hook payload and enqueues
exportstate.QueueRequest. The hook process must not load Langfuse config or export directly.--watchremains the only automatic exporter. - If the provider does not have a stable hook or session discovery contract, support explicit path export first:
codex-langfuse-exporter --provider <provider> --path <transcript>.
Do not add provider wrapper execution, a second fixture manifest, a second Langfuse projection, direct hook export, transcript directory polling, compatibility shims, or placeholder providers without real fixtures. New agents should make the shared tests broader, not create parallel Codex-shaped and provider-shaped test suites.
./uninstall.shIf Langfuse MCP was added only for this setup, remove the optional [mcp_servers.langfuse] block from ~/.codex/config.toml.
Codex Langfuse Tracer is a small Go exporter that watches local Codex CLI rollout files and turns completed coding-agent turns into clean Langfuse traces with canonical observation input/output, tool activity, token usage, and verification metadata. Install once per workstation; Codex keeps running normally.
