Back to blog

See what Claude Code is really doing: tracing with LangSmith and Langfuse

Paweł Twardziak18 min read
See what Claude Code is really doing: tracing with LangSmith and Langfuse

Claude Code shows you quite a lot in the terminal. Tool calls scroll by, and at the end there's a diff and a summary. But that view is momentary. After an hour of work the scrollback is beyond reviewing, long outputs are collapsed, and whatever happened inside a subagent is gone along with its summary.

So some questions go unanswered. How many times did the agent read the same file? Which tool call ate the most context? At what point did it start circling the same problem? What's left is the end result and a feeling that it took longer than it should have.

You can fix that in a few minutes, with no proxy and without touching your Anthropic API key. Both LangSmith and Langfuse ship plugins that hook into Claude Code's lifecycle and send every turn to your observability dashboard [1][4].

Where the data comes from

This isn't wiretapping the API, and it isn't a proxy in front of the model. Both tools use Claude Code's hook system, that is scripts fired at specific points of a session's lifecycle [9]. What differs is how deep they hook in.

Langfuse takes the minimal route. It registers Stop, which fires at the end of a turn, and SessionEnd to close the session out [5]. The script reads the transcript file, a JSONL of user messages, model responses, tool calls and their results, turns it into a trace tree, and ships it [4].

The LangSmith plugin hooks into nine points at once: UserPromptSubmit, PreToolUse, PostToolUse, Stop, StopFailure, SubagentStop, PreCompact, PostCompact, and SessionEnd [2]. Tool calls are therefore captured as they happen, and separate hooks catch subagent completion and context compaction. Those are the two events the transcript alone shows worst.

In both cases the turns of one session are tied together by a shared session ID [6]. This design has three consequences:

  • A complete trace arrives after the turn, not during it. The LangSmith docs say outright that a trace is complete only once Claude Code has responded [1]. This is not a live view in the debugger sense.
  • Timestamps are backdated. The Langfuse hook reaches into the OTel tracer internals to set a historical start_time and end_time from the transcript [4]. That's why durations in the dashboard are real, instead of collapsing into a single millisecond at upload time.
  • The hook never blocks the session. Langfuse states this explicitly: no SDK, no environment variable, a dead network, and the script exits quietly without stopping Claude Code [4][5]. Which is why debugging starts with the question of whether the hook fired at all.

LangSmith: three commands and one JSON block

You need the Claude Code CLI, Node.js, and a LangSmith API key [1]. No Python at all, the plugin is written in TypeScript.

Installation comes in two equivalent forms [11]. Inside a session you use slash commands, outside it the same operations run through claude plugin in the shell. That second form is handy in a bootstrap script or on a fresh machine.

First you register the marketplace, the catalog the plugin comes from:

/plugin marketplace add langchain-ai/langsmith-claude-code-plugins

or

claude plugin marketplace add langchain-ai/langsmith-claude-code-plugins

Then you install the plugin itself, in name@marketplace form:

/plugin install langsmith-tracing@langsmith-claude-code-plugins

or

claude plugin install langsmith-tracing@langsmith-claude-code-plugins

Finally a reload, so the plugin comes up in the current session instead of after a restart:

/reload-plugins

Then you tell the plugin where to send things. Cleanest through the project settings file, .claude/settings.local.json:

{
  "env": {
    "TRACE_TO_LANGSMITH": "true",
    "CC_LANGSMITH_API_KEY": "lsv2_pt_...",
    "CC_LANGSMITH_PROJECT": "my-project"
  }
}

The same env block works in ~/.claude/settings.json, at the user level. Settings resolve per variable, and the project scope outranks the user scope [10], so the handiest layout splits across two files. Globally you keep the key and the switch, once per machine, in ~/.claude/settings.json:

{
  "env": {
    "TRACE_TO_LANGSMITH": "true",
    "CC_LANGSMITH_API_KEY": "lsv2_pt_..."
  }
}

And in the repository, in .claude/settings.json, just the project name:

{
  "env": {
    "CC_LANGSMITH_PROJECT": "my-project"
  }
}

A session in that directory gets all three variables, and every other directory lands in the claude-code project, because that's the default [2]. On the repo side pick settings.json, not settings.local.json. The former is committed, and a project name is not a secret, so the whole team gets the same split of traces. The key stays with you alone. It works the other way round too: setting TRACE_TO_LANGSMITH to "false" in the project settings turns the upload off right there. Before you enable tracing globally, just check whether you want it to cover every repository you run Claude Code in, other people's included.

That's it. You use Claude Code normally, and once it responds you switch over to LangSmith. Each of your messages is its own trace, and inside you get the full picture: the user prompt, every tool call, token usage, subagent runs, and the final response [3]. The Threads tab groups all turns of one session into a single thread, so you follow a whole conversation instead of single shots.

The docs only cover part of the variables. The repository README also lists CC_LANGSMITH_METADATA (arbitrary JSON attached to every run), CC_LANGSMITH_REDACT and CC_LANGSMITH_REDACT_EXTRA (more on those in the privacy section), plus LANGSMITH_ENDPOINT, the API address [2].

That last one is mandatory for anyone not sitting on the default instance. LangSmith runs in four regions and each has its own API address [12]:

RegionLANGSMITH_ENDPOINT
GCP UShttps://api.smith.langchain.com (default)
GCP EUhttps://eu.api.smith.langchain.com
GCP APAChttps://apac.api.smith.langchain.com
AWS UShttps://aws.api.smith.langchain.com

The prefixes here are inconsistent and I walked straight into it. I put CC_LANGSMITH_ENDPOINT with the EU address into ~/.claude/settings.json, because every other variable of the plugin starts with CC_. No such variable exists. The plugin reads the address only as LANGSMITH_ENDPOINT, under the SDK's name, which you can see in src/config.ts [2]. So my key from an EU workspace was going to the default host in the US, authorization failed, and the hook stayed quiet. The symptom: an empty project in LangSmith.

The API key is more forgiving and accepts both forms, CC_LANGSMITH_API_KEY with a fallback to LANGSMITH_API_KEY.

Langfuse: a plugin or a Python hook

There are two routes, and the choice isn't obvious, because the docs and the plugin repository describe different states of the same project.

Route one, the plugin [5]. Marketplace:

/plugin marketplace add langfuse/Claude-Observability-Plugin

or

claude plugin marketplace add langfuse/Claude-Observability-Plugin

Plugin:

/plugin install langfuse-observability@langfuse-observability

or

claude plugin install langfuse-observability@langfuse-observability

After a restart you configure it with:

/plugin configure langfuse-observability@langfuse-observability

The secret key then lands in the OS keychain instead of a file in your project directory. That's an important difference, and I'll come back to it. Runtime: either uv on PATH (the SDK is fetched for you), or Python with langfuse>=4.0,<5 installed. The plugin registers two hooks, Stop after every turn and SessionEnd to close the session.

Route two, manual [4]: you install the SDK, drop in the ~/.claude/hooks/langfuse_hook.py script, register it globally in ~/.claude/settings.json, and enable it per project through .claude/settings.json:

{
  "env": {
    "TRACE_TO_LANGFUSE": "true",
    "LANGFUSE_PUBLIC_KEY": "pk-lf-...",
    "LANGFUSE_SECRET_KEY": "sk-lf-...",
    "LANGFUSE_BASE_URL": "https://cloud.langfuse.com"
  }
}

If you go that way, add .claude/settings.json to .gitignore before you paste a secret into it. The docs say so outright, and they're right [4].

Splitting between the user file and the project file works identically here, because that's a Claude Code mechanism, not a plugin one [10]. What differs is what makes sense in which file. In LangSmith the project name is a plain string, so you override it per repo. In Langfuse the project follows from the key pair, because keys are issued inside a specific project [4]. Splitting traces across several projects would therefore mean a separate key pair in every repository, so a secret in a project file. I wouldn't do that.

A saner layout is keys and region once in ~/.claude/settings.json, and nothing but a switch in the repository:

{
  "env": {
    "TRACE_TO_LANGFUSE": "false"
  }
}

That turns tracing off where you don't want it without touching the rest of the configuration. With the plugin install the secret sits in the keychain and is in no settings.json at all, so the user file then holds only the switch and possibly the region. Splitting sessions per repository happens on the Langfuse side, by filtering on trace metadata, which carries the transcript path among other things [4].

The manual route has one more cost, which its authors honestly call critical: the script relies on SDK 4.x internals, specifically _otel_tracer and _create_observation_from_otel_span. Hence the >=4.0,<5 pin, because renaming a private method in the next minor version will simply switch tracing off without a word in the logs [4]. If you want something that survives a year unattended, the plugin is a safer bet than your own copy of the script.

That leaves regions, and here I stepped in it a second time, this time by omission. I didn't set LANGFUSE_BASE_URL at all, because the Langfuse docs list the EU endpoint as the default and my account lives in the EU [4]. It looked like a variable I didn't have to touch. The hook, however, hardcodes a different default:

host = _opt("LANGFUSE_BASE_URL") or _opt("CC_LANGFUSE_BASE_URL") or "https://us.cloud.langfuse.com"

That's line 121 of hooks/langfuse_hook.py [5]. Without the variable the traffic goes to the US, keys from an EU account fail authorization, and it ends exactly like the LangSmith case: silence in the log, an empty project in the dashboard.

RegionLANGFUSE_BASE_URL
EUhttps://cloud.langfuse.com
UShttps://us.cloud.langfuse.com (default in the hook code)
Japanhttps://jp.cloud.langfuse.com
HIPAAhttps://hipaa.cloud.langfuse.com

Set your host explicitly instead of counting on the default, because the docs and the code disagree here [4][5]. Small consolation: the CC_ prefix that wrecked my LangSmith config does work in this one, since the hook also accepts CC_LANGFUSE_BASE_URL.

What you actually get to see

A trace shows the rhythm of the agent's work: tool call, generation, tool call, generation. In Langfuse you get it as three levels of nesting, a turn, generations inside the turn, and tool spans under the generation that called them [4]. In LangSmith each message is its own trace, with tool and subagent spans sitting inside [1].

One turn in LangSmith: 138.64 s and 795.6K tokens.

That's a screenshot from my own session, and it shows more than the description promises. The span column has a steady rhythm: Claude, Bash, Bash, Claude. Every model call carries a token counter, and within a single turn it climbs from 66.5K to 75.4K. Context swells by nine thousand tokens before the turn is even over.

The same mechanics in Langfuse: a turn, the model call under it, with latency, tokens, and cost alongside.

Langfuse adds a column LangSmith didn't show in this view: cost. The model call in the screenshot is 28.41 s, 66,958 tokens, and $0.44588 on claude-opus-5. In LangSmith the same entry read $0.00. I haven't checked whether that's a matter of pricing configured on the project side or of the plugin not sending cost data. Either way the difference is real and it matters when you estimate the bill.

The same goes for the agents you build

Claude Code is a convenient proving ground here, because you get a trace in a few minutes and on somebody else's code. The mechanics are the same as in your own LangGraph app or a bare loop on the SDK. An agent doesn't blow up with a stack trace. It just does something other than what it should and finishes with an answer that looks fine.

While you're building, the trace is the only place that shows where a bad answer came from: which tool returned junk, which step dropped the context, how many times the model retried the same thing. Without it you debug by guessing and rerunning, and with a non-deterministic model the same failure may not come back.

In production you get the numbers on top: cost per session, latency of a single step, the share of turns that end in a tool error. Plus material for evaluation, because a failed production run is a ready-made test case, and a prompt or tool change can be compared before and after instead of judged by feel. An agent in production without tracing is a service without logs. Doable, just hard to justify.

Which one to pick

DimensionLangSmithLangfuse
RuntimeNode.jsuv or Python with SDK 4.x
Hooks9 lifecycle points, including PreToolUse, PostToolUse, SubagentStop, PreCompactStop + SessionEnd
Where the secret landssettings file or environment variablesOS keychain with the plugin install
Secret redactionbuilt in, on by default, with custom patternsnone, checked in the hook code
Session groupingThreads tabSessions tab
Attaching to an existing traceCC_LANGSMITH_PARENT_DOTTED_ORDERCC_LANGFUSE_TRACEPARENT (W3C)
Multiple destinations at onceyes, replicasnot in the docs
Tracing in GitHub Actionsdocumentednot in the docs
Truncating long payloadsnot in the docsCC_LANGFUSE_MAX_CHARS, 20k characters by default
Self-hostingEnterprise plan onlyyes, open source, free
Plugin licenseMITMIT

Pricing, as of July 2026. LangSmith: the Developer plan at $0 gives up to 5k base traces a month and one seat, Plus costs $39 per seat and raises the threshold to 10k, base retention is 14 days [7]. Langfuse: Hobby free with 50k units and 30-day retention, Core at $29 with 100k units and 90-day retention, plus a self-hosted variant at zero [8].

Before you decide the free tier is enough, count what a single session really generates. Traces from Claude Code are fat. Every file read, every Bash, every Grep is its own span with full input and output, and one intense session is dozens of turns. For scale: the turn in the screenshot above is 795.6K tokens, and a single day's list has over twenty threads in my case. For one person the free tier is plenty. For a five-person team working this way daily the bill looks different, especially on the retention side.

In practice: LangSmith if you're already on LangChain or LangGraph and want production agents and Claude Code in one place, and if tracing in CI matters to you. Langfuse if the data can't leave your infrastructure, or you need a specific region, or you simply don't want another SaaS in the budget. Wiring up both at once is technically possible, but that's double overhead on every turn and a double copy of the transcript leaving the building. I don't recommend it as a default.

What to think about before you switch it on

A Claude Code transcript is not metadata. It's your code, file paths, config fragments, shell command output, and everything that accidentally landed in a tool's output. Enabling tracing means sending that to an external system.

LangSmith does one thing with it that Langfuse doesn't do at all: it redacts secrets client-side, before anything leaves. By default it strips API keys, JWTs, PEM blocks, and common credential patterns, and you add your own rules through CC_LANGSMITH_REDACT_EXTRA [2]. That's a good defense, but not magic. A regex won't catch a secret that doesn't look like a secret, and it won't redact personal data the agent pulled out of a database while debugging.

There's no equivalent on the Langfuse side. I searched the hook for redaction, masking, and anonymization and there's nothing of the sort [5]. The transcript goes to the server whole, trimmed only at the CC_LANGFUSE_MAX_CHARS threshold. For data that can't leave the building, self-hosting is what's left.

Second thing: the LangSmith plugin attaches anthropic_user_id, read from ~/.claude.json, and local_username, your OS login, to the traces [2]. In a team setting that's an employee's personal data in an external vendor's system. Not a problem in itself, but it belongs in your record of processing activities rather than surfacing during an audit.

One level up: CI and nesting

The most interesting thread in the LangSmith docs is buried the deepest. The same plugin can be switched on in GitHub Actions alongside anthropics/claude-code-action, with the metadata filled from the pull request [1]:

- uses: anthropics/claude-code-action@v1
  env:
    TRACE_TO_LANGSMITH: "true"
    CC_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
    CC_LANGSMITH_PROJECT: "my-project"
    CC_LANGSMITH_METADATA: |
      {
        "pr_url": "${{ github.event.pull_request.html_url || '' }}",
        "pr_number": "${{ github.event.pull_request.number || '' }}",
        "repository": "${{ github.repository }}",
        "commit_sha": "${{ github.sha }}"
      }
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    github_token: ${{ secrets.GITHUB_TOKEN }}
    plugin_marketplaces: |
      https://github.com/langchain-ai/langsmith-claude-code-plugins.git
    plugins: |
      langsmith-tracing@langsmith-claude-code-plugins
    prompt: |
      Your prompt here

Every decision of an agent reviewing pull requests then has a trace pinned to a specific PR and commit. When someone asks why the bot blocked a change, the answer is in a link to the trace.

The second mechanism is nesting. If you run Claude Code as a subprocess inside your own traced pipeline, you can pass the parent's dotted_order through CC_LANGSMITH_PARENT_DOTTED_ORDER and see the whole thing as one tree [1]:

@traceable
def run_claude(prompt: str):
    run_tree = get_current_run_tree()
    subprocess.run(
        ["claude", "-p", prompt],
        env={
            **os.environ,
            "TRACE_TO_LANGSMITH": "true",
            "CC_LANGSMITH_API_KEY": "lsv2_pt_...",
            "CC_LANGSMITH_PROJECT": "claude-code",
            "CC_LANGSMITH_PARENT_DOTTED_ORDER": run_tree.dotted_order,
        },
    )

This is the pattern for harnesses that fire dozens of sessions in a loop: code migration, batch refactors, evaluating prompt variants. Langfuse has an equivalent in CC_LANGFUSE_TRACEPARENT, in the W3C standard [5].

When it doesn't work

Four things account for most of the problems [6]:

  1. An empty hook log. ~/.claude/state/hook.log for LangSmith, ~/.claude/state/langfuse_hook.log for Langfuse. Empty means the hook isn't firing at all, so the problem is in the registration, not in the keys.
  2. The switch as a string. "true" lowercase, in quotes. Not a boolean, not "True". It sounds like a detail and it costs people half an hour each.
  3. Debug mode. CC_LANGSMITH_DEBUG or CC_LANGFUSE_DEBUG set to "true" usually tells you exactly what went wrong.
  4. The right runtime and the right directory. Claude Code has to run from the project directory that holds the settings with the switch.

Plus two documented limitations. In LangSmith subagents are traced only on completion, so interrupting a turn mid-subagent means losing its whole subtree [1]. With a working style where Esc is a normal tool, that happens more often than you'd like. In Langfuse, desktop apps don't read shell profiles, so configuration has to go through /plugin configure, and the regular Claude Desktop chat isn't captured at all, because it isn't hook-backed [5].

What to do today

Pick one tool, wire it into one project, and put in a normal day's work with it. Then open the dashboard and look for a file read many times over, a tool called twice with the same argument, a step that ate a disproportionate amount of time. Every such spot is a line to add to CLAUDE.md or a prompt to sharpen.

The setup takes a few minutes. The rest is reading your own traces.

If you want to learn how to build and debug agentic systems from the engineering side rather than from demos, take a look at the Soft 3.0 course. Observability, evaluation, and boundaries for agents are a running theme in it.


Sources

Content verified against the official documentation and repositories of both projects. As of 28 July 2026.

  1. LangSmith, Trace Claude Code (plugin installation, variables, GitHub Actions, replicas, nesting, the subagent limitation) - https://docs.langchain.com/langsmith/trace-claude-code
  2. Repository langchain-ai/langsmith-claude-code-plugins (full variable list, secret redaction, identity metadata, MIT license) - https://github.com/langchain-ai/langsmith-claude-code-plugins
  3. LangChain, "Trace Claude Code applications in LangSmith" (video, the trace view and the Threads tab) - https://www.youtube.com/watch?v=jLOM_ahG78c
  4. Langfuse, Claude Code integration (manual and plugin setup, trace structure, regions, state management, troubleshooting) - https://langfuse.com/integrations/developer-tools/claude-code
  5. Repository langfuse/Claude-Observability-Plugin (installation, keychain configuration, the Stop and SessionEnd hooks, GUI limitations, MIT license) - https://github.com/langfuse/Claude-Observability-Plugin
  6. Langfuse, "Trace Claude Code" (video, the hook mental model, typical pathologies in a trace, four troubleshooting points) - https://www.youtube.com/watch?v=fsoBHf_WNmQ
  7. LangSmith, plans and pricing (Developer, Plus, Enterprise, trace retention) - https://www.langchain.com/pricing-langsmith
  8. Langfuse, pricing (Hobby, Core, Pro, Enterprise, self-hosting) - https://langfuse.com/pricing
  9. Claude Code, hooks documentation (session lifecycle points, including Stop) - https://code.claude.com/docs/en/hooks-guide
  10. Claude Code, settings documentation (settings file scopes, precedence order, the env key) - https://code.claude.com/docs/en/settings
  11. Claude Code, Discover and install plugins (the /plugin marketplace add, /plugin install name@marketplace, /reload-plugins commands and their claude plugin ... shell equivalents) - https://code.claude.com/docs/en/discover-plugins
  12. LangSmith, Create an account and API key (API addresses for the GCP US, GCP EU, GCP APAC, and AWS US regions, the LANGSMITH_ENDPOINT default) - https://docs.langchain.com/langsmith/create-account-api-key