Back
Blog
Engineering
SneakPeek Is Code Execution. The Payload Is a Credential Harvest.

Minh Anh Day
On 14 October, Noma Security published SneakPeek, a flaw in the HTML preview pane of Claude Code Desktop. Ask Claude to look at an HTML file from a repository, and the preview renders it. Past 256 KB, the file was written to disk and loaded from file:// instead of a sandboxed data: URL, so the content security policy no longer applied, and a script inside it could read files from your home directory and send them anywhere. Anthropic fixed it on 17 August (Claude Desktop 1.30096.1), rated it CVSS 8.7, and then fixed it again, because the first fix was bypassed.
Most of the coverage filed it under data exfiltration. I think that undersells it, and the difference matters for how you defend against the next one.
Arbitrary code execution, not a leak
A leak is when a program shows something it shouldn't. SneakPeek is different: a file in a repository gets to run code its author chose, on your laptop, inside a trusted process that can read your files and reach the network. What that code does is entirely up to the person who wrote the file.
The bypass is the tell. Anthropic's first fix closed one way for the script to run; the bypass moved the same code into a Web Worker, which is just another place for it to run. You don't play whack-a-mole with a leak. You play it with an execution context.
And notice who didn't do anything: the agent. No tool call, no command, nothing to approve or deny, which is why there was never a permission prompt. The preview renderer did all of it. Every control that lives inside the agent (permission prompts, allowlists, sandboxes around tool calls) was looking at the wrong process.
So what would you run?
If you could run any code on a developer's laptop for a few seconds, without asking, what would you run? You'd go for the thing that's worth the most and travels the best: their credentials. All of them.
That's what the write-ups list as SneakPeek's targets: SSH private keys, AWS credentials, GitHub secrets, Kubernetes configs, and Claude's own OAuth token, all at once. It's the same list as the litellm PyPI compromise in March and the pgserve npm compromise in April. The way in changes every month (a preview pane, a postinstall script, a .pth file, next month something else). The payload doesn't. It's credential harvesting.
Why harvesting is hard to catch without crying wolf
Here's the problem. Every individual step of a harvest is something legitimate software does all day:
sshreads your SSH key.The AWS CLI reads
~/.aws/credentials.kubectlreads your Kubernetes config.gitreads~/.ssh, then pushes to a remote it has never talked to, because you just forked something.
That last one is the killer. "A process read a credential, then talked to a host it has never contacted" describes SneakPeek, and it also describes git push to your new fork. A detector built on that rule fires every time someone forks a repo. Within a day, developers mute it. A muted detector catches nothing, including the real attack the next week.
So false positives aren't a cosmetic annoyance in this space. They're how detection dies. The whole job is drawing a line that legitimate software almost never crosses and real harvests almost always do.
One key is picking. Four keys is harvesting.
The line we draw starts with a simple observation. Legitimate tools pick: each one reads the one credential it needs. ssh doesn't open your AWS keys. The AWS CLI doesn't open your SSH key. Nothing you run on purpose needs your SSH key, your AWS credentials and your Kubernetes config at the same moment.
A harvester needs exactly that, because it doesn't know which key will turn out to be valuable, so it takes all of them. So the question isn't "did a process read a credential?" It's "how many different kinds of credential did one process hold?"
That's what our credential_harvest check looks for: a single process holding three or more distinct classes of credential (SSH, cloud, Kubernetes, package-registry tokens, wallets, agent tokens). One class is picking and stays quiet. Three is harvesting and is graded critical. Three important design choices:
It doesn't wait for weird traffic. The best harvesters send perfectly boring HTTPS. If you need the network to look strange before you alert, you've let them choose when you see them.
It stands down for self-access. An application reading its own credential store isn't harvesting. That's what the store is for.
It's in our release gate. Every release replays real harvest shapes against it (the litellm compromise, the pgserve compromise and an AI-credential sweep) on macOS, Linux and Windows. A build that misses one doesn't ship.
The part of the recipe we keep: grading in more than one dimension
Counting credential classes is one axis. It isn't the whole answer, and no single axis is. The core of how EDAMAME grades a finding is that it looks at several independent dimensions at once, each of which is weak, and often perfectly innocent, on its own:
how many distinct kinds of credential the process touched;
whether the destination is one this specific program has used before;
whether the flow looks anomalous, or is a sustained stream rather than a handshake;
whether a second process is involved, such as one reading and another sending;
whether the destination is already known to be bad;
whether what's being read actually looks like a secret.
A new destination alone is a Tuesday. Two credential classes alone is a developer setting up a machine. A sustained stream alone is a backup. It's when several of them line up in the same process, in the same window, that the probability of an attack moves from low to high to critical. That combination is what we score, and the exact weights are the part of the recipe we don't publish.
Benign explanations count too. An application talking to its own vendor's servers is ordinary, and earns credit against an alert. But that credit is withheld the moment credentials are riding the connection. An app talking to its vendor is fine. An app talking to its vendor while streaming your SSH key is not, and the first fact is never allowed to excuse the second.
What we tested, and where SneakPeek still gets past us
We added a SneakPeek-style scenario to the EDAMAME Attack Simulator. The trigger is public. It reproduces only what the host can see and none of the browser exploit (no preview pane, no 256 KB trick, no file:// bypass). A render engine embedded in an AI desktop app holds credential files open while streaming data to a destination it has never contacted. The credentials are decoys, the endpoints are public test services, and nothing secret leaves the machine.
That's the noisy version, and EDAMAME catches it: high and critical findings for sensitive-material egress and token exfiltration. We also checked two things the demo depends on. The render engine is observed under its own name, not as a generic interpreter. And we don't wave it through as a browser: browsers get a pass on "talking to a new host," since every page you open is one, but an agent's embedded renderer must not get that pass. A test now pins that down.
The real SneakPeek is the quiet version, and it's quiet in three ways at once. Per Noma's write-up, the script reads each file with fetch(), which opens and closes it in milliseconds. It sends one beacon, not a stream. And in a Chromium-based app the two halves land in different processes: the app's main process serves the file read, and its network process sends the request. So no file is held open, nothing streams, and no single process both reads and sends. Each of those removes one dimension we grade on, and together they remove the combination.
By our own analysis, that shape very likely produces no finding at all today. Part of it we've measured. The read-once, single-process version grades low: visible in the console, never alertable. The two-process part comes from how Chromium is built, not from a run on Claude Desktop. Either way, we'd rather write it down than let a demo stand in for it.
Closing it is the same problem as the rest of this post, which is why I find it the interesting part. The fix is to tie a read in one process to a send from a sibling process of the same application, and to count how many kinds of credential the application touched, not just one connection. Done naively, that turns the detector into an allow-list of every legitimate credential reader on a developer machine, which is the false-positive trap from the start of this post. That's the work.
What to take from it
The next SneakPeek won't be a preview pane. It'll be a postinstall script, a .pth file, an MCP server, or something nobody has thought of yet. Patches arrive after the fact, and as this one showed, the first patch can be bypassed. What won't change is the payload: someone will try to take every credential on the machine, at once.
So detection that lasts has to key on the shape of the payload, not on the way in. And it has to be quiet enough that nobody turns it off. That's the trade we're making: pick out the harvest by what it can't avoid doing (touching many kinds of secret), grade it across several dimensions so no single coincidence trips it, and say plainly where the line still sits. Today, that includes SneakPeek's quiet version.
If you want to see the SneakPeek-style scenario run on your own machine, the Attack Simulator ships with EDAMAME: https://www.edamame.tech/agents
And the earlier post on the same primitive, a trusted process quietly reading a key and sending it out, is here: https://www.edamame.tech/blog-full/in-process-key-theft-solana-web3js

Minh Anh Day
Share this post



