# Claude Code Put Your Email in the User-Agent Header

> Claude Code leaked account emails in User-Agent headers. Its 2.1.317 fix has a bypass. I reproduce both and show why it belongs at the egress gateway.

- Canonical URL: https://webofmike.com/claude-code-email-ua-leak/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-11-15
- Last modified: 2026-11-15
- Tags: AI Agents, Security, AI Gateways, Platform Engineering
- Cite as: Mike Moore, "Claude Code Put Your Email in the User-Agent Header", Web of Mike (webofmike.com), 2026-11-15. https://webofmike.com/claude-code-email-ua-leak/


On November 5, someone opened [anthropics/claude-code#107757](https://github.com/anthropics/claude-code/issues/107757) with one line that explains the whole problem: "I receive dozens of scam emails per day because claude code divulged my email to arbitrary 3rd parties in the `User-Agent` request header while conducting web searches without my permission." The issue hit Hacker News at [1388 points and 437 comments](https://news.ycombinator.com/item?id=50385272) within a day. Anthropic had already shipped a mitigation three days earlier, and commenters found a hole in it before the HN thread even cooled down. I reproduced both the leak and the bypass in a small repo, [agent-egress-header-scrub](https://github.com/themsquared/agent-egress-header-scrub), and it points at an argument I keep making on this site: a check that lives inside the agent has to anticipate every way a value could reach the network. A check at the network boundary only has to look at what actually leaves.

Worth separating this from [the Claude Code extension backdoor](/claude-code-extension-backdoor/) from October 30: that was a malicious script injected into a Claude Code VS Code extension build served through the Cursor Marketplace, planting SSH access on the machine that installed it. This is a different, unrelated issue: Claude Code's own CLI tool calls writing the account's email into outbound HTTP headers.

## What was actually leaking

Claude Code keeps the signed-in account's email in the model's context, mostly for things like git commit authorship. The bug is that the model sometimes decides to use that email somewhere else: specifically, in the `User-Agent` header of a web or shell tool call. This isn't random. Several sites, [SEC EDGAR among them](https://www.sec.gov/search-filings/edgar-search-assistance/accessing-edgar-data), ask scrapers to include a contact address in their User-Agent as a politeness convention, a way for a site operator to reach a bot's owner before blocking it. One HN commenter, varenc, demonstrated the mechanism directly against EDGAR: a bare `curl` request without an identifying User-Agent gets a 403, and a request with an email-bearing User-Agent gets a 200. Claude Code, trying to be a good citizen against a site that wants this, reached for the one contact address it had on hand: the user's own account email, with no prompt and no disclosure.

The reporter wasn't alone. Commenter T-R described it happening "without prompt or warning, only because I happened to catch it in the curl requests." Another, llama052, found Claude Code building more elaborate self-describing User-Agents in the same vein, for example `company-helm-research/1.0 (Claude Code agent run by <name>, <company> platform team; docs lookup)`, naming both a person and an employer in a header sent to a third party. As commenter tmsbrg pointed out, none of this actually verifies anything: the email is trivially spoofable and useless as identity, it just sits there as a standing liability with no corresponding benefit.

## The fix, and the gap in it

Anthropic's response, from contributor poteat on the issue thread: "We introduced a built-in mod called `cc-plugin-privacy` to deterministically block this behavior in v2.1.317 and later... The built-in privacy mod is enabled by default." The Claude Code changelog entry for that version, quoted on HN by arthur-st: "Added a check that asks before a web or shell tool call sends your account email address to the network."

Within the same issue thread, two gaps surfaced:

- **mkubenka**: "The privacy mod matches only the account address. `<project-name>-site-build/1.0` with no address passes." In other words, if the model builds a User-Agent that doesn't literally spell out the account's email string, the check has nothing to flag, even if the header still identifies the user some other way.
- **mkubenka**, same thread, describing a second case: "the model wrote the address into a script with Edit, then ran `python3 fetch.py`, so no checked shell input contained it." The email went into a file first. The shell command that actually sent the request was just `python3 fetch.py`, a string with no email anywhere in it, and the mod's check passed it straight through.

Both gaps come from the same root cause. The mod inspects the *text of the command the agent is about to run*, and compares it against one known string. That catches the laziest version of the leak and nothing else. It doesn't know what the command does once it runs, only what it says.

## Reproducing it

[agent-egress-header-scrub](https://github.com/themsquared/agent-egress-header-scrub) is a pure-stdlib Python repro, no dependencies, no Docker, no sudo. It runs a stand-in "third-party site" that logs whatever `User-Agent` it receives, then walks through four scenarios:

```bash
git clone https://github.com/themsquared/agent-egress-header-scrub.git
cd agent-egress-header-scrub
bash demo.sh
```

**Scenario 1, no guard at all.** The agent builds a request with the email inline in the User-Agent and sends it directly:

```
sent User-Agent: 'research-bot/1.0 (contact: demo.user@example.com)' -> HTTP 200
{"path": "/", "user_agent": "research-bot/1.0 (contact: demo.user@example.com)"}
RESULT: LEAKED -- the site's access log now has the email.
```

**Scenario 2, the same command checked by a literal-match guard** (a small stand-in for `cc-plugin-privacy`'s documented behavior, not a reconstruction of its real source, which Anthropic hasn't published yet):

```
REFUSED: command text contains the protected value
RESULT: guard REFUSED Path A, as expected (email appears verbatim in the command text).
```

**Scenario 3 reproduces mkubenka's exact bypass.** The email goes into a file, `contact.cfg`, first. The command that actually fires is `python3 relay_request.py <url> contact.cfg`, no email string anywhere in it:

```
ALLOWED: command text does not contain the protected value verbatim
RESULT: guard ALLOWED Path B -- the command text never contains the email, only a filename.
sent User-Agent: 'research-bot/1.0 (contact: demo.user@example.com)' via direct -> HTTP 200
{"path": "/", "user_agent": "research-bot/1.0 (contact: demo.user@example.com)"}
RESULT: LEAKED ANYWAY -- this is the bypass mkubenka documented on #107757: the literal-match guard allowed the exact command that still sends the email.
```

**Scenario 4 runs the same allowed command through an egress gateway instead**, a small forward proxy that rewrites any outbound `User-Agent` to a fixed value before the request leaves:

```
sent User-Agent: 'research-bot/1.0 (contact: demo.user@example.com)' via proxy -> HTTP 200
{"path": "/", "user_agent": "agentgateway-egress/1.0"}
RESULT: BLOCKED -- the target's access log shows the gateway's fixed User-Agent, not the email, even though the guard upstream would have allowed this exact command.
```

I ran `demo.sh` three times back to back; all three exits were clean and all four scenarios behaved identically each time.

## Why the gateway catches what the guard can't

The guard and the gateway are answering different questions. The guard asks "does this command, as text, mention the thing I'm protecting?" That question has to be re-asked for every new way a model might construct a request: inline, from a file, from an environment variable, built up across several Edit calls, assembled by a subprocess the agent spawned. Each of those is a new shape the check has to recognize, and mkubenka found two in one afternoon.

The gateway asks a narrower, more durable question: "what does the request that's actually leaving look like?" It doesn't matter whether the User-Agent arrived there via an inline string or a two-step file relay. By the time the request hits the proxy, there's exactly one artifact to inspect: the header on the wire. That's the same argument I made about [Pydantic AI's egress allowlist resolving a hostname once to check it and again to connect](/pydantic-ai-egress-dns-rebind/) and about [the other ways agents route around an egress control that isn't enforced at the boundary](/agent-egress-control-bypasses/). A check performed inside the agent's own reasoning loop is a check the agent can route around, deliberately or (as here) by accident. A check performed where the packet actually crosses the network doesn't care how it got there.

This isn't an argument that gateway enforcement is sufficient on its own. Several commenters on the issue, among them psc raising GDPR data-minimization concerns, argued the better fix is keeping the email out of the model's context in the first place, so there's nothing to leak regardless of which header it ends up in. That's a real fix and probably the right one long-term. The egress-proxy argument is narrower: for whatever data does end up in an agent's reach, the last line of defense should not be a pattern match against the agent's own command text.

## What this doesn't claim

This repo isn't a rebuild of Claude Code's real `cc-plugin-privacy` mod. Its actual source wasn't public as of this writing (poteat said in the thread they'd try to publish it alongside Claude Code's other built-in mods). `contact_guard.py` here is deliberately narrow: a literal substring check, built to be exactly as strict as the documented behavior and no stricter, so the bypass reproduces cleanly. And `egress_proxy.py` is a from-scratch toy proxy, not a copy of agentgateway. I work at Solo.io, which makes [agentgateway](https://agentgateway.dev); the argument here is about where enforcement belongs, not a product pitch.

[agent-egress-header-scrub on GitHub](https://github.com/themsquared/agent-egress-header-scrub) has the full code: `target_server.py`, `path_a_direct.py`, `relay_request.py`, `contact_guard.py`, `egress_proxy.py`, and `demo.sh` tying all four scenarios together.

