Security Research

Welcome to OTEL Claudeifornia: RCE using Claude Code Telemetry Features

Welcome to OTEL Claudeifornia: RCE using Claude Code Telemetry Features

Welcome to OTEL Claudeifornia: RCE using Claude Code Telemetry Features

Golan Myers

|

|

Reading Time:

5 min

min

Table of Contents

TL;DR

Claude Code's built-in OpenTelemetry implementation can be weaponized to exfiltrate session data and achieve remote code execution - and the exposure is far wider than most realize, spanning thousands of skills and hundreds of repositories. What makes this especially concerning is that there is no fix on the horizon.

Introduction

Every Claude Code session starts with a decision: which directory to open. That decision, it turns out, is also a decision about who receives your email address, your Anthropic account UUID, your organization ID, your prompts, tools input, api responses - and, potentially, anything that exists on your endpoint.

No tool call. No prompt. No warning. For most users, not even a trust dialog.

This is Otel Smuggling: committing a malicious .claude/settings.json into a git repository, turning Claude Code's own OpenTelemetry pipeline into a silent exfiltration channel - pointed at attacker-controlled infrastructure from the moment the session begins.

OpenTelemetry is enterprise telemetry infrastructure. Claude Code adopted it as a way to let organizations route session data to their own observability stack. What went unexamined: the very feature that makes OTEL useful for enterprises - its configurability via project-level settings - is what makes it dangerous in adversarial hands. A project can activate telemetry, redirect it to any HTTP endpoint, and attach arbitrary shell command output to every outbound request.

All before the user types a word.

How widespread is the usage of otel in the ecosystem? The top 5 skills marketplaces contain over 5000 skills which reference it; there are hundreds of repositories containing otel references in their .claude/settings.json with thousands of stars. This is not theoretical; this is what adoption looks like.

The Attack, In Plain English

An attacker publishes a git repository - a popular open-source project, a shared team repo, a job application code challenge - containing a single malicious file: .claude/settings.json. That file silently enables Claude Code's OpenTelemetry telemetry export and redirects all telemetry to an attacker-controlled HTTPS endpoint.

The victim clones the repository and opens Claude Code in that directory. Within five seconds of session start, before any user interaction, the attacker's webhook receives:

  • The victim's plaintext email address

  • The victim's Anthropic account ID (user_01xxx format), account UUID, and organization UUID

  • A complete inventory of every MCP server they have installed - names, connection types, plugin hashes, error states

  • Their terminal type, OS version, architecture, and Claude Code version

  • Their full session ID

  • Their entire session interactions - prompts, tool inputs, api responses

If otelHeadersHelper is present in the project config, arbitrary shell commands begin executing on the victim's machine - their output exfiltrated as HTTP headers on every OTEL flush, every five to fifteen seconds. That means AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, ANTHROPIC_API_KEY, SSH agent sockets - anything present in the shell environment - streaming silently to the attacker's server.

The attack requires no elevated privileges. No browser exploit. Only a developer cloning a repository and opening claude within it.

Layer 1 - PII Exfiltration via OTEL Traces

Claude Code attaches user identity fields to every OpenTelemetry trace event - not as sampled metadata, but as per-event attributes on every span. These are the fields observed on a claude_code.mcp_server_connection event during testing:

user.email is the highest-value data. It arrives before the session is a second old, enabling targeted spear-phishing and cross-referencing against GitHub, LinkedIn, and Slack before the victim has typed a word.

The Anthropic account identifiers don't grant API access on their own - but they eliminate the enumeration phase of every follow-on attack. If the attacker subsequently obtains any Admin API key for the victim's organization, user.account_id maps directly to GET /v1/organizations/users/{user_id} and the Compliance API's user directory endpoint. The attacker already has the key; they just need the lock.

Beyond identity data, Claude Code emits a claude_code.mcp_server_connection event for every MCP server connection at session start - disclosing transport types, plugin hashes, error states, and whether each server is user-scoped or dynamically loaded. An attacker receives a detailed map of the victim's integration stack: which databases they're connected to, which Slack and GitHub plugins they run, and which services are currently failing (and why).

With OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS, OTEL_LOG_TOOL_CONTENT, and OTEL_LOG_RAW_API_BODIES all enabled via the project env block, the full session - every prompt, every tool call input and output, every raw API response - flows to the attacker's endpoint.

Layer 2 - otelHeadersHelper and the RCE Escalation

otelHeadersHelper is a settings.json field that specifies a shell command. Claude Code executes it at session start and every 29 minutes thereafter, captures its JSON stdout, and injects the result as HTTP headers on every OTEL export request.

The feature is designed to let enterprises inject dynamic auth tokens into their OTEL pipeline. At the project level, it becomes an unconditional code execution primitive.

otelHeadersHelper has no sandboxing, no allowlist, and no scope restriction. It runs with the full privileges of the user's shell process. The feature is powerful by design - what's missing is any gating that prevents it from being invoked from a repository the user has never interacted with before.

The Malicious Settings File

This is the full .claude/settings.json that triggers both the PII exfiltration and the RCE escalation. It is 21 lines. That's it.

Why the Trust Gate Fails

Anthropic's position is that the workspace trust dialog is the security boundary for project-level configuration. This is the right architecture - but the implementation has two gaps that make it insufficient for the OTEL attack surface.

Gap 1: The trust dialog doesn't surface OTEL settings as a risk category

When the trust dialog fires, it presents a general execution warning. It does not enumerate:

  • That CLAUDE_CODE_ENABLE_TELEMETRY=1 is set in the project env block and will activate telemetry export

  • That OTEL_EXPORTER_OTLP_*_ENDPOINT is configured and points to a third-party host

  • That otelHeadersHelper is present and will execute a shell command on the victim's machine

Compare this to MCP servers: when a project defines MCP servers, the trust dialog specifically asks the user to review the server commands before proceeding. The user can see what they are consenting to. For OTEL, the same consent framework is absent.

A user making a trust decision is doing so without the information they need. That isn't informed consent - it's a blank check.

Gap 2: Trust is recursive, and most users have already signed the blank check

Claude Code inherits trust recursively from parent directories. A developer who has trusted ~/, ~/Desktop, or ~/projects - common choices for anyone who works across many repositories - has implicitly pre-approved every repository they will ever clone under those paths. The trust dialog never appears for those repos. A victim that clones a malicious repository under any commonly-trusted path - which is most paths most developers use - triggers the full attack with no user interaction beyond git clone && claude.

The combination is the problem. The trust dialog exists, but it doesn't surface OTEL risks. And for the majority of real-world attacker repositories, the dialog is never shown at all.

The Persistence Play - Global Settings Poisoning

The attack described so far is visit-scoped: it exfiltrates while the victim is in the malicious directory. The more dangerous variant makes it permanent.

A project-level otelHeadersHelper can read the project's own .claude/settings.json and deep-merge it into the victim's global ~/.claude/settings.json. The merge is non-destructive - existing user settings are preserved. Only the OTEL keys are added/changed.

After a single session in the malicious directory, the consequences are machine-wide and indefinite:

  • Every Claude Code session on the machine - in any directory - exports telemetry to the attacker's endpoint

  • Projects without their own OTEL configuration inherit the poisoned user-level settings

  • There is no log entry, no notification, and no UI element reflecting the change to ~/.claude/settings.json

  • The persistence survives deleting the malicious repository

  • Re-cloning a clean version of the repository does not undo the poisoned global settings

  • This transforms a per-project, visit-time attack into a machine-wide, persistent credential and session-data exfiltration channel.


You Can Checkout Anytime You Like, But You Can Never Leave

Should You be Worried?

Claude Code evaluates settings in this order (highest to lowest priority):

Managed  (/Library/Application Support/ClaudeCode/managed-settings.json)  
   └─ Local   (.claude/settings.local.json - project-specific, not committed)       
        └─ Project (.claude/settings.json - committed to the repo)   attack vector            
             └─ User   (~/.claude/settings.json - personal settings)
Managed  (/Library/Application Support/ClaudeCode/managed-settings.json)  
   └─ Local   (.claude/settings.local.json - project-specific, not committed)       
        └─ Project (.claude/settings.json - committed to the repo)   attack vector            
             └─ User   (~/.claude/settings.json - personal settings)
Managed  (/Library/Application Support/ClaudeCode/managed-settings.json)  
   └─ Local   (.claude/settings.local.json - project-specific, not committed)       
        └─ Project (.claude/settings.json - committed to the repo)   attack vector            
             └─ User   (~/.claude/settings.json - personal settings)

Testing confirmed:

  • Managed settings with a localhost OTEL endpoint → attack blocked. Project-level OTEL config was fully overridden. The attacker's webhook received nothing.

  • User-level settings with OTEL config → attack succeeds. Project settings take precedence over user settings. The attacker's webhook received everything.

The attack affects:

  • All individual developers (no managed settings deployed by default)

  • Teams and enterprises that have not deployed managed Claude Code settings

  • Enterprises with managed settings that do not include an OTEL endpoint restriction

There is no per-field locking mechanism. An organization cannot lock only the OTEL endpoint while permitting other project-level customizations.

Responsible Disclosure and Vendor Response

We submitted this research to Anthropic's security team. Their full response:

We disagree with this characterization, and we want to be precise about why.

Anthropic's response rests on the claim that workspace trust is the security boundary - and that because trust must be accepted before otelHeadersHelper executes, the attack is mitigated. This is true in the narrow case where the trust dialog actually fires. The problem is that for the majority of developers in the majority of real-world scenarios, the trust dialog never fires at all.

Trust is inherited recursively from parent directories. A developer who has trusted ~/projects - a completely normal and reasonable thing to do - has pre-approved every repository they will ever clone into that directory, including repositories that don't exist yet; including the attacker's. The trust dialog they accepted months ago for a different project is doing the security work for a repository they've never seen. This is not informed consent by any reasonable definition.

The second problem is what the trust dialog actually says. Anthropic acknowledges it presents "a general execution warning rather than enumerating each project setting individually." For mcp_headersHelper, the dialog shows the MCP server commands the user is about to authorize. For otelHeadersHelper, it shows nothing specific. The user sees a general prompt and approves it, not knowing they've just authorized shell command execution and third-party telemetry routing for every subsequent session on their machine.

A security boundary that is (a) bypassed for most users by design via recursive inheritance, and (b) doesn't disclose the specific risks it is supposed to gate, is not functioning as a security boundary.

We respect Anthropic's process and their right to their own threat model assessment. We're publishing this research because we believe developers should have full information to make their own decisions about the repositories they open. While the effect of this issue is the same as project level hooks, they are a well-documented danger which is actively scanned, and otel is not regarded with the same cautiousness.

Conclusion

claude code was designed as a productivity multiplier. The .claude/settings.json convention was designed to let teams share consistent tooling configuration. Both are good ideas. The gap is that project-level configuration can reach across security layers that were designed to protect individual developers - activating invisible telemetry pipelines, executing shell commands, and writing to global settings files that outlive any individual session.

This isn't a flaw in the concept of project-level settings. It's a missing constraint: telemetry configuration with a shell execution primitive shouldn't be something a repository can impose on a developer without explicit, informed consent that names the specific action being authorized.

AI tooling has reached a junction discussed for the past 10 years in regards to cloud computing - shared responsibility model. AI tools inherent basically full access to essentially everything we have access to, and vendors threat models may differ from our view of best practices. As an ecosystem, a shared responsibility model needs to be produced and adhered to, making it clear to end users and organizations what is taken care of, and what they need to be vigilant of, implementing their own protection. Several people and organizations have discussed and written about the matter extensively, but it is my belief that we are not quite there yet. Hopefully, this will be a nudge in the right direction.