Claude Code Desktop 256KB Preview Flaw: SSH Keys Could Be Stolen

Claude Code Desktop's preview panel had a size-branch flaw: HTML over 256KB bypassed CSP to read local credentials, fixed in 1.30096.1.
Claude Code Desktop's HTML preview panel wrote files over 256KB to a temp file and loaded them via `file://` URL. That path had no CSP or opaque-origin protection, and in Chromium the `file://` origin can read other local files. An attacker could publish a repository with an oversized coverage report; the script executed without a prompt and read SSH keys, AWS credentials, GitHub tokens, and more, exfiltrating them. Anthropic's first patch was bypassed via a `blob:` URL-created Web Worker, and the final fix shipped August 17 with Claude Desktop 1.30096.1. The root pattern — branching on input size with safety checks on only one side — also appears in streaming upload handlers.
The most boring file in any repository is often a code coverage report. You clone a project, ask Claude to check the test coverage, and that HTML report opens in the app's preview panel. According to Noma Labs, before the August 17 patch, that was enough for a script hidden in the report to read your SSH private keys, AWS credentials, and GitHub tokens, and send them to someone else's server.

Noma Security named the research SneakPeek. The flaw was in the HTML preview feature of the Claude Code Desktop app, not in the model itself. Anthropic rated it CVSS v4.0 8.7 (high severity) and, per Noma, treated the impact "as equivalent to code execution on the host." The fix was pushed to users via auto-update with Claude Desktop 1.30096.1.
That hole has been closed. But it's still worth five minutes of your time: confirm you're on the patched version, and understand the shape of this bug — because you're quite likely to make the same mistake in your own code.
Three gaps, only aligned above 256KB
Claude Code Desktop has a Browser panel. Anthropic's documentation says it can "open static HTML files, PDFs, images, and videos in your project," alongside dev servers that Claude launches to inspect its own changes. Noma's report describes how the panel loads HTML files, with file size determining which path is taken:
- Files under 256KB are loaded as
data:URLs, with a Content Security Policy (CSP) ofscript-src 'none'and an opaque origin, so inline scripts are blocked. - Files over 256KB are written to a temp file and loaded via
file://URL. This path never received CSP, and in Chromium, pages with afile://origin can usefetch()to read other local files. - A request filter does block
file://reads, but only when the panel is proxying a dev server. When the panel renders a static HTML file from disk, it isn't active.
CSP, opaque origin, and the request filter all existed, but the large-file path got none of them. The researcher who wrote the report told CSO Online: "These protections are real, they just didn't cover every path."
Small files went through the guarded door. Files over 256KB went through the side door.
Content Security Policy (CSP) is a mechanism browsers use to restrict what resources a page can load and execute, declared via HTTP response headers or <meta> tags. The script-src 'none' mentioned in the article is a CSP directive meaning no scripts may execute, including inline and external scripts. An opaque origin is the origin held by pages loaded from data:, blob:, and similar URLs. The browser treats it as an isolated domain that cannot be queried and cannot access resources from other origins, so cross-origin reads initiated from such an origin are blocked by the same-origin policy. Together, these two ensure that malicious scripts under the data: URL path can neither run nor steal local data.
The file:// origin is entirely different. It's a special origin in the browser representing the local filesystem. In Chromium, file:// pages are granted read access to other local files by default, and both fetch() and XMLHttpRequest can directly access arbitrary paths on disk. This is the key to the vulnerability chain: as long as the preview panel loads large files directly as local files, the page bypasses CSP and opaque-origin isolation and gains the ability to read local files like SSH keys and AWS credentials.
How the attack would have worked
Noma's proof of concept didn't need an exploit kit, just a normal-looking repository:
- The attacker publishes a repository with a coverage report padded past 256KB, hiding a script inside.
- You clone it and ask Claude to review a file, say that report.
- The preview panel renders it, the script runs, and per Noma, no permission prompt appears.
- The script reads files such as SSH keys, AWS keys, npm and GitHub tokens, Kubernetes credentials, Docker registry auth, and Claude's own OAuth token, and exfiltrates them via
navigator.sendBeacon().
Noma's TL;DR adds that the payload could also obtain your real IP address via WebRTC. Neither Noma's post nor its press release reported exploitation in the wild.
navigator.sendBeacon() is an asynchronous data-sending API provided by the browser, designed to reliably transmit small batches of data to a server during page unload or background execution. Its characteristics are that it doesn't block page close and doesn't wait for a server response, so it's often used for telemetry. In this attack chain, it's used to quietly send the read SSH keys, cloud credentials, and tokens to an attacker-controlled server, and because the request happens inside the preview panel, no user-visible popup or permission prompt is triggered.
The companion technology, WebRTC, is a technology that allows browsers to perform direct peer-to-peer audio, video, and data transmission. In a web context, WebRTC's ICE candidate gathering mechanism exposes the local LAN IP and, via STUN servers, the public IP. Even when users are behind NAT or a proxy, attackers can obtain a more accurate real network location than with ordinary HTTP requests. This is the principle behind Noma's TL;DR mention that the payload could obtain the real IP via WebRTC.
The patch was bypassed once
The timeline is the part that should most make you wary of the phrase "we've fixed it":
- July 4: Noma reports the vulnerability chain with a proof of concept.
- July 9: Anthropic triages it and assigns a score of 8.7.
- July 24: Anthropic's patch — blocking pages from issuing
fetch()andXMLHttpRequesttofile://— is sent for retesting. The same day, Noma movesfetch()into a Web Worker created from ablob:URL, a context the filter didn't cover. - Early August: Anthropic confirms the bypass and begins a deeper fix.
- August 17: The fix ships via auto-update with Claude Desktop 1.30096.1 and higher.
Noma speaks kindly of Anthropic's handling: according to its post, the company "took Noma's disclosure seriously from the start." Anthropic did not immediately respond to CSO Online's request for comment. CSO reported that Anthropic was preparing a GitHub security advisory and had applied for a CVE. Noma's post lists both as pending, though its press release says they were published in October. As of public information from multiple sources, no CVE number has been given, so check GitHub's advisory database before looking for one.
A blob: URL is a type of object URL that the browser creates in memory to reference a Binary Large Object (Blob). It looks like a normal URL, but points to data generated by the page itself, not a resource on the network. Anthropic's first patch restricted pages from issuing fetch() and XMLHttpRequest directly to file://, but researchers put the same request inside a Web Worker created from a blob: URL, bypassing the filter.
A Web Worker is a script environment in the browser that runs independently of the main thread. Its characteristic is that it has its own execution context and global scope. Many filters based on main-page request characteristics don't cover network requests initiated from inside a Worker, creating a detection blind spot. An attacker only needs to have the main page create a blob: URL, use it to instantiate a Worker, and initiate a read of local files inside the Worker to bypass a patch aimed only at main-page fetch(). This also shows that allowlist-style patches are often insufficient against composable browser APIs; a real fix needs to return to the loading path itself and route all entry points through the same trust boundary.
What to do now
Check the Desktop app version. Both the vulnerability and the fix are in the Desktop app's preview renderer, so what really matters is the Claude Desktop version. Anthropic's documentation says to open Claude → Check for Updates on macOS or Help → Check for Updates on Windows, and on Linux update via the package manager. You need 1.30096.1 or later. If a machine on your team has had auto-update disabled since the summer, that's the one to look at.
Think about what's on that machine's disk. If anyone on your team previewed HTML from an untrusted repository in the Desktop app before August 17, rotating credentials on that machine is cheap insurance. No real-world attacks have been reported, so treat it as hygiene.
Reduce the value of stolen files. CSO's sources recommend using scoped, short-lived tokens on developer machines instead of long-lived credentials, and observing file reads and outbound connections at the runtime layer, since agent logs may not capture such activity. Expired tokens are useless no matter where they leak.
Take a look before handing a stranger's repository to any agent. The command below lists HTML files over 256KiB in a fresh clone, skipping node_modules:
find . -name '*.html' -size +256k \
-not -path '*/node_modules/*'
A large HTML file by itself proves nothing. Coverage reports and bundled docs get big on their own. It just tells you which files are worth a look in a plain text editor first.
The fix is in place. The habit of checking what an untrusted repository carries remains your own responsibility.
The bug to look for in your own code
Strip away the Electron shell, and this bug is a size-based branch where only one side carries a safety check. Here's a simplified sketch of the pattern Noma described:
// Sketch of the pattern, not Anthropic's code
const LIMIT = 256 * 1024;
function previewUrl(html: string, tmpPath: string) {
if (Buffer.byteLength(html) < LIMIT) {
// CSP blocks scripts, origin is opaque
const b64 = Buffer.from(html).toString("base64");
return `data:text/html;base64,${b64}`;
}
// Big file: written to disk, loaded from file://
// No CSP here, and the origin can read local paths
return `file://${tmpPath}`;
}
Size branches are usually there for performance, and the branch facing large inputs often inherits none of the protections. Upload handlers that stream large files and bypass validator logic have the same shape. Black Duck's head of cybersecurity governance told CSO Online: "Relying on size-based thresholds or client-side filters as a security boundary is inherently fragile."
When you review a branch like this, ask one question: does every path leaving this function go through the same checks? If the answer depends on input size, file type, or which mode the app is in, then the check belongs after the branch, not inside one arm of it.
Size-based branching is common in engineering and isn't a bug by itself. The problem is that when the two sides of a branch cross different trust boundaries, a safety check attached to only one side leaves the other as an unguarded passage. The previewUrl function in the article is typical: small files go through a data: URL and inherently inherit CSP and an opaque origin; large files go through file:// with no protection at all.
There are many similar shapes. For example, an upload endpoint might read and type-check files under a threshold directly, while trusting large files that are streamed to disk; or an API gateway might fully authenticate small request bodies while large bodies take a separate fast path. The correct defense is to place the check after the branch statement, so that all paths pass the same set of validations before leaving the function, rather than duplicating or omitting them in each branch.
The essence of the boundary problem
SneakPeek began as a design problem: the same panel had two entry paths, and only one was inside the trust boundary. The value of this case isn't the specific 256KB threshold, but the universal pattern it reveals — when you design branches with performance-optimization thinking, security protections easily end up applying to only one path.
Agent tools are increasingly opening files, rendering content, and executing commands on your behalf. Every shortcut that lets an agent "just look at it" is a new trust path. While enjoying the convenience, developers need to re-examine whether those paths are really all under the same set of protections.
One-line Summary
Claude Code Desktop's preview panel once had a size-branch flaw: HTML over 256KB could bypass CSP and read local credentials, fixed in version 1.30096.1.
Paragraph Summary
When Claude Code Desktop's HTML preview panel handled files over 256KB, it wrote them to a temp file and loaded them via file:// URL. That path had neither a Content Security Policy nor opaque-origin protection, and in Chromium the file:// origin can read other local files. An attacker only needed to publish a repository containing an oversized coverage report; the script would execute without a prompt during preview, reading SSH keys, AWS credentials, GitHub tokens, and more, and exfiltrating them. Anthropic's first patch was bypassed by researchers using a Web Worker created from a blob: URL, and the final fix shipped on August 17 with Claude Desktop 1.30096.1. The root pattern is a branch on input size where only one side carries a safety check — an antipattern that also exists in scenarios like streaming upload handlers.
Related articles

Uber Deploys ChatGPT Work Company-Wide: AI in the Workplace Enters the Scaled Implementation Phase
Uber deploys ChatGPT Work to 30,000+ employees, extending it from engineering to all roles. We analyze its expert-engineer pairing, end-to-end automation, and questions of AI control and power concentration—insights for scaling AI in the workplace.

Shift at Midnight Hands-On: When "ID Verification" Becomes Alien Hunting
Shift at Midnight blends ID verification with horror survival in a four-player co-op game. We break down its inspection mechanics and chaotic humor.

Zelensky Recounts the Outbreak of War: I Need Ammunition, Not a Ride
Zelensky recalls the first day of war in a YouTube interview: farewell to family, rejecting Biden's evacuation offer with "I need ammunition, not a ride," and how Bucha shaped his stance on compromise and peace talks.