ENGINEERING BLOG · CLAUDE CODE

Claude Code Observability: Every Session, Tool Call, and Dollar

Claude Code already exports OpenTelemetry. Point it at Traccia with one settings block and see every session, model call, tool run, and the cost estimate Claude Code reports. No SDK, no wrapper, no collector.

13 min read
Read the setup guide

Key takeaways

  • A terminal agent needs a record of what it ran. In 2025, malware ran Claude Code with permission checks off to hunt for secrets, and two Claude Code CVEs let a command skip the approval prompt. A trace of every session and tool call is how you find out after the fact.
  • Setup is one env block. Turn on telemetry, enable the trace, metric, and log exporters, and point them at Traccia in ~/.claude/settings.json. No code changes.
  • Content is opt-in. By default Claude Code redacts prompt text and Bash commands. Turn on OTEL_LOG_USER_PROMPTS and OTEL_LOG_TOOL_DETAILS only if you want them in the audit trail.
  • The dollar figure is an estimate. Traccia shows the cost_usd Claude Code reports, which Anthropic documents as an estimated cost. Your invoice is still the source of truth.

Why Trace Claude Code? Lessons From Real Incidents

Claude Code is not autocomplete. It runs in your terminal, reads your repo, runs shell commands, edits files, spawns subagents, and keeps going until the task is done. One prompt can turn into dozens of model calls and tool runs. Three public incidents show why that activity deserves a record. Each one ends with what a trace adds.

1. Malware can drive the agent you installed

On August 26, 2025, malicious releases of the Nx build tool were published to npm. Wiz reported that the payload looked for installed AI coding CLIs, including Claude Code, and ran them with flags such as --dangerously-skip-permissions to search the machine for secrets and wallet files. The stolen data was pushed to public GitHub repositories named s1ngularity-repository.

What a trace adds

Claude Code reads its telemetry settings from ~/.claude/settings.json, so a session started by a script under your account can export like any other. An unexpected session with no prompt from you, a burst of Bash and Read calls, and an odd start time is the kind of row you can spot on the Traces page.

2. Approval prompts have had bugs

In August 2025, Cymulate disclosed two Claude Code vulnerabilities. CVE-2025-54794 was a path restriction bypass, fixed in 0.2.111. CVE-2025-54795 let a crafted command run without the approval prompt, fixed in 1.0.20. Both needed untrusted content, such as a malicious file, to reach Claude Code's context.

What a trace adds

Tool spans record which tool ran, when, and whether it failed. With OTEL_LOG_TOOL_DETAILS=1, they also carry the Bash command itself. Claude Code tags its telemetry with app.version, so you can also find sessions still running a version older than a fix.

3. Autonomous runs look different from human ones

In November 2025, Anthropic reported a state-sponsored group that used Claude Code as an autonomous orchestrator against about 30 targets, with the AI performing an estimated 80 to 90 percent of the tactical work. Anthropic caught it on its side. On your side, the same pattern shows up as volume: far more tool calls per session than a person at a keyboard produces.

What a trace adds

Traccia counts tool calls per run, so a Tool Storm alert flags sessions that run far past your team's normal pace.

Questions every team should answer

QuestionWhere Traccia answers itNeeds an opt-in?
Where did the time go?Execution timeline with every model call and tool runNo
How big did the context get?Peak context and cumulative tokens per sessionNo
What did this session cost?Claude Code's reported cost estimate, rolled up per sessionNo
Which tools ran, and which failed?Tool spans with status and tool retry badgesNo
Which Bash commands ran?Tool span attributesOTEL_LOG_TOOL_DETAILS=1
What was the developer's prompt?user_prompt log events in the audit trailOTEL_LOG_USER_PROMPTS=1

How Claude Code Telemetry Reaches Traccia

Claude Code sends OTLP traces, metrics, and logs to Traccia's ingestion endpoint. Traccia normalizes the claude_code.* spans, stitches prompts into sessions on session.id, and attaches the cost estimate from Claude Code's api_request log events. The result shows up on the Traces page like any other agent.

Figure 1. Claude Code exports OpenTelemetry straight to Traccia. There is no collector or proxy in between.
Claude Code signalWhat Traccia does with it
claude_code.interaction spanOne trace per prompt, typed as the agent turn
claude_code.llm_request spanLLM span with model and token usage
claude_code.tool spanTool span (Bash, Read, Edit, Grep, subagents)
session.id attributeGroups interactions into one session
api_request log eventsClaude Code's reported cost estimate (cost_usd)
claude_code.* metricsAlso stored as gen_ai.client.* for shared dashboards

Setup in Four Steps

Step 1: Create an ingestion key

Sign in at app.traccia.ai, open Settings → API Keys, and create an ingestion key.

Step 2: Configure Claude Code

Add an env block to ~/.claude/settings.json. Over HTTP (protobuf):

~/.claude/settings.json
json
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "https://api.traccia.ai/v2/traces",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "https://api.traccia.ai/v2/metrics",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "https://api.traccia.ai/v2/logs",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer YOUR_TRACCIA_API_KEY",
"OTEL_SERVICE_NAME": "claude-code",
"OTEL_RESOURCE_ATTRIBUTES": "agent.id=claude-code,agent.name=claude-code,env=production"
}
}

Or over gRPC, with one endpoint for all three signals:

~/.claude/settings.json
json
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.traccia.ai:4317",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer YOUR_TRACCIA_API_KEY",
"OTEL_SERVICE_NAME": "claude-code",
"OTEL_RESOURCE_ATTRIBUTES": "agent.id=claude-code,agent.name=claude-code,env=production"
}
}

Every line matters. Leave out the exporter lines and Claude Code sends nothing, even with telemetry enabled:

VariableWhat it does
CLAUDE_CODE_ENABLE_TELEMETRYTurns on Claude Code's telemetry
CLAUDE_CODE_ENHANCED_TELEMETRY_BETATurns on tracing. Without it you get metrics and logs but no spans
OTEL_TRACES_EXPORTERSet to otlp to send spans
OTEL_METRICS_EXPORTERSet to otlp to send token and cost counters
OTEL_LOGS_EXPORTERSet to otlp to send events, including the api_request events that carry cost
OTEL_EXPORTER_OTLP_PROTOCOLClaude Code has no default protocol, so set http/protobuf or grpc
OTEL_EXPORTER_OTLP_HEADERSYour Traccia ingestion key as a Bearer token
OTEL_RESOURCE_ATTRIBUTESThe agent ID and environment Traccia files these traces under

Restart Claude Code after editing settings

Claude Code reads the env block when a session starts. Sessions that were already open keep their old exporter settings.

Step 3: Run Claude Code

bash
claude

Do real work: read files, run tests, iterate. Telemetry exports every few seconds and flushes when you exit with /exit.

Step 4: Open Traces in Traccia

Each prompt is its own trace under the claude-code agent, with tokens, cost estimate, duration, and outcome.

Traccia Traces page listing three claude-code traces with tokens, cost, duration, and Success outcome
Figure 2. Claude Code interactions on the Traces page. Each prompt is one trace.

What Gets Sent, and What Stays Redacted

The setup above sends metadata only. Claude Code redacts the text of your prompts and the details of tool calls unless you opt in. That default is safe for a first rollout, and it is enough for sessions, timelines, tokens, cost, and tool failure rates.

SettingDefaultWhat it adds when set to 1
OTEL_LOG_USER_PROMPTSOffThe text of each prompt on user_prompt events. Also turns on assistant response text unless OTEL_LOG_ASSISTANT_RESPONSES says otherwise
OTEL_LOG_TOOL_DETAILSOffTool parameters on tool events and span attributes: Bash commands, MCP server and tool names, skill names, and tool input
OTEL_LOG_TOOL_CONTENTOffTool output on the tool.output span event. Requires tracing
OTEL_LOG_RAW_API_BODIESOffFull Anthropic API request and response bodies, including the entire conversation history

To audit what the agent was asked and which commands it ran, add the first two to the same env block:

~/.claude/settings.json
json
{
"env": {
"OTEL_LOG_USER_PROMPTS": "1",
"OTEL_LOG_TOOL_DETAILS": "1"
}
}
Before you turn content on

Tool details and tool output include whatever Claude Code read or ran. If it opens a .env file or echoes a token, that value can land in a span. Keep secrets out of the folders Claude Code works in, and open a few traces to check what actually arrives before a wide rollout.

Sessions: One Conversation, One Row

Claude Code gives every conversation a stable session.id. Each new prompt is a new interaction that shares it. Turn on Group by session and the conversation collapses into one row with totals for tokens, duration, and cost. Expand it to see each prompt, or click Open for the session detail page.

Traccia Traces page with Group by session enabled, showing one claude-code session with three interactions expanded underneath
Figure 3. Group by session: three prompts, 2,707 tokens, and 18.21s rolled into one session.

This answers "what did this coding session cost?" without adding up traces by hand.

Inside a Trace

Open an interaction to see the summary cards (duration, cost, LLM calls, total tokens, tool calls, status) and the Execution Timeline. The root claude_code.interaction span is the agent turn. claude_code.llm_request spans are model calls. claude_code.tool spans are Bash, Read, Edit, Grep, and subagent runs.

Traccia trace detail for a claude-code interaction showing duration, cost with an Anthropic API badge, LLM calls, tokens, three tool calls, status, and an execution timeline with a claude_code.tool span and its blocked_on_user and execution children
Figure 4. Trace detail: Claude Code's cost estimate, call counts, and the execution timeline with each tool run.

Click any span to open its drawer with model, tokens, cost, and attributes.

Traccia span drawer open for a claude_code.llm_request span next to the execution timeline
Figure 5. The span drawer for a claude_code.llm_request.

A failed tool call does not fail the turn

Say Claude Code runs your tests, pytest exits 1, and it edits the code and runs them again. Traccia keeps the error on the first tool span, adds a Tool retry badge to the interaction, and keeps the outcome as Success. You see how the agent recovered without a false alarm, and the badge is your cue to open the turn.

Figure 6. The failed pytest run stays visible as an error span, while the turn itself is marked Success.

Under the Hood

Before anything is stored, Traccia's ingestion service runs Claude Code spans through a dedicated normalizer. These excerpts come straight from that code.

Span typing

Each claude_code.* span gets a Traccia span type, and token counts are mapped to standard fields:

claude_code_normalizer.py
python
if name == "claude_code.llm_request":
attrs["span.type"] = "llm"
...
input_tokens = _coerce_int(attrs.get("input_tokens") or attrs.get("gen_ai.usage.input_tokens"))
output_tokens = _coerce_int(attrs.get("output_tokens") or attrs.get("gen_ai.usage.output_tokens"))
if input_tokens:
attrs["llm.usage.prompt_tokens"] = input_tokens
if output_tokens:
attrs["llm.usage.completion_tokens"] = output_tokens
elif name.startswith("claude_code.tool"):
attrs["span.type"] = "tool"
tool_name = attrs.get("tool_name") or attrs.get("tool.name")
if tool_name:
attrs["tool.name"] = tool_name
elif name == "claude_code.interaction":
attrs["span.type"] = "agent"

Subagents stay inside the parent agent

A subagent's tool span carries its own agent_id. Left alone, every subagent run would appear as a new agent in your inventory. Traccia keeps it as metadata instead:

claude_code_normalizer.py
python
subagent_id = attrs.get("agent_id") or attrs.get("agent.id")
if subagent_id:
attrs["claude.subagent_id"] = str(subagent_id)
attrs.pop("agent.id", None)
attrs.pop("agent_id", None)
parent_agent = attrs.get("parent_agent_id")
if parent_agent:
attrs["agent.parent_id"] = parent_agent

Honest timelines

Claude Code can leave child spans open until the exporter flushes, which would make a five-second tool call look like a minute. Traccia caps child spans to the interaction window:

claude_code_normalizer.py
python
def _cap_claude_code_span_ends(payload):
"""Cap child span end times to the interaction window (CC leaves spans open until flush)."""
...
if start and int(start) > cap_ns:
span["startTimeUnixNano"] = cap_ns
span["endTimeUnixNano"] = cap_ns
elif end_ns and int(end_ns) > cap_ns:
span["endTimeUnixNano"] = cap_ns

Stable agent identity

Model-suffixed IDs like claude-code-minimax collapse to claude-code, while deliberate team splits stay separate:

claude_code_normalizer.py
python
_TEAM_AGENT_SUFFIXES = frozenset({"backend", "frontend", "infra", "data", "mobile", "web"})
def normalize_agent_id(agent_id):
"""Collapse legacy model-suffixed CC agent ids (e.g. claude-code-minimax) to claude-code."""
...
suffix = agent_id[len("claude-code-"):]
if suffix in _TEAM_AGENT_SUFFIXES:
return agent_id
return "claude-code"

Metrics that fit existing dashboards

claude_code_normalizer.py
python
def alias_claude_code_metric_name(name: str) -> str:
"""Map CC metric names to gen_ai.client.* for dashboard compatibility."""
if name.startswith("claude_code."):
suffix = name[len("claude_code."):]
return f"gen_ai.client.{suffix}"
return name

Cost: Claude Code's Own Estimate

For runs against the Anthropic API, Traccia does not re-price tokens. It shows the cost_usd Claude Code reports on each api_request event. Anthropic documents that field as an estimated cost, so treat it as Claude Code's reported cost estimate, not your invoice.

Traccia also labels the figure by where Claude Code is routed:

claude_code_normalizer.py
python
def _ollama_billing_context(model, resource_attrs):
if not model_str or _is_anthropic_model(model_str):
return None
if ":cloud" in model_str.lower():
return "ollama_cloud"
env = str(resource_attrs.get("env") or resource_attrs.get("deployment.environment") or "")
if env in ("local", "dev"):
return "ollama_local"
return None
BackendCost card in TracciaWhat it means
Anthropic APIClaude Code's reported cost estimate, with an Anthropic API badgeClose to what the API charges, but your Anthropic invoice is the source of truth
Ollama Cloud$0, plus a CC reference figureTraccia does not price Ollama Cloud usage. $0 does not mean the usage was free
Local Ollama$0, plus a CC reference figure when availableInference runs on your hardware, so there is no API charge to show
Read the label before the number

The cost card is labeled Billed, but for Anthropic runs the amount is Claude Code's estimate. For Ollama Cloud, $0 only means Traccia has no price for that usage: Ollama offers paid plans and usage beyond its free tier, so check your Ollama account for what you actually pay. The CC reference figure is the same tokens priced at Anthropic rates, for comparison only.

Peak context vs cumulative tokens. Traccia shows peak context (the largest single request, like /context in Claude Code) separately from cumulative usage (every API call added up). A long session can have high cumulative tokens and a modest peak. Both are correct.

Alert on Claude Code Usage, and What Traccia Does Not Do

After Run policies check traces once they land, so they can watch Claude Code with no change on the developer's machine. Scope each one to the agent ID claude-code, or to your team IDs. Four templates fit well; start from your own baseline before you rely on them:

  • Cost Spike Alert. The sum of cost over a window, starting at $100 a day. It sums Claude Code's estimates, and $0 Ollama runs add nothing.
  • Tool Storm Alert. Too many tool calls in one run, starting at 25. Long agentic tasks can pass that on their own, so size it for your team.
  • Input Token Limit. Average input tokens past a budget, starting at 8,000. Claude Code sends large contexts by design, so raise this to fit.
  • Unapproved Model Alert. A run used a model outside your list. The template allows only gpt-4o and gpt-4o-mini, so replace the list with the Claude models your team uses.

On the Review step, run Simulate Last 7 Days to replay the draft against last week's traces and see how many would have matched, then turn on email alerts and activate it.

Traccia does not block Claude Code

Traccia records sessions and alerts after a run. It does not sit in front of Claude Code's tool calls. To stop commands before they run, use Claude Code's permission rules and hooks, keep approval prompts on, avoid --dangerously-skip-permissions, and keep Claude Code updated.

Rolling Out to a Team

  • Push the same env block to every developer with Claude Code managed settings, so nobody has to edit their own file.
  • Give each team its own Traccia workspace and a workspace-scoped ingestion key so traces stay isolated.
  • Decide on content capture once, in managed settings: metadata only, or with OTEL_LOG_USER_PROMPTS and OTEL_LOG_TOOL_DETAILS for a full audit trail.
  • Split profiles with distinct agent IDs:
bash
OTEL_RESOURCE_ATTRIBUTES="agent.id=claude-code-backend,agent.name=claude-code-backend,env=production"

Claude Code log events (tool decisions, API requests, and prompts when you opt in) also feed Traccia's audit pipeline. See the Compliance Hub. For the broader picture, read What Is AI Agent Observability?

Troubleshooting Claude Code Tracing

SymptomWhat to check
No data at allConfirm the three exporter lines are set to otlp and OTEL_EXPORTER_OTLP_PROTOCOL is set. Restart Claude Code, run a prompt, and exit with /exit.
Metrics and cost, but no tracesTracing is off by default. Set CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER=otlp.
Traces, but no costOTEL_LOGS_EXPORTER is missing. Cost comes from api_request log events.
401 or no data with gRPCCheck the header is Authorization=Bearer YOUR_KEY and that port 4317 is open from your network. Try the HTTP settings instead.
Prompts and Bash commands show as redactedThat is the default. Set OTEL_LOG_USER_PROMPTS=1 and OTEL_LOG_TOOL_DETAILS=1 if you want them.
Traces appear, but not on the Traces pageCheck the time range filter and that you are in the workspace your ingestion key belongs to.

Frequently Asked Questions

Do I need an OpenTelemetry Collector to send Claude Code telemetry to Traccia?

No. Traccia accepts Claude Code's OTLP traces, metrics, and logs directly over HTTPS or gRPC. You add an env block to ~/.claude/settings.json that turns on telemetry, enables the trace, metric, and log exporters, and points them at Traccia.

Why don't I see traces after editing my settings?

Check that the env block sets CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER=otlp, since tracing is off by default. Then restart Claude Code, because it reads the env block when a session starts. Run a prompt, exit with /exit so telemetry flushes, and check the time range filter on the Traces page.

Is the Claude Code cost in Traccia my Anthropic invoice?

No. Traccia shows the cost_usd that Claude Code reports on each api_request event, which Anthropic documents as an estimated cost. Use it to compare sessions, developers, and models. Your invoice from Anthropic remains the source of truth for what you pay.

Does Claude Code send my prompts and commands to Traccia?

Not by default. Prompt text and tool details such as Bash commands are redacted unless you set OTEL_LOG_USER_PROMPTS=1 and OTEL_LOG_TOOL_DETAILS=1. Without them, Traccia still receives sessions, models, token counts, timings, tool names, and errors.

Can Traccia block a command Claude Code is about to run?

No. Traccia records Claude Code sessions after the fact and can alert on them, but it does not sit in front of Claude Code's tool calls. To stop commands, use Claude Code's permission rules and hooks, and avoid --dangerously-skip-permissions.

How do I separate Claude Code usage by team?

Give each team its own Traccia workspace and ingestion key, and use distinct agent IDs such as claude-code-backend and claude-code-frontend in OTEL_RESOURCE_ATTRIBUTES.

References

External sources read on October 26, 2026.

See your Claude Code sessions in Traccia

Create an ingestion key, add one env block, and your next session shows up in seconds.

Read the setup guide