Open Source 6 min read

Claude Code privacy is a plugin, so test your own agent

Claude Code privacy in practice: the account email sat in the model's context. I pointed opencode and Codex at a request logger and read what they send.

Abstract illustration: a small amber envelope inside a teal box, with one thin coral line leaving through a gap in the wall.

Claude Code privacy took a hit this week. Claude Code is Anthropic’s closed-source coding agent, and a GitHub issue reports that it handed the signed-in user’s account email to the model, which then sometimes wrote that email into the User-Agent header of its own curl calls. The address went to whatever site it was fetching. lydia_hallie, who opens her Hacker News comment with “I work on Claude Code” and lists @anthropics on GitHub, confirmed the mechanism. The shipped fix is a plugin that its own README calls “a courtesy, not a boundary.”

Here is why the email was in the context, what the plugin matches and misses, and what two open-source agents, opencode (MIT) and OpenAI’s Codex CLI (Apache-2.0), sent to a request logger on my own machine, signed out and signed in. My position: privacy belongs in what goes into the context, not in a filter on what comes out, and you can test your own agent in ten minutes.

Abstract illustration: a small amber envelope inside a teal box, with one thin coral line leaving through a gap in the wall.
The model can only leak what it was handed. The gap in the wall is the part you can measure.

Why the account email was in the model’s context

Her explanation is blunt: “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.” Then, by her account, when Claude wrote a curl command itself it sometimes put the address in the User-Agent as a contact address, because some sites ask scripted clients for one.

So the email was a patch for a model habit, and it handed an inbox to a text generator in the hope it would behave.

It is not new. Issue 81138 was opened on July 25, three months earlier. Its author decompiled the v2.1.220 bundle and found a block that reads the address from oauthAccount.emailAddress in ~/.claude.json and injects “The user’s email address is” into every session. The author says it is documented nowhere.

One commenter on the November issue had their own Claude scan their session transcripts and reports 53 calls that put their email into a User-Agent, across 8 sessions, mostly from subagents writing curl -A. That is one person’s count from one machine. The November reporter also blames dozens of daily scam emails on it; that link is an inference, the header leak is what she confirmed.

The newest release on the release list I read is v2.1.322, and its one note, about fast mode on Vertex, does not remove the address from the context. She wrote that it would come out “in the next release.”

What the Claude Code privacy plugin checks, and what it misses

The fix that did ship is a built-in mod, and it changed shape in six days. The v2.1.317 note (November 3): “Added a check that asks before a web or shell tool call sends your account email address to the network.” The v2.1.321 note (November 9): “Changed the account email check to refuse only a shell command that holds your email and names a User-Agent, without asking, so other commands and scheduled runs no longer stop at its prompt.” So web tools were covered by an ask, and since 2.1.321 they are not checked.

The README describes the 2.1.321 state. One hook watches Bash, PowerShell and Monitor calls and refuses one if its input holds the account address and names a User-Agent. “The test is two word matches over the whole input. There is no parser, on purpose.”

It then lists what passes: the address in a URL, a request body, curl -u or a From: header; WebFetch, WebSearch and MCP tools, which are not checked; a value computed at run time such as $(git config user.email); and a script that holds the address. The line that matters: “This is a courtesy, not a boundary, and it covers one shape.”

A commenter hit that last gap within a day: the model wrote the address into a script with its edit tool, then ran python3 fetch.py. She said as much: the check “still doesn’t prevent all cases, like when Claude writes a script and runs on its own.”

I have seen this shape before. When I tested eleven Claude Code features, one hook printed “Blocked” and blocked nothing. A policy layer next to the model fails quietly.

One commenter put it in two sentences: “Privacy shouldn’t be a plugin. It should be an invariant.” A model cannot write what it was never shown. Once the address is in the context, every Claude Code privacy protection after is a guess about wording.

What opencode and Codex sent to my request logger

Bon, the measurement. The logger is nine lines of Python on 127.0.0.1. It appends every POST to a file and answers 200 with nothing:

import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class L(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("content-length", 0)))
        rec = {"path": self.path, "headers": dict(self.headers), "body": body.decode()}
        open("seen.jsonl", "a").write(json.dumps(rec) + "\n")
        self.send_response(200); self.end_headers()
HTTPServer(("127.0.0.1", 18081), L).serve_forever()

Each agent got a fresh home directory and a git repo whose identity was a canary: user.email canary.7f3a@example.invalid, user.name Canary Person, one commit. I pointed its provider at http://127.0.0.1:18081/v1, ran opencode run "say hi" and codex exec --skip-git-repo-check "say hi" under timeout 60, then searched the capture.

opencode 1.19.0 sent two distinct requests to /v1/chat/completions. The first, 2,443 bytes, is a title request with no tools. The second, 30,698 bytes, carries a 9,562-character system prompt and 10 tool definitions. The User-Agent is opencode/1.19.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14. Three headers carry the same session id. The system prompt ends in an environment block: working directory, git repo yes or no, the date.

Codex 0.162.1 sent one distinct request, 39,458 bytes, to /v1/responses. Its User-Agent names the build and the OS. Its x-codex-turn-metadata header is JSON: installation, session and thread ids, "analytics_enabled":true, and the repo’s latest commit hash with "has_changes":true.

Signed out and signed in, no canary left the machine

Signed out, neither capture held the canary. Across 428 requests (422 from opencode, 6 from Codex, because both retry when the logger answers with nothing) I counted 0 hits for canary and 0 strings shaped like an email address.

The issue is about a signed-in account, so I reran it with the canary in each agent’s credentials file: Codex’s auth.json in ChatGPT-login shape, with the address as the email claim of its token, and opencode’s auth.json with an OAuth-style entry carrying it in an email field (opencode’s format has no email field, so that entry is mine). Same homes, logger and timeout 60. Of 370 requests (364 opencode, 6 Codex), 0 carried the canary in a header or body, in plain text or base64. The empty replies never ask for a tool, so this shows what each harness puts in context, not what a model writes in a command. Builds measured: opencode 1.19.0 (npm latest 1.19.5) and Codex 0.162.1 (latest 0.167.0, November 6).

Both send your working directory; yours is probably /Users/yourname/..., an identifier too. The difference is that you can read this. With a closed harness you are trusting a changelog.

Run the check on your own agent

Four steps:

  1. Run the logger above as logger.py in an empty folder.
  2. Make a throwaway git repo with a canary identity and a fresh home directory.
  3. Point the agent’s provider at http://127.0.0.1:18081/v1. In opencode that is baseURL on a custom provider; in Codex, base_url under model_providers in config.toml. Pi, the harness the reporter is moving to, is MIT-licensed and takes a custom provider too.
  4. Send one short prompt, run grep -c canary seen.jsonl, then read the User-Agent, the headers and the first screen of the system prompt. Look for your username in the paths.

A zero on the canary tells you what the harness puts in context.

If you use Claude Code, search the transcripts under ~/.claude/projects for your own address and count the files that hit.

My rule from here: the agent gets the identity of the project, never of the person. If a site wants a contact address, I type it into that one request myself.

Point the logger at your agent tonight. Whatever lands in seen.jsonl is what the vendor decided you did not need to be told.

Dominic Plouffe

CTO of Trellis, with exits at FuseTalk, 360pi and DeadHeat. I write about making agents and big data work together safely, and I'm building yaaf, an open-source platform for running teams of AI agents that actually finish the job: yaaf.dplouffe.ca