Langfuse Tracing Plugin for OpenAI Codex

September 4, 2026 · View on GitHub

A Codex plugin that traces agent turns, model calls, tool executions, token usage, and subagent threads to Langfuse.

Once enabled, every Codex turn shows up in Langfuse as a trace you can inspect, debug, evaluate, and monitor for cost — turning Codex from a black box into an observable agent.

What gets traced

After each Codex turn, the plugin reads the session's rollout transcript and uploads it to Langfuse as a trace. The structure mirrors how Codex actually works:

  • Turn (Codex Turn, an agent observation) — one trace per turn, from your prompt to the final answer.
  • Generations — one per model response within the turn, named LLM (or LLM Subagent inside subagent threads), with the model recorded on the observation plus reasoning, assistant text, the tool calls it requested, and token usage.
  • Tool calls — shell commands, apply_patch, spawn_agent, MCP tools, web searches, etc., each with its input, output, and error status. MCP calls are named server.tool, and failed commands are flagged as errors.
  • Subagents — subagent threads are resolved from their own rollout files and nested under the spawning turn as Codex Subagent Turn.
  • Sessions — all turns from one Codex session are grouped via the Codex thread id, so you can replay the whole session in Langfuse's Sessions view.

Interrupted turns (where you cancel mid-response) are still uploaded and flagged as interrupted.

Prerequisites

  • Node.js >= 22
  • The npm CLI on your PATH. Codex installs this plugin from the npm registry and shells out to npm pack to fetch it.
  • Codex >= 0.128
  • A Langfuse Cloud account (or a self-hosted instance) and API keys

Installation

1. Add the plugin marketplace

codex plugin marketplace add langfuse/codex-observability-plugin

2. Enable the plugin

Enable hooks and the tracing plugin globally in ~/.codex/config.toml, or only for a specific project in <project>/.codex/config.toml. Use the current hooks feature key; plugin_hooks was removed in Codex 0.146.

[features]
hooks = true

[plugins."tracing@codex-observability-plugin"]
enabled = true

3. Review and trust the hook

Start Codex after installing and enabling the plugin. When Hooks need review appears, review the Langfuse Stop hook and trust it before expecting traces. In the Codex CLI, you can also run /hooks to inspect, review, and trust the hook.

Codex records trust against the current hook hash, so plugin updates can change the hash and require another review.

4. Set your Langfuse credentials

Tracing stays off until TRACE_TO_LANGFUSE is true, so you opt in explicitly.

Option 1: Shell environment (recommended)

Add to your ~/.zshrc, ~/.bashrc, or ~/.bash_profile:

export TRACE_TO_LANGFUSE="true"
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU (default)

Option 2: JSON config file

Create ~/.codex/langfuse.json (global) or <project>/.codex/langfuse.json (per-project):

{
  "enabled": true,
  "public_key": "pk-lf-...",
  "secret_key": "sk-lf-...",
  "base_url": "https://cloud.langfuse.com"
}

Config is resolved as defaults → ~/.codex/langfuse.json<project>/.codex/langfuse.json → environment variables (environment wins). LANGFUSE_CODEX_* variables take precedence over the matching standard LANGFUSE_* variables, so you can scope credentials to Codex without disturbing other Langfuse tooling.

5. Get your Langfuse API keys

  1. Go to cloud.langfuse.com (or your self-hosted instance).
  2. Create a project (or open an existing one).
  3. Go to Settings → API Keys → Create new API keys.
  4. Copy the public key (pk-lf-...) and secret key (sk-lf-...).

Run a Codex turn, then open your Langfuse project to see the trace.

Updating

Codex refreshes configured marketplaces in the background when a session starts, so updates usually arrive on their own with the next restart. To pull one immediately:

codex plugin marketplace upgrade codex-observability-plugin
codex plugin list

codex plugin list shows the installed version. If a release changes the Stop hook command, Codex shows Hooks need review and the hook has to be trusted again in /hooks before traces resume.

Environment variables

VariableRequiredDefaultDescription
TRACE_TO_LANGFUSEYesfalseSet to "true" to enable tracing
LANGFUSE_PUBLIC_KEY / LANGFUSE_CODEX_PUBLIC_KEYYesLangfuse public key (pk-lf-...)
LANGFUSE_SECRET_KEY / LANGFUSE_CODEX_SECRET_KEYYesLangfuse secret key (sk-lf-...)
LANGFUSE_BASE_URL / LANGFUSE_CODEX_BASE_URLNohttps://cloud.langfuse.comLangfuse host / data region
LANGFUSE_TRACING_ENVIRONMENT / LANGFUSE_CODEX_ENVIRONMENTNoEnvironment label for the traces (e.g. production)
LANGFUSE_CODEX_USER_IDNoCodex auth email, if foundAttach a user id to all traces
LANGFUSE_CODEX_TAGSNoTags for all traces (JSON array or comma-separated)
LANGFUSE_CODEX_METADATANoJSON object of metadata to attach to all traces
LANGFUSE_CODEX_TRACE_SEEDNoDerive deterministic trace ids (details)
LANGFUSE_CODEX_MAX_CHARSNo20000Truncate inputs/outputs longer than this many characters
LANGFUSE_CODEX_DEBUGNofalseSet to "true" for verbose logging to stderr
LANGFUSE_CODEX_FAIL_ON_ERRORNofalseSet to "true" to make hook upload errors fail the hook

Data regions

RegionLANGFUSE_BASE_URL
🇪🇺 EUhttps://cloud.langfuse.com
🇺🇸 UShttps://us.cloud.langfuse.com
🇯🇵 Japanhttps://jp.cloud.langfuse.com
⚕️ HIPAAhttps://hipaa.cloud.langfuse.com

Deterministic trace ids

By default, trace ids are auto-generated, and an external system (a CI harness, benchmark runner, or dataset-experiment service) that runs codex exec headlessly has to poll the Langfuse API to discover the trace a run produced. Set LANGFUSE_CODEX_TRACE_SEED (or trace_seed in langfuse.json) to make trace ids predictable instead:

  • Turn N of the main thread (1-based, in rollout order) gets the trace id hex(sha256("<seed>:<N>")).slice(0, 32).
  • Turn N of a subagent thread gets hex(sha256("<seed>:<childThreadId>:<N>")).slice(0, 32), scoped by the subagent's thread id so it cannot collide with main-thread ids. (Subagent turns spawned within a main-thread turn are nested inside that turn's trace as usual and don't get their own trace id.)

The main-thread formula deliberately excludes the Codex thread id, so you can compute the trace id before the run starts — no thread id, no polling. The derivation matches the Langfuse SDKs' createTraceId(seed) helper and always yields a valid W3C trace id.

Use a unique seed per session (e.g. a UUID or your job/run id). Reusing a seed across sessions produces colliding trace ids, and the second upload would merge into (and overwrite parts of) the first trace.

If derivation ever fails, the hook falls back to auto-generated ids and still uploads — it never blocks the session (set LANGFUSE_CODEX_FAIL_ON_ERROR=true while testing to surface such errors).

A harness can compute the trace id up front and register it with a dataset run — without ever fetching traces:

SEED="$(uuidgen)" # unique per codex exec invocation

# Trace id of the first main-thread turn: hex(sha256("<seed>:1")).slice(0, 32)
TRACE_ID=$(printf '%s:1' "$SEED" | shasum -a 256 | cut -c1-32)

# Link the precomputed trace id to a dataset run item before (or after) the run.
curl -s -X POST "$LANGFUSE_BASE_URL/api/public/dataset-run-items" \
  -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"runName\": \"codex-benchmark-2026-07-13\",
    \"datasetItemId\": \"$DATASET_ITEM_ID\",
    \"traceId\": \"$TRACE_ID\"
  }"

# Run Codex; the Stop hook uploads the turn with exactly $TRACE_ID.
LANGFUSE_CODEX_TRACE_SEED="$SEED" codex exec "your prompt"

The same works from JavaScript with the Langfuse SDK: await createTraceId(${seed}:1) (from @langfuse/tracing) returns the identical id.

JSON config reference

Config keyEnvironment variableDefaultDescription
enabledTRACE_TO_LANGFUSEfalseEnable tracing
public_keyLANGFUSE_PUBLIC_KEY / LANGFUSE_CODEX_PUBLIC_KEYLangfuse public key
secret_keyLANGFUSE_SECRET_KEY / LANGFUSE_CODEX_SECRET_KEYLangfuse secret key
base_urlLANGFUSE_BASE_URL / LANGFUSE_CODEX_BASE_URLhttps://cloud.langfuse.comLangfuse host
environmentLANGFUSE_TRACING_ENVIRONMENT / LANGFUSE_CODEX_ENVIRONMENTEnvironment label
user_idLANGFUSE_CODEX_USER_IDCodex auth email, if foundUser id for all traces
tagsLANGFUSE_CODEX_TAGSTags for all traces
metadataLANGFUSE_CODEX_METADATAMetadata object for all traces
trace_seedLANGFUSE_CODEX_TRACE_SEEDDeterministic trace-id seed
max_charsLANGFUSE_CODEX_MAX_CHARS20000Input/output truncation threshold
debugLANGFUSE_CODEX_DEBUGfalseVerbose logging
fail_on_errorLANGFUSE_CODEX_FAIL_ON_ERRORfalseFail the hook on upload errors

Troubleshooting

  • No traces appear — confirm [features] hooks = true, the plugin is enabled in config.toml, and TRACE_TO_LANGFUSE=true is visible to the Codex process. Run codex features list to verify hooks are enabled and codex plugin list to verify tracing@codex-observability-plugin is installed and enabled. Run with LANGFUSE_CODEX_DEBUG=true to log to stderr.
  • Hook is installed but does not runcodex plugin list showing the plugin as installed and enabled does not mean the hook is trusted. Open /hooks, review the Langfuse Stop hook, and trust the current hook hash. A successful test should show hook: Stop followed by hook: Stop Completed.
  • Authentication fails — check that the public/secret keys are valid and that LANGFUSE_BASE_URL matches the region the keys belong to.
  • Traces land in the wrong project — API keys are project-scoped in Langfuse; use the keys for the project you want.
  • Testing hook failures — set LANGFUSE_CODEX_FAIL_ON_ERROR=true together with LANGFUSE_CODEX_DEBUG=true to make Codex report upload or flush errors instead of failing open.
  • Checking dedup sidecars — successful uploads of completed turns are recorded next to the rollout as <rollout>.jsonl.langfuse. If a Stop hook reads the rollout before Codex has written the turn-completed marker, the trace may upload without a sidecar entry; the next Stop hook will finalize and mark it.
  • Verifying in Langfuse — use npx langfuse-cli api traces list --from-timestamp <recent ISO> --limit 10 --order-by timestamp.desc --fields core,metrics,observations --json with credentials for the same project.
  • Sandboxed/network-restricted runs — Codex sandbox or network policy can prevent exports from reaching Langfuse. Debug logging and fail-on-error mode are the quickest way to distinguish hook execution from network failure.
  • Self-hosting — the TypeScript SDK requires Langfuse platform version >= 3.95.0.

Data sent to Langfuse

When enabled, the plugin uploads completed Codex transcript data to Langfuse: prompts, assistant messages, reasoning summaries, tool-call inputs and outputs, model metadata, and token usage. Do not enable tracing for sessions containing data you do not want stored in Langfuse. Use LANGFUSE_CODEX_MAX_CHARS to cap how much of large inputs/outputs is captured.

How it works

Codex emits a Stop hook after each turn, passing the path to the session's rollout transcript on stdin. The plugin:

  1. Reads the rollout JSONL and reconstructs each turn (model steps, tool calls, usage, subagents).
  2. Converts them into Langfuse observations with the original timestamps, using the Langfuse TypeScript SDK on top of OpenTelemetry.
  3. Records uploaded turn ids in a sidecar file (<rollout>.langfuse) so resuming a session does not re-upload completed turns.

The hook fails open: any tracing error is logged and swallowed so it never blocks your Codex session.

Development

pnpm install
pnpm test        # build, then run the test suite
pnpm run lint    # prettier + tsc + build
pnpm run build   # bundle the hook to plugins/tracing/dist/index.mjs

The hook ships as a single self-contained plugins/tracing/dist/index.mjs, because Codex runs the plugin without an install step and never installs its dependencies. The bundle is a build output and is not committed: prepack builds it when the npm package is published, so it travels in the tarball instead of in Git. pnpm test builds first, since the hook-command test executes the bundled hook.

Releasing

Releases go through the tag-triggered workflow, never through a manual npm publish. Bump the version in both plugins/tracing/package.json and plugins/tracing/.codex-plugin/plugin.json, then push a matching tag:

git tag v0.4.0 && git push origin v0.4.0

The workflow refuses a tag whose name disagrees with either version, then lints, tests, and stages the package on npm with provenance. A maintainer approves the staged publish with 2FA (npm stage approve <id>), and the draft GitHub release still has to be published. Finally, point .agents/plugins/marketplace.json at the new version, because that pin is what users install.

The two versions matter for different things: the one in package.json is the npm version, and the one in plugin.json decides the cache directory Codex installs into (~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/) and therefore whether Codex refreshes an existing install at all. Publishing them out of step ships a package that reports the wrong version.

License

MIT