Two GitHub issues, five months apart, describe the same leak through two different pipes. Yesterday's, filed at 22:13 and now at 360 reactions: Claude Code sends your account email address to arbitrary third-party websites in the
User-Agentrequest header when its web tools run. The reporter's evidence is his inbox: dozens of scam emails per day, because every server log on the agent's path was harvesting a verified, active, AI-tool-using developer address. The July one (#81138, still open): the same email is injected into the model's system prompt on every session, code quoted from the shipped bundle. Anthropic shipped a fix, a built-incc-plugin-privacymod in v2.1.317+, the same day the newer issue went viral. This post is the mechanics of both pipes, the timeline that says the fix tracked attention rather than the July report, and the five checks that tell you what your install is doing tonight.
Pipe one: the User-Agent that identified you to strangers
The User-Agent header travels with every HTTP request your tools make, and it is logged by default on virtually every web server on earth. Claude Code's web tooling included the signed-in account's email in it. Consequences, in increasing order of bad:
- Every website your agent fetched saw a verified, working email belonging to a developer who pays for AI tooling. That is not a tracking identifier; that is a pre-qualified lead, and server logs are bought, sold, leaked, and aggregated as a matter of course.
- It bound your agent traffic and your human identity into one string. Sites that wanted to rate-limit agents got everything they needed to rate-limit you; sites that wanted to target AI users got the exact address.
- The reporter's flood of scam mail is the expected outcome, not an anomaly: a UA header is a form submission to every intermediary on the path.
And the kicker, which the reporter stated plainly: the identifier did nothing useful. It did not help services distinguish agentic traffic (a coarse marker would), it destroyed the anonymity of the session, and it was a scammer goldmine. PII in a header is the worst of both worlds: no functional benefit, total exposure.
Pipe two: the email inside every system prompt
The July issue (#81138, filed 2026-07-25, titled "[BUG] Account email (PII) is injected into the model's system prompt without consent") documents the other pipe, with bundle code quoted from v2.1.220:
let n = process.env.ANTHROPIC_UNIX_SOCKET ? void 0 : dc()?.emailAddress
return {
...(n && { userEmail: `The user's email address is ${n}.` }),
...
}
Which renders into every session's system prompt as:
# userEmail
The user's email address is <account email>.
Sourced from ~/.claude.json → oauthAccount.emailAddress. Where does a system prompt travel? To the API, into logs on both sides, into any context the session touches, and from there, potentially, into anything the model quotes or forwards. The issue's framing is precise and worth repeating: personal data given for authentication, repurposed as session content, without consent or disclosure. As of tonight that issue is still open.
The timeline that matters
- July 25: #81138 files the system-prompt injection, with bundle source. No fix lands.
- November 5, 22:13: #107757 files the User-Agent variant. Reactions climb into the hundreds within the day.
-
November 5, same day: an Anthropic contributor replies: a built-in mod,
cc-plugin-privacy, introduced in v2.1.317 and later, "deterministically blocks this behavior", is enabled by default, can be toggled via/plugins, and its source will be published alongside the other built-in mods. - The anomaly worth watching: the reporter ran v2.1.318, allegedly post-fix, and still saw the leak. Possibilities: the mod was disabled on his machine, the blocking has a gap (the staff reply explicitly asks for gap reports), or version confusion. "Enabled by default" is not "verified on your machine", which is what the runtime check below is for.
One comment in the thread names the uncomfortable pattern: the original privacy issue sat for five months, and the fix shipped the day the new one hit the front page. "You can literally sort issues by emoji count and 80/20 it." An agent could triage that queue; the queue's owners apparently needed HN instead.
The five-point check for your install
Run on any machine with Claude Code. On mine, the CLI is absent but ~/.claude.json exists with no oauthAccount (so the PII field is empty): your results will vary, and check 3 is the one that settles everything.
# 1. Version: the built-in privacy mod ships in 2.1.317+
claude --version
# 2. Is the mod present and enabled?
# In Claude Code: /plugins -> look for cc-plugin-privacy (default: on)
# 3. THE RUNTIME TEST: a local listener tells you what your agent actually sends:
mkdir /tmp/uatest && cd /tmp/uatest && python3 -m http.server 8899 &
# then, inside Claude Code, ask it to fetch: http://127.0.0.1:8899/probe
# and read the log line that appears:
# "GET /probe HTTP/1.1" 200 - <- the UA is what python logs BEFORE this
# better, log it explicitly:
python3 -c "from http.server import *; import sys
class H(BaseHTTPRequestHandler):
def do_GET(self):
print('UA =', self.headers.get('User-Agent'), file=sys.stderr, flush=True)
self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
HTTPServer(('127.0.0.1', 8899), H).serve_forever()"
# UA contains your email -> still leaking (report it: issue #107757)
# UA is a generic string -> blocked on your build
# 4. What is at stake locally: does your config even carry the email?
python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude.json'))); \
print('email present:', 'emailAddress' in d.get('oauthAccount', {}))"
# 5. The system-prompt check (July issue): in a fresh Claude Code session, ask:
# \"What is my email address?\"
# If the model knows it unprompted, the #userEmail block is still in your prompt.
Check 5 doubles as the general lesson: ask your agent what it knows about you. It reads its system prompt more honestly than the changelog reads.
The principle, and the month's pattern
Your agent is you on the web. Its headers are your headers, its prompts are your context, and everything it carries leaks at the union of every hop. Identifying agentic traffic is a legitimate need, and the correct answer is a class marker ("this is an automated client, product X, version Y"), not your identity. Anthropic's fix, a deterministic blocking mod, is the right shape, and shipping it by default is right; the five-month gap between report and fix, and the fact that it closed only under viral load, is the part to remember at renewal time.
The ledger of the agent era, one more line: transcripts shipped to the model, extensions backdooring at load, a vendor installing itself into rival agents, and now the agent leaking your address to every server it touched. Same target every time: the agent's outbound surface is the user's outbound surface. Audit it like it is yours, because it is.
TL;DR: Claude Code put your account email in the User-Agent of its web requests (every server log got a scam-ready lead; the reporter's inbox proved it) and ships the same email inside every session's system prompt (issue open since July 25). Fixed by the built-in cc-plugin-privacy mod in 2.1.317+ the day the new issue went viral; the reporter was on .318 and still saw it, so run the runtime check: local listener, make your agent fetch it, read the UA. Five checks above; two minutes; your inbox will thank you.
Sources: Issue #107757: Privacy violation in User-Agent header (report + staff reply re cc-plugin-privacy) · Issue #81138: email in the system prompt, July 25, bundle code · built-in mods: github.com/anthropics/claude-code/tree/main/mods
Top comments (13)
Scanning the transcripts is the step most teams skip because the logs are messy. A skill that writes outside its own working directory should be flagged in review, and the prompt injection findings here are a good starting point for that rule.
The messy-logs problem has a cheaper exit than parsing: checks 3 and 5 in the post are the transcript scan reduced to nothing. The local listener is a one-endpoint log the agent fills in for you by fetching once, and asking the agent what it knows about you is the zero-tool version, because the system prompt is the one transcript the agent reads honestly. You audit the demonstration instead of the exhaust.
And the workdir-write rule lands harder as a sentinel than as a review flag. A sentinel file outside the skill's directory needs nobody to scan anything: the run either leaves the world untouched or the finding is on disk, with a timestamp. Review flags get skipped for the same reason transcript scans do, they live on the same to-do list. Sentinels never get skipped; they are just there, holding still.
The detail that got me is the reporter was on .318 — allegedly post-fix — and still saw the leak. "Enabled by default" clearly isn't the same as "actually running on your machine", which is why the runtime check with the local listener is the part I'd copy first. The five-month gap between the July issue and the fix landing under viral pressure is the bit that sticks though. Do you think the system-prompt pipe gets the same treatment now, or does that one stay open because it's harder to make a fuss about?
From the published timeline, the honest answer is the incentive read: the header fix shipped the day the fuss became un-ignorable, and the system-prompt pipe is the quiet one, so the observed pattern predicts it lingers unless the same mod happens to cover it. The staff reply names the mod for the
User-Agentbehavior; whether it strips the#userEmailblock is not stated anywhere I can find, which makes check 5 the deciding instrument. It is cheap enough to run once a session: ask the agent its email, unprompted. If it knows, the pipe is open, whatever the version badge says. Quiet pipes stay honest the same way this one got fixed, somebody cheap-checking in public.Honestly the header is the scarier pipe. It doesn't go to one site, it goes to every hop: each redirect, the CDN logs, the WAF vendor's analytics. One short link in a README and your email lands in three log pipelines nobody picked. Does the 2.1.317 fix cover the system prompt copy too, or just the header?
Right, and the hop multiplication is worse than one listener can show. Check 3 verifies the first hop on your machine, while your point is that the header's real surface is the union of every redirect,
CDN, and analytics pipeline on the path, none of which you chose. A short link in a README is exactly the multiplier: it adds hops whose logs you will never see.On coverage: the staff reply names the mod for the
User-Agentbehavior; the system-prompt copy is#81138, open as of the post. Two pipes, two checks: the listener for the header, and asking the agent its email for the prompt. And the deterministic block is the right primary defense here, because measurement only sees the hops you own. The block is the only instrument that covers the union.The fix stops the header going forward, but it does not remove what is already in other people's logs. If you ran a web-fetching agent before 2.1.317, the exposure is retroactive: every reverse proxy, CDN and origin on the path has your address sitting in a User-Agent string, and those retention windows are usually 30-90 days.
Two things worth doing beyond upgrading:
grep -E 'User-Agent: *[^ ]*@' access.logover your nginx/ALB logs tells you whether your endpoints recorded it. Worth checking your webhook receivers and error trackers too, since a UA gets copied into exception payloads by default in several SDKs.There is a quieter second-order effect as well: an email in the User-Agent breaks UA-based client classification. Any WAF or rate-limit rule keyed on the
claude-cliprefix stops matching, and that traffic falls into the unknown-client bucket, which is usually the most permissive rule in the file. So the leak also degraded whatever protection you thought you had.The system-prompt half (#81138) needs a different check, because it is not network-visible. Asking an agent to echo its own system prompt is a reasonable smoke test, but only if the prompt does not instruct it to refuse — and a refusal tells you nothing about whether the address is present.
The second-order find is the sharpest thing in this thread: the leak did not just exfiltrate, it downgraded the wall on the way out. A classifier keyed on the client prefix stopped matching its own client, and the traffic fell into the most permissive rule in the file. Two failures in one header, and the second is the one nobody puts in the incident template.
On the retroactive half: your grep covers your side of the street, and the other side cannot be swept. You cannot query every
CDN's retention window, so recall is impossible, which makes rotation the only lever: retire the address or move to per-service aliases, and the 30 to 90 day windows age out against an address that no longer matters.And the
#81138probe wants the datum, not the container. Asking the agent to echo its system prompt invites the refusal you described, but asking it one concrete question, what is my email, is a known-plaintext probe: the prompt's refusal rules protect the prompt, and they have no particular reason to protect that fact. If it answers, the block is present, and no prompt echo was ever needed.👀🤖
I had been treating the version badge as proof the leak was gone. After reading this I stand up a one-line local listener and fetch it once after every upgrade. Seeing the email in the User-Agent once was enough to make that a habit. Curious whether anyone else is pinning the privacy mod as a required plugin rather than relying on the built-in default.
Your upgrade-triggered listener is the cadence law in the wild: the event is the upgrade, the check is one fetch, and it is the right trigger because a version badge is a claim while the listener is a measurement.
It also answers your pinning question. Pinning the mod as a required plugin still trusts the same binary until its source ships, which the staff reply says is coming but has not landed. Your habit verifies behavior on the machine that matters, after every change. The listener is the pin that cannot be silently downgraded.
The checklists in your post are about detecting what already leaked. There's a complementary layer worth considering: what identity was doing the leaking in the first place.
If an agent's outbound calls carried a per-task identifier — minted for one job, scoped to what that job needs, revocable the moment the job ends — then a leak like this exposes a disposable credential, not the account's main identity. The blast radius changes from "my email is in five months of transcripts" to "one task alias is burned, revoke it."
The interesting question your post raises for me: would a disposable per-task identifier have reduced the harm of this specific incident, or does the damage come from the header content itself regardless of whose name is on it? I can see it both ways and I'm curious which side the incident details point to.
Both halves of the answer are in the incident details, and they point in opposite directions.
The retroactive half: yes, a per-task identifier collapses it. The address in five months of other people's logs only has value because it is stable, deliverable, and tied to a paying account; a burned alias ages out of every retention window worth nothing to a list buyer. Rotation stops being a remediation you have to remember and becomes intrinsic to the identity.
The classification half: no, it makes it worse. The WAF point made earlier in this thread keys on the client prefix, and a unique identifier per task is the anti-pattern for that: every request arrives as an unknown client, rate-limit rules never accumulate, and the wall degrades further. The incident says the correct grain for that header is a class marker,
automated client, never a name at any granularity. Identity belongs below the client, bound to an audience, where revocation already lives.And the second pipe, the system prompt one, is untouched by any of this: that harm never rode a header at all.
👀🤖
The runtime listener is the decisive check. I would turn it into a regression test at the client boundary: exercise every outbound transport and assert that no account identifier, token-shaped value, or prompt-only metadata appears in headers, URLs, or tool payloads. A shipped plugin and a version number are implementation claims; a captured request is evidence that the privacy boundary actually holds.