Back to all posts
Published on · by Renaud Deraison

The symlink left the workspace, and found nothing worth taking

GhostApproval showed how a hostile repository can trick six AI coding assistants into writing and reading files outside the folder they were supposed to stay in: planting SSH keys, hijacking shell startup, and reading cloud credentials on the developer's own machine. The bug is a symlink-following mistake. The reason it hurts is that the agent runs on a machine full of your secrets. Bromure Agentic Coding changes that second fact.

A coding agent is a program you invite to read and write files on your laptop. GhostApproval is the story of what happens when a hostile repository convinces that program to write a little outside the lines: into your SSH keys, your shell config, your cloud credentials. The bug is old and dull. The damage comes from where the agent was standing.

On July 8, 2026, the research team at Wiz published GhostApproval, a write-up of the same flaw found, in six different forms, across six popular AI coding assistants: Amazon Q Developer, Anthropic's Claude Code, Augment, Cursor, Google's Antigravity, and Windsurf. The details differ per product. The shape does not. You open a repository, maybe a tutorial someone linked or a project a teammate asked you to look at, and you point your coding agent at it. Somewhere in that repository is a file that is not a file.

A file that is not a file.

The trick is a symbolic link, or symlink: a filesystem entry that looks like a file but is a signpost pointing at some other path. Symlinks are a normal, forty-year-old Unix feature. The security problem, catalogued as CWE-61, is almost as old: a program that opens a symlink without checking where it points lets an attacker steer it toward a file the author never intended.

In GhostApproval, a malicious repository ships a symlink named to look innocent, say project_settings.json. Instead of settings, it holds a pointer at ~/.ssh/authorized_keys, or ~/.zshrc, or ~/.aws/credentials, sitting in the developer's home directory, well outside the repository folder. A README, or the user acting on the repo's instructions, then asks the agent to "update project_settings.json." The agent follows the signpost without resolving where it leads, and writes to the real target instead.

WORKSPACE — the cloned repoproject_settings.jsonlooks like a config file — is a symlink"Please update project_settings.json"— the repo's README, obeyed by the agentWORKSPACE BOUNDARYassumed safe — the assumption is the bugDEVELOPER'S HOME (~) — outside the repo~/.ssh/authorized_keys~/.zshrc~/.aws/credentialsreal host access · real cloud keys · shell run on every loginagent follows the linkCWE-451 — THE PROMPT MISREPRESENTS WHAT IS ABOUT TO HAPPENAgent's real target: ~/.ssh/authorized_keysShown to you: "Edit project_settings.json? [Yes]"
How GhostApproval works. A cloned repository contains a symlink disguised as a config file; it points outside the workspace into the developer's home directory. When the agent edits it, the write lands on ~/.ssh/authorized_keys, ~/.zshrc, or ~/.aws/credentials. Worse, in several products the confirmation prompt showed the harmless name the agent was told to edit, not the sensitive path it was about to touch (CWE-451).

Two things make this more than a curiosity. The first is what the targets do. Appending a line to ~/.ssh/authorized_keys installs an attacker's key for passwordless SSH into your machine. Writing to ~/.zshrc runs the attacker's command every time you open a terminal. Augment's agent did not even need to write: asked about "AWS keys in the project," it walked a symlink out to a credentials file and printed the secrets into the chat, with no prompt at all.

The second is a subtler failure that Wiz classifies as CWE-451, user interface misrepresentation. With Claude Code, the agent's own internal reasoning recognized the file as a shell config, and the confirmation dialog still asked only to "Make this edit to project_settings.json?" The one moment where a human could have caught the danger showed the human the wrong information. Windsurf and Amazon Q Developer had it worse: they wrote the file to disk before showing the approval buttons, so the dialog was an undo prompt rather than a gate. The attacker's SSH key was already in place.

Most vendors have since fixed their variant: Cursor in v3.0 (CVE-2026-50549), Amazon Q in language-server 1.69.0 (CVE-2026-12958), Google in Antigravity v1.19.6, Claude Code with a symlink-resolution warning. That is the right response, and you should take the updates. But patching each product's symlink handling fixes the mechanism and leaves the stakes where they were.

The stakes are set by where the agent stands.

Every one of these payloads assumes the same thing: that reaching the developer's home directory is worth reaching. That ~/.ssh holds a key that opens a real machine, ~/.aws/credentials holds a live cloud token, and ~/.zshrc runs on a shell you will open tomorrow. The symlink is the road. The destination is a laptop with your whole working life on it.

Most coding-agent attacks rest on the same assumption. The agent is a capable, open-ended program that reads untrusted input (repositories, web pages, package tarballs, tool output) and takes actions on a machine where your credentials sit in plain files a few directories away. Harden the agent all you like; it shares one filesystem and one keychain with everything else you run. Attackers will keep paving new roads. The destination is the problem.

Bromure Agentic Coding removes the destination. The agent, the shells it spawns, the repositories it clones, and any code inside them run inside a per-profile Linux VM: a separate machine that the hypervisor walls off from your Mac. Your real credentials never enter it. We wrote about why a boundary beats a cage in why Bromure Agentic Coding is not a sandbox; GhostApproval is a clean test of the idea, because it escapes the workspace folder, the one small boundary the industry was relying on.

Agent on your MacONE MACHINE, ONE HOME DIRagent + cloned reposymlink → ~/.ssh/authorized_keys~/.ssh/authorized_keysREAL~/.aws/credentialsREAL~/.zshrcREALResultattacker's SSH key installed,cloud keys read, shell hijacked —on the machine you work from.Bromure Agentic CodingPER-PROFILE VM — DISPOSABLEagent + cloned reposymlink → ~/.ssh/authorized_keyswrite still happens~/.ssh/authorized_keysSTUB~/.aws/credentialsSTUB~/.zshrcguestResulta key added to a throwaway guest,placeholder cloud keys exfiltrated —your Mac never entered the room.
The same symlink escape, two machines. On a normal setup the agent runs beside your real ~/.ssh and ~/.aws, so following the link reaches live keys and a machine worth taking over. In Bromure the agent runs in a per-profile VM; the symlink still resolves and the write still happens, but it lands in a disposable guest whose home directory holds only stubs: placeholder files that mean nothing on the public internet. Your Mac's filesystem sits below the hypervisor line and is never mounted into the VM.

Trace the same escape inside the VM. The symlink still resolves (Bromure never needs to detect or block the trick) and the write still lands on ~/.ssh/authorized_keys. But that file lives in the guest's home directory, on a disposable machine that has no route to your Mac. The attacker's key now grants login to a machine you will throw away, one that was never reachable from the outside in the first place.

The credential-theft variant is even more anticlimactic. When a compromised agent walks a symlink to ~/.aws/credentials and prints the contents into the chat, it reads a stub: a well-formed credentials file whose keys authenticate to nothing. Real tokens stay on the host, behind a broker that swaps the placeholder for the real secret only at the moment of an outbound call you allowed, and swaps it back out of the response. A dependency or a tricked agent that sweeps the filesystem for secrets finds furniture.

The prompt, and the second net.

Bromure meets GhostApproval's two sharper edges at different layers.

The first is the confirmation-prompt problem, CWE-451: the dialog that said project_settings.json while the agent knew it meant ~/.zshrc. Bromure drops the file-write dialog; writes inside the disposable VM touch an expendable disk and need no approval. Bromure inserts the human at the real boundary instead, for state-changing calls that leave the VM: a git push, a database DROP, a cloud Terminate. Those pause on the host and show you the literal operation rather than a summary the agent wrote. It is the same principle GhostApproval violated, applied where it matters: the moment a decision has consequences outside the sandbox is the moment a human should see the real call.

The second edge is the instruction that starts the whole attack: a README telling the agent to "update project_settings.json," which the model reads as context and obeys as an order. Bromure reads that untrusted content first, on-device: a local classifier scores incoming files, web fetches, and tool output for instructions that have no business being there, and the rules files an agent treats as standing orders get a sterner pass. The classifier will miss some phrasings, and isolation defeats this attack on its own; treat it as a second net under the first.

Where the line does not save you.

Isolation is a specific shape, and the shape has edges.

A profile is long-lived, so persistence persists.

The ~/.zshrc payload will run the next time a shell opens inside that profile's VM. It runs to a guest with no host keys and only stub credentials, a shell in an empty room, but it does run. If that worries you for a given task, run it in a fresh profile.

What you mount, you expose.

The VM holds nothing of yours by default. If you deliberately mount a real host directory into a profile, or paste a live token into the guest, a symlink escape can reach it. The line keeps your secrets out; putting them back in is a choice only you can make.

Scope the broker on purpose.

Stubs mean the file on disk is worthless, but the broker still holds real tokens for the calls you allow. A profile that only needs to read a repo should not carry a token that can publish it. Isolation contains the blast; scoping decides how large it could ever be.

The workspace was the wrong wall.

GhostApproval is, at heart, a story about what the industry agreed to treat as a boundary: the workspace folder, the idea that "the agent stays in the project directory" is a security property rather than a default any signpost can step around. The symlink itself is a bug the vendors can and did patch. Draw your boundary at a folder and you will spend forever proving that no symlink, no .., no clever path ever leaves it. Draw it at the hypervisor and the question stops mattering, because the place the agent escapes to is a disposable machine that never held anything of yours.

Take the vendor patches; they close a real hole. But the hole hurt because the agent was standing on your laptop, one directory away from your keys. Move the agent off your laptop and the next escape (there will be one) arrives in an empty room. Bromure Agentic Coding is free, open-source, and shipped today.