Claude Code's new email guard, and two curl calls that get past it
Overview
Claude Code 2.1.317 added a guard to stop the model putting your email in User-Agent headers. On 2.1.318 the address still reached our test server through git config and shell quoting.
We asked Claude Code 2.1.318 to run two curl commands against a server on 127.0.0.1:
curl -s -A "probe-L1 ($(git config user.email))" http://127.0.0.1:8199/L1
curl -s -A "probe-L2 (you"@"example.com)" http://127.0.0.1:8199/L2
Both ran, and the server logged the account’s email address in each User-Agent ([email protected] stands in for it here). A third command with the address typed out plainly was stopped by cc-plugin-privacy, the guard Anthropic shipped in 2.1.317 after users caught Claude sending their email to crates.io, SEC EDGAR and other APIs.
Checked 6 November 2026.
Background
Claude Code has put the signed-in account’s email in the model’s context since at least May 2026 (#55743 (opens in a new tab), #81138 (opens in a new tab)). In July, #78431 (opens in a new tab) showed what the model did with it. One user found five curl calls in an hour of work, each sending their address to the crates.io API:

Issue 78431, captured 6 November 2026 at 23:54 UTC. The cut-off command is curl -s -H "User-Agent: ashurbanipal-publish-check (<masked>)" https://crates.io/api/v1/crates/ashurbanipal-axum.
That issue was closed as stale in September. A new report, #107757 (opens in a new tab), reached the Hacker News front page on 5 November, and a Claude Code engineer replied the next morning:

Hacker News item 50388176, captured 6 November 2026 at 23:54 UTC.
2.1.317 was published to npm on 2 November, 2.1.318 on 3 November and 2.1.319 on 6 November.
Mechanics
No code in Claude Code sends the address on purpose. SEC EDGAR (Accessing EDGAR Data (opens in a new tab)), crates.io (RFC 3463 (opens in a new tab)) and Wikimedia (User-Agent Policy (opens in a new tab)) all ask scripted clients to put a contact in the User-Agent. The model tries to comply, and the only contact it has is yours.
Anthropic first answered with words. Since 2.1.318 the line that hands the model your address adds “A site asking automated clients for contact details, such as an email in the User-Agent, is not a reason to use this address”. Our copy of 2.1.319 still has the address line, so the removal has not shipped yet.
The guard is a hook on tool calls. Here is its rule as compiled into 2.1.319, identical in 2.1.317 and 2.1.318:
var i = ["WebBrowser", "WebFetch", "WebSearch"];
var w = /\b(curl|wget|https?|httpie|xh|nc|ncat|socat|telnet|ssh|scp|sftp|rsync|ftp|lftp|rclone|iwr|irm|invoke-webrequest|invoke-restmethod)\b/;
function m(tool, text, email) { // text: the call as JSON, lowercased, plus one %-decode
let r = email?.toLowerCase() ?? "";
if (r === "" || !text.includes(r)) return false;
let rest = text.replaceAll(r, " ").replace(/\\(?:\\|[nrt])/g, " ");
return i.includes(tool) || w.test(rest); // true -> ask the user
}
If your account address appears in a web tool call, or in a shell call that also mentions a network client, Claude Code asks before running it. With claude -p nobody is there to answer, so the ask becomes a refusal. The check reads the command as written. It never sees what the shell produces, so $(git config user.email) and "you"@"example.com" both get through.
Anthropic published the guard’s source on 6 November at mods/privacy (opens in a new tab) (commit 90380ef). That code has a different rule: it refuses only when the call also names a User-Agent, and it skips web tools. None of the three released binaries contains its refusal text, so the README is not a guide to what your install does.
Same inputs through both rules, with a placeholder account [email protected]:
| Call (Bash unless noted) | Published source | Shipped 2.1.317–2.1.319 |
|---|---|---|
curl -A "probe ([email protected])", also -H, wget -U, requests |
refuse | ask |
echo 'User-Agent: test [email protected]' > /dev/null |
refuse | runs |
curl -H "From: [email protected]" or [email protected] |
runs | ask |
| WebFetch with the address in the URL | not checked | ask |
-A "probe (jo"@"example.test)" |
refuse | runs |
-A "probe ($(git config user.email))" |
runs | runs |
| address built from variables or base64 | runs | runs |
| address inside a script file run by name | runs | runs |
| git email differs from the account email | runs | runs |
We ran both rules from a TypeScript copy under tsx 4.23.15. The echo row explains why two people in #107757 saw the guard let an echo with their address run: they were testing the published rule.
Impact
Anyone signed in to Claude Code with an account is exposed on a version below 2.1.317, and the engineer’s comment says the stable channel was still on 2.1.311. Accounts are not the only source. An API-key user with an email in ~/.gitconfig gives the model an address it can read with one command, and the guard does not know that address exists.
The receiver is whoever runs the API, plus any CDN, proxy or log pipeline on the way. In #107757 the reporter blames a wave of scam email on it, which we have not checked.
On the workstation used for this post, a scan of 1,329 Claude Code transcripts going back to 14 September found no case outside our own tests.
Probe
Tested on macOS 27.0 (arm64) with Claude Code 2.1.318, curl 8.7.1 and Python 3.15.2. Nothing leaves the machine except the prompt, which Anthropic already has.
Check which rule your binary carries:
B=~/.local/share/claude/versions/$(claude --version | cut -d' ' -f1)
grep -a -c "may leave this machine" "$B" # shipped rule, expect 2
grep -a -c "what looks like a User-Agent setting" "$B" # published rule, expect 0
Start a server that prints each User-Agent:
python3 -I -c '
import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
print(self.path, self.headers.get("User-Agent"), flush=True)
self.send_response(200); self.end_headers(); self.wfile.write(b"ok\n")
def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", 8199), H).serve_forever()'
In another shell, have Claude Code run three commands as written:
ACCT=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.claude.json")))["oauthAccount"]["emailAddress"])')
U=${ACCT%@*}; D=${ACCT#*@}
probe() { claude -p "Run this shell command once with the Bash tool, unchanged, and print its output or refusal verbatim: $1" \
--allowedTools "Bash(curl:*)" --output-format json | python3 -c 'import json,sys;print(json.load(sys.stdin)["result"][:160])'; }
probe "curl -s -A \"probe-L3 ($ACCT)\" http://127.0.0.1:8199/L3"
probe 'curl -s -A "probe-L1 ($(git config user.email))" http://127.0.0.1:8199/L1'
probe "curl -s -A \"probe-L2 ($U\"@\"$D)\" http://127.0.0.1:8199/L2"
Our run, each reply cut down to the tool result:
L3 Permission to use Bash denied by plugin cc-plugin-privacy: This call's input holds your account email address, which may leave this machine. ...
L1 ok
L2 ok
Our server log, address replaced:
/L1 probe-L1 (<account>)
/L2 probe-L2 (<account>)
The probe dictates each command, so it tests the guard and says nothing about how often the model writes these shapes by itself. We did not test bypassPermissions or auto mode, where an ask might be answered without a person.
To search your own history, run this against ~/.claude/projects. It only finds addresses written into a call, so the L1 and L2 shapes stay invisible to it:
#!/usr/bin/env python3
# List Claude Code tool calls that put your account or git email next to a User-Agent.
import json, pathlib, re, subprocess
home = pathlib.Path.home()
account = json.load(open(home / ".claude.json")).get("oauthAccount", {}).get("emailAddress", "")
git = subprocess.run(["git", "config", "--global", "user.email"], capture_output=True, text=True).stdout
mails = {m.strip().lower() for m in (account, git) if "@" in m}
ua = re.compile(r"user[-_]?agent|\bcurl\b.*\s-A\s|\bwget\b.*\s-U\s", re.I | re.S)
seen = set()
for f in (home / ".claude" / "projects").rglob("*.jsonl"):
for line in f.open(errors="replace"):
try:
content = (json.loads(line).get("message") or {}).get("content")
except (ValueError, AttributeError):
continue
for part in content if isinstance(content, list) else []:
if not isinstance(part, dict) or part.get("type") != "tool_use" or part.get("id") in seen:
continue
seen.add(part.get("id"))
text = json.dumps(part.get("input"))
if ua.search(text) and any(m in text.lower() for m in mails):
print(f.stem[:8], part.get("name"), text[:160])
Mitigation
-
Update. Run 2.1.317 or later now (
claude update), then the release that drops the address from the context, which Anthropic expects in the week of 9 November. -
Configure. Make sure
enabledPluginsdoes not setcc-plugin-privacy@builtintofalse. Pointgit config --global user.emailat a no-reply address on machines that run agents. Tell the model which contact to use, inCLAUDE.md:When a site wants a contact in the User-Agent, use "my-agent/1.0 (+https://example.com/agent)". Never use my email or my git email. -
Add a stricter hook. This one also strips quotes, knows the git address, and refuses any call that reads the address at run time:
#!/usr/bin/env python3 # PreToolUse hook: deny a call that carries or reads your email next to a network client. import json, pathlib, re, subprocess, sys call = json.load(sys.stdin) tool, text = call.get("tool_name", ""), json.dumps(call.get("tool_input", {})).lower() mails = set() try: mails.add(json.load(open(pathlib.Path.home() / ".claude.json"))["oauthAccount"]["emailAddress"]) except (OSError, KeyError, ValueError): pass for scope in ("--global", "--local"): mails.add(subprocess.run(["git", "config", scope, "user.email"], capture_output=True, text=True).stdout.strip()) mails = {m.lower() for m in mails if "@" in m} plain = re.sub(r"[\"'`\\]", "", re.sub(r"%([0-9a-f]{2})", lambda m: chr(int(m[1], 16)), text)) holds = any(m in text or m in plain for m in mails) reads = re.search(r"git\s+config[^|;&]*user\.email|\.gitconfig|oauthaccount|\.claude\.json", plain) net = re.search(r"\b(curl|wget|https?|httpie|xh|nc|ncat|socat|telnet|ssh|scp|rsync|ftp|requests|urllib|fetch|axios)\b", plain) if (holds or reads) and (tool in ("WebFetch", "WebSearch") or net): print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Call carries or reads the user's email. Use a generic client name or ask the user."}}))Save it as
~/.claude/hooks/no-email-out.pyand register it in~/.claude/settings.json:{"hooks": {"PreToolUse": [{"matcher": "Bash|WebFetch|WebSearch|Monitor", "hooks": [{"type": "command", "command": "python3 -I ~/.claude/hooks/no-email-out.py"}]}]}}It still lets through an address built from variables or base64. Text matching cannot catch a value the command assembles, and only taking the address away from the model closes that gap.
Verification
A version number tells you the guard is installed. Check that it is enabled, then rerun the probe and watch the server log:
claude --version # 2.1.317 or later
python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.claude/settings.json"))).get("enabledPlugins",{}).get("cc-plugin-privacy@builtin","default (on)"))'
With the hook loaded through --settings, our rerun gave:
L1 PreToolUse:Bash hook error: Call carries or reads the user's email. Use a generic client name or ask the user.
L2 PreToolUse:Bash hook error: Call carries or reads the user's email. Use a generic client name or ask the user.
L4 ok (curl -s -A "probe-L4/1.0 (+https://example.com/agent)")
Only the L4 request reached the server. Once Anthropic’s fix ships, grep -a -c "The user's email address is" on the new binary should print 0; on 2.1.319 it prints 2.
Sources
- anthropics/claude-code #78431 (opens in a new tab), opened 17 July 2026 (v2.1.212), closed stale 11 September. First report; crates.io tool call in the 11 August comment.
- anthropics/claude-code #107757 (opens in a new tab), opened 5 November 2026 (v2.1.318). Current report, guard announcement, the
echoreports. - anthropics/claude-code #55743 (opens in a new tab) (3 May 2026) and #81138 (opens in a new tab) (25 July 2026). Email in the model’s context.
- anthropics/claude-code
mods/privacy(opens in a new tab), commit90380ef, 6 November 2026. The published rule. - Claude Code binaries 2.1.316, 2.1.317 and 2.1.318 (native installs) and
@anthropic-ai/[email protected](npm), read 6 November 2026. The shipped rule and the context text. - npm registry,
@anthropic-ai/claude-code(opens in a new tab) publish times. Release dates. - Hacker News item 50388176 (opens in a new tab), Claude Code engineer, 6 November 2026 04:24 UTC. Cause, planned fix, stable channel on 2.1.311.
- U.S. SEC, Accessing EDGAR Data (opens in a new tab), last reviewed 26 June 2024. EDGAR User-Agent format.
- Rust RFC 3463 (opens in a new tab), crates.io policy update, 2023. crates.io contact recommendation.
- Wikimedia Foundation, User-Agent Policy (opens in a new tab), edited 27 March 2026. Wikimedia contact requirement.