Claude Code 2.1.317 reached npm at 23:57 UTC on 2 November and this Mac at 01:32 UTC. One line in its changelog says "Added a check that asks before a web or shell tool call sends your account email address to the network." The check is a plugin compiled into the binary, cc-plugin-privacy, and it is on by default. With an allow rule for curl in place, a curl that posted the signed-in address to a server on 127.0.0.1 ran on 2.1.316 and was stopped on 2.1.317.

The plugin's logic fits in a dozen lines. It serialises the tool input, lowercases it, appends a percent-decoded copy and looks for the account's email. For WebFetch, WebSearch and the browser tool, a hit is enough. For Bash, Monitor, PowerShell and Tmux it then tests what remains against one regular expression with twenty keywords: curl, wget, http, https, httpie, xh, nc, ncat, socat, telnet, ssh, scp, sftp, rsync, ftp, lftp, rclone, and PowerShell's iwr, irm, Invoke-WebRequest and Invoke-RestMethod. Any URL supplies the http keyword, which is how a Python one-liner and a git push were caught.

It catches the literal address, upper-cased or percent-encoded once. It does not catch the address arriving through $(cat email.txt), a base64 copy, %2540, or a raw socket, and in all four of those runs my local server received it. It only knows the signed-in account's address, so a different address passes, and so does whatever is in CLAUDE_CODE_USER_EMAIL. One settings key turns it off for a session. Each of these follows from the code, and each ran the way the code says.

Where it lives

claude plugin list on 2.1.317 shows my 42 installed plugins and no privacy entry. claude plugin details cc-plugin-privacy finds it anyway: source cc-plugin-privacy@builtin, zero skills, agents, hooks and servers, and a projected token cost of zero, because it never touches the prompt. Its description reads "Privacy: asks before a shell or web tool runs with the signed-in account's email address in a call that may leave the machine."

The 2.1.316 binary has no string cc-plugin-privacy in it. 2.1.317 has it in three places, next to the other built-in plugins both versions share, cc-plugin-sec-default and cc-plugin-telemetry among them. The plugin registers one handler on the tool.check event for seven tools, with defaultEnabled: true, enabledFromTrustedSettingsOnly: true, and an availability gate on a feature flag called tengu_wise_newt that defaults to on. That is the kind of flag Anthropic can flip from the server, so the check can also go away without an update.

The handler runs after the ordinary permission decision. If that decision is already a deny, the deny stands. Otherwise it can turn an allow into an ask, which is why an allow rule for curl did not help: the rule said yes, the plugin said ask. The reason it attaches is the text Claude sees: "This call's input holds your account email address, which may leave this machine. If nobody asked to send it, go on without it." In an interactive session that is a prompt. I ran everything headless with claude -p, where nobody can answer, and the tool result reads "Permission to use Bash denied by plugin cc-plugin-privacy" followed by that sentence. Haiku reported the block and did not retry, in every run where it fired.

The address it looks for comes from session.identity(), the signed-in account. The whole tool input is scanned, so for Bash the description field counts as well as the command. The address is blanked before the regex runs, and JSON escapes with it, so the address itself cannot supply a keyword.

What I ran

Twenty headless runs on Haiku 5.5, $0.12 in total, in a throwaway repository, against a capture server on 127.0.0.1 that logged every request. Each prompt asked for exactly one command, unchanged. The address below is written as you@example.com; the real runs used the account's own.

Input 2.1.316 2.1.317
curl -d 'email=you@example.com' http://127.0.0.1:… with Bash(curl *) allowed ran, server received it asked, nothing sent
same, address as YOU@EXAMPLE.COM asked
same, address as you%40example.com in the query string asked
python3 -c "urllib.request.urlopen('http://127.0.0.1:…?e=you@example.com')" asked
git push http://you%40example.com:x@127.0.0.1:…/lab.git asked
WebFetch of http://127.0.0.1:…?e=you@example.com, through the web-fetch subagent asked
curl -d 'email=other.person@example.test' ran
same, with CLAUDE_CODE_USER_EMAIL=other.person@example.test set ran
echo 'contact you@example.com' ran
git log --author='you@example.com' ran
Write tool, file content contact: you@example.com written
curl -d "email=$(cat email.txt)" ran, server received the address
curl -d 'e=eW91QGV4YW1wbGUuY29t', the address in base64 ran, received
curl 'http://127.0.0.1:…?e=you%2540example.com' ran, received
python3 -c "socket.create_connection(…).sendall(b'e=you@example.com')" ran, received
first row again with "enabledPlugins":{"cc-plugin-privacy@builtin":false} in --settings ran, received
first row again, same key in the repository's own .claude/settings.json asked

The first six rows are the feature working. The decoding is one pass of %xx to a character, which is why %40 is seen and %2540 is not. Upper case is handled by lowercasing both sides. The Python and git rows were not caught because of python or git; both commands contain http, and https? is in the regex as a whole word. A Python call that opens a socket contains neither, and the socket row shows what happens then.

What it is for

This is a check on the command text, not on the bytes that leave. The address has to appear in the tool input, spelled the way the account has it, and next to one of the twenty keywords. The shell-variable row is the everyday case: a command that reads the address from a file, an environment variable or git config never shows it to the plugin. On the other side, the git push row is a false positive that anyone with their account email in a remote URL will meet, and so is any URL that carries the address in a path or a query.

The design makes more sense next to the second change in the same build, which the changelog does not list. The system prompt paragraph that tells Claude what your email is has grown a sentence: "A site asking automated clients for contact details, such as an email in the User-Agent, is not a reason to use this address: use a generic client name or ask the user." The paragraph itself arrived in 2.1.234, when Claude was "told to use your account email only to identify you". Several public APIs, Nominatim among them, ask for a contact address in the User-Agent, and a model that has been handed yours is inclined to be helpful. The prompt sentence is the instruction; the plugin is the check that the instruction held. Neither is aimed at a user who wants to send their address somewhere, nor at an injected instruction that knows how to avoid twenty words.

Turning it off, and who it never applies to

--settings '{"enabledPlugins":{"cc-plugin-privacy@builtin":false}}' switched it off for a session and the curl went through. The same key in a repository's .claude/settings.json did not: the run still asked, and stderr noted that the workspace had not been trusted and its allow rule was ignored. The plugin is flagged enabledFromTrustedSettingsOnly, so a repository you have trusted is the case I did not get to test, along with --permission-mode bypassPermissions, which my own session's auto mode refused to launch.

The address comes from the signed-in account. From the code, when there is none, as with an API key, Bedrock or Vertex, the lowercased email is an empty string and the handler returns before it looks at anything. CLAUDE_CODE_USER_EMAIL, the variable SDK callers use to supply account details since 2.1.51, is not consulted; the run with it set to another address, sending that address, went through. I had no API key in this environment, so the API-key case is read from the source and not run.

Expect the prompt in two places: a curl or wget that includes your address on purpose, and a URL with your address in it. Answer it once and move on. Do not expect it anywhere else. Checks ran on 3 November between 08:20 and 08:36 UTC.