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_timeandend_timefrom 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-pluginsThen 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-pluginsFinally 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]:
| Region | LANGSMITH_ENDPOINT |
|---|---|
| GCP US | https://api.smith.langchain.com (default) |
| GCP EU | https://eu.api.smith.langchain.com |
| GCP APAC | https://apac.api.smith.langchain.com |
| AWS US | https://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-PluginPlugin:
/plugin install langfuse-observability@langfuse-observability
or
claude plugin install langfuse-observability@langfuse-observabilityAfter 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.
| Region | LANGFUSE_BASE_URL |
|---|---|
| EU | https://cloud.langfuse.com |
| US | https://us.cloud.langfuse.com (default in the hook code) |
| Japan | https://jp.cloud.langfuse.com |
| HIPAA | https://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].
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.
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
| Dimension | LangSmith | Langfuse |
|---|---|---|
| Runtime | Node.js | uv or Python with SDK 4.x |
| Hooks | 9 lifecycle points, including PreToolUse, PostToolUse, SubagentStop, PreCompact | Stop + SessionEnd |
| Where the secret lands | settings file or environment variables | OS keychain with the plugin install |
| Secret redaction | built in, on by default, with custom patterns | none, checked in the hook code |
| Session grouping | Threads tab | Sessions tab |
| Attaching to an existing trace | CC_LANGSMITH_PARENT_DOTTED_ORDER | CC_LANGFUSE_TRACEPARENT (W3C) |
| Multiple destinations at once | yes, replicas | not in the docs |
| Tracing in GitHub Actions | documented | not in the docs |
| Truncating long payloads | not in the docs | CC_LANGFUSE_MAX_CHARS, 20k characters by default |
| Self-hosting | Enterprise plan only | yes, open source, free |
| Plugin license | MIT | MIT |
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 hereEvery 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]:
- An empty hook log.
~/.claude/state/hook.logfor LangSmith,~/.claude/state/langfuse_hook.logfor Langfuse. Empty means the hook isn't firing at all, so the problem is in the registration, not in the keys. - 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. - Debug mode.
CC_LANGSMITH_DEBUGorCC_LANGFUSE_DEBUGset to"true"usually tells you exactly what went wrong. - 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.
- LangSmith, Trace Claude Code (plugin installation, variables, GitHub Actions, replicas, nesting, the subagent limitation) - https://docs.langchain.com/langsmith/trace-claude-code
- 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 - LangChain, "Trace Claude Code applications in LangSmith" (video, the trace view and the Threads tab) - https://www.youtube.com/watch?v=jLOM_ahG78c
- Langfuse, Claude Code integration (manual and plugin setup, trace structure, regions, state management, troubleshooting) - https://langfuse.com/integrations/developer-tools/claude-code
- Repository
langfuse/Claude-Observability-Plugin(installation, keychain configuration, theStopandSessionEndhooks, GUI limitations, MIT license) - https://github.com/langfuse/Claude-Observability-Plugin - 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
- LangSmith, plans and pricing (Developer, Plus, Enterprise, trace retention) - https://www.langchain.com/pricing-langsmith
- Langfuse, pricing (Hobby, Core, Pro, Enterprise, self-hosting) - https://langfuse.com/pricing
- Claude Code, hooks documentation (session lifecycle points, including
Stop) - https://code.claude.com/docs/en/hooks-guide - Claude Code, settings documentation (settings file scopes, precedence order, the
envkey) - https://code.claude.com/docs/en/settings - Claude Code, Discover and install plugins (the
/plugin marketplace add,/plugin install name@marketplace,/reload-pluginscommands and theirclaude plugin ...shell equivalents) - https://code.claude.com/docs/en/discover-plugins - LangSmith, Create an account and API key (API addresses for the GCP US, GCP EU, GCP APAC, and AWS US regions, the
LANGSMITH_ENDPOINTdefault) - https://docs.langchain.com/langsmith/create-account-api-key