How to Check Claude Code Transcripts for Your Email Address
Claude Code writes every session to a .jsonl file in ~/.claude/projects. Read only the tool calls, find addresses that sit inside a User-Agent value, and check whether each call actually ran.
These notes come from building the Claude Code email leak checker on this site. It does all of this in your browser tab, so you can skip the code and just drop your folder in.
Why you might want to check
Some sites ask automated clients to put contact details in the User-Agent header. Claude Code can see the signed-in account’s email, and users have reported it filling in their real address, for example in curl calls to crates.io and in web searches. The transcripts are the record of what it sent.
Where the files are and what is in them
On Windows the folder is %USERPROFILE%\.claude\projects. On a Mac or Linux it is ~/.claude/projects. There is one subfolder per project and one .jsonl file per session. Each line is one JSON object. The lines that matter look like this, shortened:
{"type":"assistant","timestamp":"2026-10-27T03:01:00.000Z",
"message":{"content":[{"type":"tool_use","id":"toolu_01...",
"name":"Bash","input":{"command":"curl -A \"bot/1.0 (you@...)\" ..."}}]}}Only read tool_use blocks. Your address also appears in the session’s context and in tool output, and neither of those is a request the agent sent. Counting them gives you a scary number that means nothing.
Reading 1.9 GB without freezing the tab
My own folder had 1,206 files and 1.9 GB. A single file can be tens of megabytes, so don’t call file.text(). Stream each file and split on newlines. Then skip any line that can’t hold a leak before paying for JSON.parse.
async function eachLine(file, onLine) {
const reader = file.stream()
.pipeThrough(new TextDecoderStream())
.getReader();
let rest = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
rest += value;
let nl;
while ((nl = rest.indexOf("\n")) !== -1) {
onLine(rest.slice(0, nl));
rest = rest.slice(nl + 1);
}
}
if (rest) onLine(rest);
}
// Most lines can't hold a leak. Skip them before JSON.parse.
function worthParsing(line) {
return line.includes('"tool_use"') && line.includes("@");
}With that, the whole folder scanned in 24 seconds in Edge on my machine, with no network requests during the scan.
The bug: git add -A is not a User-Agent
curl sets a User-Agent with -A. My first version treated an -A before the address as that flag, so git add -A in a chained command counted as a User-Agent. On my real transcripts the scan went from 17 raw hits to 8 once the address had to sit inside the header’s value. The naive rule below has the same flaw.
// Naive: a User-Agent or -A anywhere before the address.
const NAIVE = /user[-_ ]?agent|\s-A\s/i;
// Fix: the address must sit inside the header's value.
const USER_AGENT =
/(?:user[-_ ]?agent["']?\s*[:=,]?\s*["']?|\s-A\s+["']?)[^"'\n]*$/i;
// Test each rule on the 120 characters before the address.The trick is [^"'\n]*$ at the end. Between the header name and the address there can be no closing quote, so the address must still be inside the value. Here are both rules on four sample lines, all using a made-up address:
naive=HIT fixed=HIT curl -s -A "feed-reader/1.0 (contact priya.dev@…
naive=HIT fixed=HIT UA = {"User-Agent": "ImageTrial/1.0 (priya.dev@…
naive=HIT fixed=miss git add -A && git commit -m "fix" && curl -d "to=…
naive=HIT fixed=miss curl -A Mozilla/5.0 -d '{"email":"priya.dev@…The fourth line still matters: the address is in an email field of a request. The checker lists those separately under “worth a look”, because signing up to your own app does that on purpose.
Did the request actually go out?
A tool call in the file is only an attempt. You might have refused it at a permission prompt. Each tool_result block carries the id of the call it answers, so keep the ids of calls with a hit and mark them when their result arrives.
// A tool call and its result share an id.
// tool_use.id === tool_result.tool_use_id
const status =
block.is_error === true && REFUSED.test(body) ? "blocked" : "ran";
const REFUSED =
/doesn't want to proceed|was rejected|permission|denied|blocked|not allowed|refused/i;One more trap: a resumed session repeats its history in the new file. Skip any call id you have already seen, or every hit is counted twice.
What it found on my machine
4 User-Agent headers with an address in them, on 2 sites: 1 to reddit.com and 3 to commons.wikimedia.org. A scan can only find addresses written out in full. One read from a variable or a config file at run time won’t show up, so zero hits means none found, not none sent.
Want the result without the code? Use the free Claude Code email leak checker. More notes are on the blog.
Written by Himanshu Srivastava, a backend engineer in Bengaluru.