Langfuse Observability Plugin for Claude Code

September 3, 2026 · View on GitHub

This plugin sends Claude Code sessions to Langfuse. It records the user prompts, the agent turns, the model generations with their tokens and cost, and the tool calls, with no change to your code.

Langfuse also documents this integration on the Claude Code integration page.

What can this integration trace?

The plugin runs as a Claude Code hook and reads the session transcript on every turn, so tracing needs no change to the way you work:

  • Agent turns: one trace per user prompt, with all turns of a session grouped under one session ID.
  • Model generations: every assistant message with inputs, outputs, cost and token usage, including cache reads and a cache write split by the lifetime it was written under, which each carry their own rate. token usage, including cache-read and reasoning splits.
  • Thinking: assistant thinking blocks attach to the generation that produced them and render as thinking blocks in the trace. Claude Code writes the thinking text only when showThinkingSummaries is true in its settings.
  • Tool calls: each tool Claude Code invokes, with input and output.
  • Subagents: subagent and Workflow-spawned agent transcripts nest under the turn that started them.
  • Skills: traces carry a skill:<name> tag for every skill a turn invokes.
  • Images: pasted images and screenshots from tool results upload as Langfuse media and render inside the trace.

Tracing covers the claude CLI and desktop Code mode; regular Claude Desktop Chat mode runs no Claude Code hooks and is not traced.

Prerequisites

One of:

  • uv (recommended) on PATH. The hook uses uv run --script and installs the Langfuse SDK from the script metadata.
  • Python 3.10+ as python3 with langfuse>=4.7,<5 installed. This is only a fallback for when uv is not on PATH.

On the first hook run, uv downloads the SDK from PyPI and caches it. An offline or proxied machine retries that download every turn until the cache is warm, so pre-warm it from a networked terminal:

echo '{}' | uv run --quiet --script <plugin-root>/hooks/langfuse_hook.py

Without a usable runtime the hook exits without tracing, never blocks or slows Claude Code, and logs the reason, see Troubleshooting.

Install

claude plugin marketplace add langfuse/Claude-Observability-Plugin
claude plugin install langfuse-observability@langfuse-observability

The marketplace command registers the plugin marketplace and refreshes its local cache. The install command enables the plugin for your Claude Code user scope. Restart Claude Code afterwards so the hook configuration is loaded.

Add your Langfuse credentials

Configure the plugin from inside a Claude Code session. This is a Claude Code slash command, not a shell command:

$ claude
> /plugin configure langfuse-observability@langfuse-observability

Alternatively, pass the values during install:

claude plugin install langfuse-observability@langfuse-observability \
  --config LANGFUSE_PUBLIC_KEY=pk-lf-... \
  --config LANGFUSE_SECRET_KEY=sk-lf-... \
  --config LANGFUSE_BASE_URL=https://cloud.langfuse.com

Only LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are required; without LANGFUSE_BASE_URL the plugin uses https://cloud.langfuse.com (EU region). Get keys from your Langfuse project settings → API Keys. The secret key is held in your OS keychain, not in a file.

Configuration options

Set these through /plugin configure or --config, unless the table says the value is a per-run environment variable.

OptionDescriptionRequired
LANGFUSE_SECRET_KEYYour Langfuse secret key (sk-lf-...). Held in your OS keychain.Yes
LANGFUSE_PUBLIC_KEYYour Langfuse public key (pk-lf-...).Yes
LANGFUSE_BASE_URLLangfuse host. EU: https://cloud.langfuse.com, US: https://us.cloud.langfuse.com, or your self-hosted URL.No (defaults to EU)
LANGFUSE_USER_IDUser identifier attached to every trace, shown as the user in Langfuse.No
CC_LANGFUSE_DEBUGVerbose logging to the hook log (default false).No
CC_LANGFUSE_MAX_CHARSTruncate captured inputs and outputs to this many characters (default 20000).No
CC_LANGFUSE_SKILL_TAGSTag traces with skill:<name> for every skill invoked in the turn (default true).No
CC_LANGFUSE_CAPTURE_SKILL_CONTENTInclude injected skill instruction text in the Skill tool span output (default false).No
CC_LANGFUSE_CAPTURE_IMAGESUpload images to Langfuse and show them in the trace (default true). Needs media upload on your deployment (self-hosted: LANGFUSE_S3_MEDIA_UPLOAD_*). Set it to false if media upload is unavailable: the trace then shows a marker per image, such as [image image/png ~200KB].No
CC_LANGFUSE_STATE_DIRAbsolute directory (~ is expanded) for the hook's state, lock and log files (default ~/.claude/state). Set one per CLAUDE_CONFIG_DIR installation to keep them apart. An unusable value falls back to the default and logs a warning.No
CC_LANGFUSE_TRACE_SEEDSeed that makes trace IDs predictable, so a headless caller can derive a run's trace ID before the trace exists. Use a unique seed per session, otherwise sessions collide on the same trace IDs.No
CC_LANGFUSE_TRACEPARENTPer-run environment variable. W3C traceparent of an existing trace to attach to — see Attach runs to an existing trace.No
CC_LANGFUSE_PARENT_TRACE_ID / CC_LANGFUSE_PARENT_SPAN_IDPer-run environment variables. Explicit alternative to CC_LANGFUSE_TRACEPARENT (32-hex trace id plus 16-hex span id).No

Update

Updates are manual since auto-update is off by default for marketplaces outside Anthropic's.

claude plugin marketplace update langfuse-observability
claude plugin update langfuse-observability@langfuse-observability

The marketplace command pulls the current code from GitHub into the local marketplace clone. Restart Claude Code afterwards so the new hook is loaded.

To see which version is installed:

claude plugin list

Enable and disable tracing

ScopeHow
Change the settings/plugin configure langfuse-observability@langfuse-observability in a session
Stop tracing new sessionsclaude plugin disable langfuse-observability@langfuse-observability --scope user
Start tracing againclaude plugin enable langfuse-observability@langfuse-observability --scope user
Remove the pluginclaude plugin uninstall langfuse-observability

Disabling keeps the plugin installed and keeps your configuration. Check the result with claude plugin list.

A session that already runs loads its hooks at startup, so restart it after you disable the plugin. Enable and disable can be project-scoped, so run claude plugin list from the directory you work in.

Attach runs to an existing trace

When your application launches Claude Code headlessly (claude -p) as one step of an already-instrumented workflow, the run can join your existing Langfuse trace instead of creating its own root trace. Create a span for the agent run, then pass its trace context when you launch Claude Code:

with langfuse.start_as_current_span(name="Claude Code run") as run_span:
    traceparent = f"00-{run_span.trace_id}-{run_span.id}-01"
    subprocess.run(
        ["claude", "-p", "Refactor utils.py"],
        env={**os.environ, "CC_LANGFUSE_TRACEPARENT": traceparent},
    )

Every turn of the session, with its model calls, tool calls and subagents, then appears under your Claude Code run span. CC_LANGFUSE_PARENT_TRACE_ID and CC_LANGFUSE_PARENT_SPAN_ID are an explicit alternative to the traceparent.

Troubleshooting

Nearly every failure explains itself in ~/.claude/state/langfuse_hook.log, or in CC_LANGFUSE_STATE_DIR if you set a usable one. Send one message, then match the newest lines against this table:

What the log showsWhat to do
No new lines at allThe hook never launched: the plugin is disabled, no usable runtime was found, or uv could not download the SDK. Run claude plugin list from the directory you use, and put uv on the PATH of the app that launches Claude Code.
langfuse import failed (…) python=… PATH=…The Python that ran the hook cannot import the SDK. The line names the interpreter and PATH. Install uv on that PATH, or make python3 a 3.10+ environment with langfuse>=4.7,<5.
Langfuse config incomplete: missing …The named keys did not reach the hook. Configure them with /plugin configure. If the line also says loaded under plugin identity '@inline', see below.
Hook started plus a skip reasonThe hook ran and skipped on purpose, which is usual for background sessions. Report it with the log line if real turns are missing.
Processed N turns … but nothing in LangfuseDelivery failed after the SDK took the turns. Check LANGFUSE_BASE_URL (EU against US), key validity, and proxy reachability.

Hook started and other [DEBUG] lines need CC_LANGFUSE_DEBUG. The failure lines above are [INFO] and appear without it.

Desktop app (GUI) sessions

A GUI app does not read your shell profile and resolves PATH once at launch, so a variable you export in ~/.zshrc never reaches a GUI-spawned hook. After you install uv, fully quit and relaunch the app, and set keys through /plugin configure rather than shell exports.

Recent Claude Desktop builds also load user-installed plugins under a second plugin identity (langfuse-observability@inline), so keys from /plugin configure never reach the hook: terminal sessions work while desktop sessions stay silent. The log line then contains loaded under plugin identity '@inline'. As a temporary workaround, repeat your options under that identity in ~/.claude/settings.json:

"pluginConfigs": {
  "langfuse-observability@inline": {
    "options": {
      "LANGFUSE_PUBLIC_KEY": "pk-lf-...",
      "LANGFUSE_SECRET_KEY": "sk-lf-...",
      "LANGFUSE_BASE_URL": "https://cloud.langfuse.com"
    }
  }
}

The entry needs LANGFUSE_SECRET_KEY, because the keychain secret applies only to the installed identity. It is plain text, so use a dedicated key pair and remove the entry once it is no longer needed. Settings are read on the next message, with no restart.

Development

uv run --group dev pytest

The hook is a single uv script with inline dependency metadata, and Claude Code runs hooks/langfuse_hook.py directly, so there is no build step.

License

MIT