Skip to content

pdhoolia/langfuse-codex-tracing

v0.2.0MIT

Trace Codex conversations, tools, subagents, and lifecycle events into Langfuse.

Langfuse Codex Plugin

Trace opted-in Codex conversations, tool calls, subagents, lifecycle events, and feedback into Langfuse. Requires Node.js 20 or newer.

Version 0.2.0 adapts the fixes in ../langfuse-claude-code-plugin through commit 958f8af to Codex's hook and rollout formats. It adds conversation history, cache-aware usage, safe state writes, interrupted-turn recovery, and current Codex lifecycle coverage.

What it traces

  • One Langfuse trace per user turn, with a generation summarizing the assistant response and child tool spans.
  • Earlier turns as ChatML generation input, including assistant tool calls and tool results. Trace input stays focused on the current prompt.
  • Shell, MCP, local function, and custom tool calls such as apply_patch.
  • Permission requests, compaction timing, and subagent observations with generations from their available transcripts.
  • Interrupted turns, recovered on Interrupt, SessionEnd, or the next prompt.
  • Turn feedback (turn_feedback) and session feedback (session_feedback).

The hook configuration covers SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, Stop, Interrupt, SessionEnd, PreCompact, PostCompact, SubagentStart, and SubagentStop. Session startup includes resume, fork, and compact sources.

The event list and input fields follow the official Codex hook reference. Claude's StopFailure is not a documented Codex event, so it is not registered. Codex's compaction payload does not promise a summary; compaction spans record timing and trigger.

Configuration

Export these variables in the environment that launches Codex:

export CODEX_LANGFUSE_PUBLIC_KEY=pk-lf-...
export CODEX_LANGFUSE_SECRET_KEY=sk-lf-...
export CODEX_LANGFUSE_BASE_URL=https://cloud.langfuse.com
export TRACE_TO_LANGFUSE=true

Tracing is opt-in by default. Set CODEX_LANGFUSE_TRACE_DEFAULT=all for a global default, then use TRACE_TO_LANGFUSE=false to opt out for an invocation or project environment. An explicit TRACE_TO_LANGFUSE value wins over the global default. This plugin reads environment variables; it does not load .env files or Claude project settings.

VariablePurpose
TRACE_TO_LANGFUSEExplicit tracing opt-in or opt-out.
CODEX_LANGFUSE_TRACE_DEFAULTall traces by default. Also accepts true and 1.
CODEX_LANGFUSE_PUBLIC_KEYLangfuse public key.
CODEX_LANGFUSE_SECRET_KEYLangfuse secret key.
CODEX_LANGFUSE_BASE_URLDefaults to https://cloud.langfuse.com.
CODEX_LANGFUSE_DEBUGEnable debug logging with true.
CODEX_LANGFUSE_MAX_CHARSPer-string truncation limit; defaults to 50000.
CODEX_LANGFUSE_STATE_FILEDefaults to $CODEX_HOME/state/langfuse_state.json.
CODEX_LANGFUSE_LOG_FILEDefaults to $CODEX_HOME/state/langfuse_hook.log.

CODEX_HOME defaults to ~/.codex. Credentials also accept CC_LANGFUSE_* and then LANGFUSE_* fallbacks. The default policy, debug setting, and truncation limit accept their CC_LANGFUSE_* counterparts. The legacy STATE_FILE override remains supported.

Local Langfuse for development

Langfuse distributes official Docker images and a Docker Compose stack. This checkout includes dev/langfuse/compose.yaml, adapted from the upstream stack, using the official Langfuse web and worker v4 images plus PostgreSQL 17, ClickHouse 25.12, Redis 7, and MinIO. Docker downloads the images on first startup; image binaries are not stored in this repository.

The stack sets LANGFUSE_MIGRATION_V4_WRITE_MODE=dual on both web and worker as a compatibility bridge for this plugin's JS SDK v3. Langfuse v4's default events_only mode rejects its trace and generation events. Keep dual enabled until the plugin migrates to a v4-compatible SDK or OTLP ingestion. Dual mode stores legacy data and propagates observations into the v4 tables, using additional storage. Older SDK data can take about 15 minutes to appear in the v4 observations view; the legacy trace API can show it sooner.

With Docker running and Docker Compose v2 or newer installed (Docker Desktop includes both):

npm run langfuse:up
source dev/langfuse/plugin.env
codex

The helper generates random credentials once, starts all six services in the background, and waits up to five minutes for health checks after image downloads. Open localhost:3000 and sign in as dev@localhost.local using LANGFUSE_INIT_USER_PASSWORD from dev/langfuse/.env. Langfuse's headless initialization creates the Codex Development organization, Codex Plugin Development project, user, and API keys automatically.

plugin.env exports the matching CODEX_LANGFUSE_PUBLIC_KEY, CODEX_LANGFUSE_SECRET_KEY, local base URL, and TRACE_TO_LANGFUSE=true. Source it in the shell that launches Codex; the plugin does not load it automatically. Install and enable the plugin using the instructions below. For a desktop launch, provide these variables to the process that launches the app and restart it if needed.

npm run langfuse:status
npm run langfuse:logs        # Ctrl+C exits the log stream
npm run langfuse:down        # Stops containers; preserves traces and credentials
node scripts/langfuse-local.mjs pull  # Explicitly update images, then run up again

The helper works from any working directory when invoked by its absolute path. Both the Compose file and helper are also included in the built plugin. Generated .env and plugin.env files have owner-only permissions, are Git-ignored, and are excluded from the packaged plugin. Keep .env alongside its Docker volumes: changing initialization keys/passwords does not rotate an existing account or project's credentials, and changing infrastructure secrets can break access to existing data.

Only the UI/API (127.0.0.1:3000) and MinIO media endpoint (127.0.0.1:9090) are published. Databases and the worker stay on the Docker network. Data persists in named Docker volumes under the langfuse-codex-dev Compose project. This setup is intended for local development.

To change ports before startup, run node scripts/langfuse-local.mjs init, edit LANGFUSE_PORT or MINIO_PORT in dev/langfuse/.env, then run npm run langfuse:up and source plugin.env again. Keep this generated file as plain KEY=value entries without quotes, spaces, or interpolation. Saved values take precedence over similarly named shell variables, including cloud API credentials. The Compose file pins major/minor image lines rather than immutable digests; pull deliberately fetches newer images on those lines. First startup needs internet access, several GB of disk space, and enough Docker memory for the databases. If startup fails, inspect langfuse:logs and Docker's available resources.

Build and validate

npm install
npm run build
npm test
npm run typecheck

The build generates self-contained Node bundles in bundle/ and a complete plugin in plugins/langfuse-codex-tracing/, including its ES module declaration. Tests exercise the packaged hooks with the real Langfuse SDK and a mocked HTTP transport; they do not contact your Langfuse project.

Install or upgrade

From this checkout, after building:

codex plugin marketplace add "$PWD"
codex plugin add langfuse-codex-tracing@langfuse-local

Alternatively, install and enable langfuse-codex-tracing from the langfuse-local marketplace in the plugin UI. Reload the plugin or restart Codex after upgrading. The marketplace points at the generated plugins/langfuse-codex-tracing/ directory. Both the portable root plugin.json and .codex-plugin/plugin.json compatibility manifest are included.

Current Codex supports plugin-bundled hooks. Review and trust the new or changed definitions through /hooks before expecting them to run. Installation does not automatically trust hooks. Lifecycle-hook plugins are distributed through manual desktop installation; they are not eligible for the public plugin directory. See OpenAI's plugin packaging documentation.

If upgrading a v0.1 installation that used config-layer hooks, choose one hook source:

  • Use bundled hooks and remove this plugin's old entries from ~/.codex/hooks.json or the project's .codex/hooks.json, preserving unrelated hooks.
  • Keep config-layer hooks as a compatibility option and avoid enabling a second copy through the plugin. Regenerate them using the installer below.

Codex combines matching hook sources, so configuring both runs both copies.

Optional config-layer installer

npm run install-hooks:global
node scripts/install-hooks.mjs --project /path/to/project

The installer reads the same event definitions as the plugin, generates absolute script paths, upgrades its previous definitions at the same location, preserves unrelated hooks, and can be rerun. --global respects CODEX_HOME. Use --output /path/to/hooks.json to preview a separate configuration. Keep the checkout at its installed path or rerun the installer after moving it.

Bundled hooks use the runtime's PLUGIN_ROOT, so they also work when the conversation's working directory differs from the plugin's installation directory.

Feedback

From the project directory, use the absolute path to the installed bundle:

node /path/to/plugin/bundle/commands/feedback.js up "helpful fix"
node /path/to/plugin/bundle/commands/feedback.js down "missed the failing test"
node /path/to/plugin/bundle/commands/journey.js up "productive session"

The included langfuse-feedback skill resolves the installed bundle location when you ask Codex to record feedback. Scores have stable IDs, so repeating feedback updates the existing score. Interrupted turns remain eligible for feedback. Session end removes its active-directory lookup without affecting a newer session in that directory.

Reliability and limits

State writes use owner-stamped locks and atomic replacement. Lock contention times out without writing unlocked state. A per-session export lock prevents concurrent terminal hooks from exporting the same turn twice. Trace, generation, and tool IDs are deterministic so retries update the same observations. Transcript cursors advance after successful SDK delivery; HTTP failures retain the pending state for a later attempt. There is no persistent outbound queue beyond the transcript and pending session state.

Codex rollout files vary by version. The parser handles nested usage, cumulative usage deltas, duplicate event/response text, custom tool calls, and partially written final rows. Cached input is split from total input using Langfuse's OpenAI input_cached_tokens price key. Anthropic cache-write lifetimes are not assumed for Codex.

Generations currently summarize a whole Codex turn; they do not claim exact per-request model boundaries. History is reconstructed from retained transcript messages, so it is not an exact replay of the model's hidden context or compaction state. Subagent generations require a readable agent_transcript_path; without one, the lifecycle span still records the event.

Hooks are best-effort and do not return blocking decisions. Network requests use a short 800 ms timeout with no SDK retries to fit terminal-hook budgets; failed turn exports can retry at the next lifecycle opportunity. Codex caps Interrupt and SessionEnd at three seconds, so a large transcript or slow network can leave recovery for a later prompt. State expires after 24 hours when sessions are pruned. Hook scripts must exist in the execution environment; cloud orchestration does not generally run local plugin command hooks.