On November 5, 2026, a developer opened Claude Code issue #107757: Claude Code had been putting their account email address into the User-Agent header of requests it sent to third-party websites. The Hacker News thread reached 1,578 points and the issue collected 950 reactions in about a day and a half. The short answer for anyone running it: the model, not the HTTP client, wrote the address into its own curl calls, because Claude Code hands every signed-in session the account email in its context. Update to 2.1.317 or later today, and expect the real fix, removing the email from the context, in the next release.

The interim guard in 2.1.317 is a string match. We read its source, ran it against eleven command shapes, then tried it live on 2.1.319: a script written with the Write tool and run with python3 carried the address in a User-Agent straight past it. Whatever a harness puts in the model's context is one tool call away from the network, and only controls that run outside the model can stop it.

How an address in the context ended up in a User-Agent

No network library or telemetry header was involved. A July issue, #81138, documented a block that Claude Code adds to the model's context for signed-in users. According to the reporter, who decompiled the shipped bundle, it reads the address from oauthAccount.emailAddress in ~/.claude.json:

# userEmail
The user's email address is <account email>.

It is still there. On November 7 we asked Claude Code 2.1.319 in print mode, with tools off, whether its context held the user's email and to name only the domain. It answered yes and gave the right domain.

Lydia Hallie, who works on Claude Code, explained why it was added in a comment on the thread:

Users had reported Claude would hallucinate names for things like @author tags, since it didn't have context on the account. So we added the account email in the model's context for signed-in users. But when Claude wrote a command itself, like a curl call, it sometimes put your email address in the User-Agent as a contact address.

Some of the sites agents fetch from ask for a contact in exactly that header. The Wikimedia User-Agent policy says scripts "should use an informative User-Agent string with contact information, or they may be blocked without notice", and its example is CoolBot/0.0 (https://example.org/coolbot/; [email protected]) generic-library/0.0. Clients with a missing or generic User-Agent get an HTTP 403. The SEC's EDGAR access page asks for Sample Company Name AdminContact@<sample company domain>.com. A model trained on those conventions, told to fetch from one of these sites and holding an email address in its context, does what the site asks.

HTTP itself has the two halves of this argument written down. RFC 9110, section 10.1.2 says "A robotic user agent SHOULD send a valid From header field so that the person responsible for running the robot can be contacted". The same section also says "A user agent SHOULD NOT send a From header field without explicit configuration by the user, since that might conflict with the user's privacy interests". A coding agent is both at once: a robot making requests and a user agent acting for one person. The model followed the robot rule with the user's identity, without the explicit configuration the other rule asks for.

That makes this a harness bug more than a model bug. The AI harness decides what the model can see, and it chose to give a web-capable agent a real person's address to fix a cosmetic problem with @author tags. A placeholder name, or asking the user once, would have fixed the tags.

Three fixes, each further from the model's judgement

Anthropic has now tried three layers, and the changelog shows the order. Each one moves the control further away from the model's judgement.

WhenVersionChangeWho enforces it
Aug 17, 20262.1.234"Claude is now told to use your account email only to identify you, and not to send it to unrelated services unless you ask"The model, via its instructions
Nov 2, 20262.1.317"Added a check that asks before a web or shell tool call sends your account email address to the network" (the cc-plugin-privacy mod)A string match on tool input
Promised for "early next week"next releaseRemove the account email from the contextNothing to enforce: the data is gone

The August instruction failed first. It told the model the right rule in plain English, and the leak still reached Hacker News three months later. A system prompt line is a request, and a model weighing "identify yourself or get a 403" against "don't send the email to unrelated services" will sometimes decide the site is related. Only the third row removes the risk. The second covers the weeks in between, and the next two sections measure how much of it.

One version detail matters for teams pinning releases. The npm stable tag still points at 2.1.311, published October 26, which predates the mod. npm lists latest as 2.1.319. If your install follows the stable channel, you have the August instruction but not the check, so move to 2.1.317 or later as the maintainers advise.

What the privacy mod actually checks

Anthropic published the source of the mod on November 6. Its README is unusually frank. The mod registers one tool.check hook on the Bash, PowerShell and Monitor tools, and refuses a call whose input both holds the account address and names a User-Agent. In the README's words, "The test is two word matches over the whole input. There is no parser, on purpose", and "This is a courtesy, not a boundary, and it covers one shape."

The hook returns early unless the input contains @ or %40, then fetches the address from $.session.identity() and runs the two word matches. We copied the matcher, namesUserAgentWithAddress in hooks/outbound/names-user-agent.ts, and its input flattener textOf from commit 90380efca into a scratch folder and called the matcher directly under Bun 1.4.0, with [email protected] standing in for the account:

Command shapeExampleMatcher result
curl user-agent flagcurl -s -A "research-bot ([email protected])" https://en.wikipedia.org/w/api.phpRefused
Explicit headercurl -H 'User-Agent: MyTool/1.0 [email protected]' https://www.sec.gov/cgi-bin/browse-edgarRefused
Inline Pythonpython3 -c "import requests; requests.get('https://www.sec.gov/', headers={'User-Agent': 'Acme [email protected]'})"Refused
Percent-encoded oncecurl -A "bot jo.dev%40example.com" https://example.orgRefused
Quote-split addresscurl -A "bot jo.dev@""example.com" https://www.sec.gov/Refused
From headercurl -H "From: [email protected]" https://www.sec.gov/Passes
Query stringcurl "https://api.crossref.org/[email protected]"Passes
Value read at run timecurl -A "bot $(git config user.email)" https://www.sec.gov/Passes
Address built from a variableD=example.com; curl -A "bot jo.dev@$D" https://www.sec.gov/Passes
Base64 decoded inlinecurl -A "bot $(echo am8uZGV2QGV4YW1wbGUuY29t | base64 -d)" https://www.sec.gov/Passes
Script on diskpython3 fetch_filings.pyPasses

Every pass in that table is one the README already admits, and the table measures how wide the gap is. A matcher on tool input can only see the literal command, and an agent that writes code has endless ways to produce the same bytes at run time. The From row matters most, since RFC 9110 names that exact header as the contact field for robots. Note also what the hook never looks at: the README says "WebFetch and WebSearch are not checked, and neither are MCP tools", and each of those can carry a URL with an address in it.

What we got past it on 2.1.319

To check the shipped binary rather than the source, we ran four probes through claude -p on macOS (Darwin 27.0.0, arm64) with Claude Code 2.1.319, explicitly asking the model to use the account email. Every request targeted http://leak-test.invalid/, which never resolves, so the address could not leave the machine. The session's init event listed both cc-plugin-privacy and cc-plugin-sec-default as loaded.

ProbePublished README predicts2.1.319 did
echo 'User-Agent: test <EMAIL>' > /dev/nullRefuseRan
curl -s -m 3 -A "test <EMAIL>" http://leak-test.invalid/RefuseRefused
curl -s -m 3 -H "From: <EMAIL>" http://leak-test.invalid/PassRefused
Write a urllib script with the email in User-Agent, then python3 probe.pyPassRan, request attempted

Both curl refusals came back with the same text, which is not the refusal string in the published source:

Permission to use Bash denied by plugins cc-plugin-sec-default, cc-plugin-privacy: This call's input holds your account email address, which may leave this machine. If nobody asked to send it, go on without it.

The script probe is the one that matters. The Write tool created the file with no objection, and python3 probe.py printed ran-7 URLError: the request was built with the address in its header and failed only because the host does not exist. With a real host, the address would have gone out. Hallie had already flagged this case on November 6, saying the mod "still doesn't prevent all cases, like when Claude writes a script and runs on its own", and it still held on 2.1.319 a day later.

Our other rows show the running code does not match the published code. On 2.1.319 a From header was refused and a harmless echo was not, which is close to the opposite of what the README and register.ts describe. Two users in the issue reported that on 2.1.317 and 2.1.318 the mod did not refuse the cases they tried, an echo and a curl -A call to a .invalid host. The sources also disagree with each other about how it behaves: the changelog and Hallie say it "asks" before the call, while the README says "The refusal raises no prompt, whoever is there and whatever the permission mode." The README also lists a server-side switch among the things that turn it off. We cannot tell from outside which of these explains the drift, and our print-mode runs could not show a prompt even if one existed. Treat the mod as a moving target and test it on your own version before you rely on it.

Removing the email from the context is necessary, not sufficient

Taking userEmail out of the context closes this incident, but other identifying data stays within reach. The model sees far more than the account block before it reads your first message (covered in an earlier post on agent context costs): CLAUDE.md files, memory notes and environment details. Any of it that identifies you can be pasted into a request by a model trying to be a polite robot.

Git config is a live example. One commenter in the issue reported an SEC request where Claude "pulled my email address from my gitconfig and sent it without asking". That is a single report, and we did not reproduce it, but nothing about the mechanism requires the account block. If the model can run git config user.email, it can find an address, and the privacy mod does not cover that path because the value never appears in the tool input. Personal details you put in instruction files and memory sit in the same place.

The same goes for the tools the agent reaches through the Model Context Protocol. An MCP server that fetches URLs, posts to a webhook or files a ticket is an egress path the shell-only mod never sees. If you run agents with many connectors, those are the calls to watch.

What to do this week

Update first. Then add a check of your own that covers different tools from the built-in one, and put an egress boundary under it for the cases no string match can catch. The controls are not interchangeable:

ControlCoversMisses
Claude Code 2.1.317+ privacy modShell calls that name a User-Agent and hold the address (per its README)Scripts and run-time values; per its README also WebFetch and MCP tools (Anthropic says web tool calls are covered; we did not test this); behavior varies by version
Your own PreToolUse hookBash, Write, Edit, WebFetch and MCP inputs that contain the literal addressValues computed at run time, such as $(git config user.email)
Sandbox network allowlistEvery host a shell command or its child processes connect toWebFetch

A hook that also watches files and web tools

Claude Code hooks receive the tool call as JSON on stdin, and a PreToolUse hook that exits with code 2 blocks the call, with its stderr shown as the reason. This one reads the account address from the same file the context builder uses and refuses any matching tool input, in any case or percent-encoded once:

#!/usr/bin/env bash
email=$(jq -r '.oauthAccount.emailAddress // empty' ~/.claude.json | tr '[:upper:]' '[:lower:]')
[ -z "$email" ] && exit 0

input=$(jq -c '.tool_input' | tr '[:upper:]' '[:lower:]')
encoded=${email/@/%40}

if [[ "$input" == *"$email"* || "$input" == *"$encoded"* ]]; then
  echo "Blocked: this call contains the account email. Use a generic contact or ask the user first." >&2
  exit 2
fi

Save it as ~/.claude/hooks/block-account-email.sh, make it executable, and register it in ~/.claude/settings.json by its full path. MCP tools are named mcp__<server>__<tool>, so the mcp__.* pattern catches all of them. The built-in mod also watches PowerShell and Monitor, so Windows users should add those names to the matcher; we only tested on macOS.

The registration, with the path changed to your own home folder:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|WebFetch|mcp__.*",
        "hooks": [
          { "type": "command", "command": "/Users/you/.claude/hooks/block-account-email.sh" }
        ]
      }
    ]
  }
}

We ran it on bash 3.2.57 and jq 1.7.1. Piped test inputs for a Bash user-agent call, a Write with the address in upper case, and a WebFetch URL with %40 all exited 2; a clean user-agent and the git config form exited 0. Then we loaded it with claude --settings on 2.1.319 and repeated the script probe. This time the file was never written:

PreToolUse:Write hook error: [.../block-account-email.sh]: Blocked: this call contains the account email. Use a generic contact or ask the user first.

Checking Write and Edit is what closes the script path the built-in mod leaves open, because the address has to land in a file before a script can send it. It is still a string match. Run-time values and encodings beyond one pass of percent-encoding get through, same as the table above, and you should add your git email or full name to the check if those matter to you.

An egress allowlist for what hooks cannot see

The control that does not care how the address was spelled is the network. The Claude Code sandbox routes connections through a local proxy that "checks each host against your allowed domains, which start empty", and the docs say the commands Claude runs "start inside its boundary, and so do the processes they start". By the docs, that would include a script like probe.py, though we did not test it. Adapted from the examples on that page:

{
  "sandbox": {
    "enabled": true,
    "network": {
      "allowedDomains": ["*.github.com", "registry.npmjs.org"],
      "strictAllowlist": true
    }
  }
}

Sandboxing is off by default, which is why enabled is there. Without strictAllowlist, a new host triggers an approval prompt in the default modes; with it, Claude Code "denies sandboxed commands access to any host outside the allowlist instead of prompting". The docs say that key only counts in user, managed or --settings settings, so put it in ~/.claude/settings.json rather than a repository file. Two caveats from the same page: allowedDomains does not limit WebFetch, which your hook and permission rules have to cover, and in bypassPermissions mode new hosts are allowed without a prompt. If you run unattended pipelines like the one in our four-agent Claude Code team guide, add a strict allowlist before anything else.

A rule in the prompt and a matcher on the command line both depend on how the model writes the call; an allowlist decides in advance where calls may go. That is the same default-deny move npm v12 made for install scripts in July.

What to watch

Watch the next Claude Code release, which should drop userEmail from the context. Hallie wrote "I'll share an update in this thread once it's out" in the HN thread, and #107757 is still open. When it lands, rerun the domain-only check from the first section on your own machine. The open question is the one this incident raises for every agent vendor: which other personal fields their harnesses put in front of a model that can open a socket, and whether anyone has asked the model what it does with them.