← Back to all posts
Published on · by Renaud Deraison

The preview could read your home folder

On October 14, Noma Labs disclosed SneakPeek, a flaw in Claude Code Desktop's preview pane. An HTML file padded past 256 KB, disguised as a test coverage report, was loaded from disk with no script policy, and its JavaScript read SSH keys, AWS secrets, kubeconfigs and Claude's own OAuth tokens from the developer's home folder, then sent them out with a background beacon. Anthropic scored it CVSS 8.7 and fixed it in August. In Bromure Agentic Coding, pages render in a separate, disposable VM that holds none of your files, and the agent's own home holds stand-ins instead of keys.

The repository looked normal. The file was a coverage report. The developer asked the agent to look at it, and the preview pane did the rest.

A developer clones a repository and asks Claude Code to look at the test coverage report in it. It's a routine request. The report is an HTML file. When Claude reads or edits an HTML file in Claude Code Desktop, a preview pane renders it next to the conversation, without asking. This report carries a script. Within seconds, the script has read the developer's SSH private key, AWS keys, npm and GitHub tokens, Kubernetes credentials and Claude's own OAuth tokens, and sent all of it to a server the attacker controls.

That is the proof of concept Noma Labs published on Wednesday, October 14, under the name SneakPeek. CSO Online covered it the same day. Anthropic scored the flaw CVSS 8.7 (High) and treated its impact as equivalent to code execution on the host. Noma reported it on July 4. The fix shipped through auto-update on August 17 in Claude Desktop 1.30096.1, after a first patch was bypassed. A GitHub advisory and a CVE are pending.

The bug is fixed. The setup that made it worth finding is still on most developer laptops: something that renders untrusted content, running as you, next to everything you own.

Three defenses and a size limit

The preview pane is a Chromium panel inside the Electron app. Noma counted three defenses around it, and found a gap in each.

A script policy for small files. Files under 256 KB were loaded as base64 data: URLs with a Content Security Policy, a list of rules that tells the browser which scripts may run. That policy blocked scripts. Files over 256 KB were written to a temporary file and loaded from disk as a file:// URL, and the policy didn't apply to that path. Padding a report past 256 KB is trivial. Real coverage reports are often that big.

An origin with the run of the disk. A page's origin decides what it may read. A data: URL gets an opaque origin that can read nothing. A page loaded from file:// got, in Noma's words, "same-origin access to the victim's entire local filesystem." The script worked out the home folder from the temporary file's path and read files with fetch() and XMLHttpRequest.

A filter that wasn't switched on. The pane had a request filter meant to block file:// access. It engaged for proxied dev servers and stayed off for static HTML rendered from disk.

To get the data out, the script called navigator.sendBeacon(), a web API that fires a one-way HTTP POST in the background. Nothing in the app limited where the pane could send requests. Noma's write-up adds the developer's real IP address, collected through WebRTC.

Anthropic's first patch, on July 24, blocked fetch() and XMLHttpRequest to file:// paths from the page. The same day, Noma moved the reads into a Web Worker started from a blob: URL, a context the patch didn't cover, and all three defects worked again. The final fix in August closed the policy gap, the file:// loading and the worker path together. Noma's researcher Eli Ainhorn summed it up for CSO: "The protections were real, they just didn't cover every path."

On a laptop: one look at a report, one beacon outPublic repositorysrc/ tests/coverage/index.htmlpadded past 256 KBone hidden script"Look at thecoverage report."Preview pane, running as you on the Macunder 256 KB: data: URL, scripts blockedover 256 KB: file:// URL, no policy,same origin as your whole diskfetch ~/.ssh/id_ed25519fetch ~/.aws/credentialsfetch ~/.kube/configfetch ~/.docker/config.jsonfetch Claude OAuth tokensno permission prompt, no clickAttacker's serversendBeacon POST-----BEGIN OPENSSHaws_secret_access…client-key-data:…npm and GitHub tokensplus your real IP,via WebRTCThe agent never ran a command. The renderer did the reading, with your account's access to your files.
SneakPeek on a laptop. A coverage report padded past 256 KB skips the script policy, loads from disk with access to the whole home folder, reads the developer's keys and beacons them out. All the developer did was ask the agent to look at a file.

The agent didn't do it

SneakPeek uses no prompt injection. The agent read a file you asked it to read, and the app around it rendered that file. The script ran in the preview's rendering process, with the app's access to your disk.

Crystal Morin of Sysdig told CSO that agent-level logs might not capture this execution, so defenders would have to watch file reads and outbound connections at the runtime layer. Your session transcript would show a harmless request and a harmless answer.

A careful model, strict permission rules and a human approving each command would have changed nothing. The renderer sat on the Mac, running as you, next to your home folder and every credential you've collected there.

Ainhorn's advice to CSO goes at that second part: keep long-lived credentials off developer machines, and use scoped, short-lived tokens.

The same report in a Bromure machine

In Bromure Agentic Coding, each agent runs inside its own Linux VM on your Mac, called a machine, and the embedded browser that renders web pages runs in a second, disposable VM beside it. Clone Noma's repository into a machine and ask Claude Code to look at the coverage report.

Reading the report is reading text

The agent reads coverage/index.html the way it reads any file: as text in its context. Nothing in the chat renders it. If you click the file in the Files pane, the detail view shows HTML as highlighted source, the same way it shows a .py or a .json. No script on the page runs. Files over 512 KB aren't previewed at all. A report padded past 256 KB to dodge a script policy shows up as 300 KB of markup.

Rendering happens in another machine

When you do want to see the report rendered, the agent serves the folder from its machine and opens it in the browser pane, at the machine's IP address. The browser is a full Chromium, and it runs in its own sidecar VM. As the manual puts it, "a file the agent writes in its home directory is not on the browser's disk."

The script runs and finds nothing to read. The page's origin is http://192.168.64.x:8000, an ordinary web origin. Chromium doesn't let it read file:// paths. A script that got past that, through a worker or a browser bug, would land on an ephemeral copy-on-write clone of the browser image: no ~/.ssh, no ~/.aws, no kubeconfig, no OAuth token, none of your projects. The home folder it derives from a temporary path belongs to a Chromium VM with nothing in it. The beacon goes out empty.

By default the browser keeps no profile. Stay signed in to websites is off, so cookies and logins vanish when the browser closes, and a hidden browser is torn down after five minutes. WebRTC stays off unless you allow the camera or microphone in the machine's Browser settings, so the IP-address trick has nothing to call.

The agent's home holds stand-ins

Now point the same script at the agent's own VM. Say the repository's test command writes and opens the report itself, so the code runs where the agent works. Walk through Noma's list in that home folder:

  • SSH private key. There isn't one. SSH_AUTH_SOCK is backed by an agent bridge to the Mac. The VM can list identities and ask for signatures, and the private key bytes never leave the host. ls ~/.ssh/id_* finds nothing.
  • AWS. ~/.aws/config points at a helper that hands out the real access key ID with a fake 40-character secret. The Mac re-signs each request with the real secret on its way to AWS.
  • Kubernetes. ~/.kube/config is synthetic. kubectl talks to the proxy on the Mac, which holds the real cluster credentials.
  • Docker, git and npm tokens. ~/.docker/config.json and ~/.git-credentials hold stand-ins shaped like the real thing. Tokens you import, from a .env or by hand, become stand-ins scoped to their hosts.
  • Claude's OAuth tokens. They stay on the Mac. The agent runs with a placeholder key, and the proxy puts the live access token on the wire for requests to Anthropic.

The stand-ins are also tripwires. The proxy on the Mac scans every request leaving the machine, headers and body, for a stand-in headed to a host it wasn't minted for. A beacon carrying the brm-docker-… password or the fake GitHub token to the attacker's server gets HTTP 451. Nothing is forwarded, the VM pauses, and an alert titled This environment may have been compromised opens on the Mac with the token preview, the credential's name, the host it belongs to and the host it was sent to. The detection lands as a red row in the Security Timeline. Set Unmatched traffic to Deny in the machine's Outbound connections firewall, and a connection to a host the project doesn't use is refused before any of that.

That covers Morin's point about the runtime layer. The checks sit on the Mac, on the wire, outside the agent and outside anything rendering inside the VM.

In Bromure: the renderer has no files, the agent has no keysMachine VM (the agent)index.html read as textFiles pane: highlighted sourceHome folder~/.ssh/id_* (none)~/.aws helper, fake secret~/.kube/config syntheticdocker, git brm-… stand-insClaude key placeholderserves coverage/ on :8000no real secret here to readBrowser VM (disposable)http://192.168.64.x:8000script runsfile:// reads refuseddisk: fresh clone, no user filesno saved profile, WebRTC offsendBeacon(…) → nothing in ittorn down 5 minutesafter it's hiddenThe Macproxy swaps the real key inonly for its own hoststand-in to wrong host:451, VM paused, alertfirewall: unknown host deniedSecurity Timeline:red row, both hosts namedKept on the Mac, outside both VMsSSH private keys · AWS secrets · cluster credentials · registry and git tokens · Claude OAuth tokens · Security Timeline
The same report in Bromure. The agent reads it as text and the Files pane shows it as source. When it renders, it renders in a disposable browser VM with none of your files. The agent's own home holds stand-ins, and a stand-in sent to the wrong host stops the machine.

The fix and the next preview

Anthropic fixed SneakPeek in August and Noma retested it. Six weeks passed between the report and the fix, and the first patch fell to a Web Worker the same day it shipped. Renderer fixes tend to go that way. A browser engine has many ways to start a script and many ways to make a request, and each defense has to cover all of them.

Coding tools keep adding panes that render things: HTML previews, Markdown with embedded content, notebook outputs, dev servers, screenshots of the app the agent just built. Each one is a renderer pointed at content the developer didn't write, from repositories they cloned a minute ago. On a laptop, each one runs next to the home folder.

In Bromure, the pane that renders pages runs in a VM that holds none of your files, and the machine where the agent works holds stand-ins. A renderer bug there exposes a disposable Chromium and a set of strings no server will accept.

Source, not a page

The agent reads HTML as text, and the Files pane shows it as highlighted source. Opening a report to review it runs none of its scripts.

A browser with nothing on its disk

Pages render in a separate, disposable Chromium VM. The agent's files and your Mac's files aren't on its disk, and it forgets everything when it closes.

A home folder full of stand-ins

No SSH private key, a fake AWS secret, a synthetic kubeconfig, stand-in registry and git tokens. The real credentials stay encrypted on the Mac.

An alarm on the wire

A stand-in sent to a host it wasn't minted for gets a 451. The VM pauses, an alert names both hosts, and the Security Timeline keeps the record.

Look at the report

"Look at the coverage report" is about as ordinary as requests to a coding agent get. Developers will keep asking it about files in repositories they didn't write, and tools will keep rendering those files to be helpful.

Ask in a Bromure machine, where the page renders in a VM with nothing to read and the agent's keys are stand-ins. For more on stand-ins and the alarm they raise, read The agent leaked its own key and The key was your .env file.