What CVE-2026-22708 actually broke
Hand an agent a list of pre-approved commands and you have made yourself a quiet promise: anything worth worrying about will stop and ask first. Pillar Security's disclosure retired that promise. Nothing was corrupted and no privileges were escalated. The dialog was honest and the command behind it was harmless. The trap had been set a minute earlier, somewhere the dialog never looked, by redefining what that harmless command would do.
The whole attack is two lines
The chain needs two switches thrown. The agent has to be in Auto-Run Mode, executing without pausing to ask, and an allowlist has to be turned on.
Command-line tools take instructions from their environment at startup. Git consults PAGER for the program that should display its output; Python consults PYTHONWARNINGS. These are the settings nobody audits, which is exactly what makes them useful to an attacker.
Setting them takes a shell built-in: export, typeset, declare. Built-ins are not programs sitting on disk, and programs on disk were what the check was looking for. Nothing to match, so nothing to show the developer.
# This one runs silently. You are never asked.
export PAGER="open -a Calculator"
# This one you are asked about, and you say yes, because obviously.
git branch
Asked to list branches, Git consulted PAGER for a display program and obediently executed the string it found there. The setting's origin is not fussy — a README, a package somewhere in the dependency tree, a comment on an issue. If the agent reads it, it can carry the instruction.
"Anything that could get text in front of the agent, a README or a dependency or an issue comment, could use them to change environment variables silently. A command the developer then approved, something as ordinary as git branch, would run the attacker’s code instead."/"The commands that change them are shell built-ins, and Pillar’s research names export, typeset and declare specifically, a detail reported independently at disclosure. Built-ins are not programs sitting on disk, and the checker was looking for programs on disk, so they went through without ever being surfaced."/"The developer was shown an accurate prompt, approved a command that was genuinely harmless, and got arbitrary code execution anyway, because the meaning of that command had been changed a minute earlier by something they were never shown."/"Git looked up PAGER to work out how to show the branch list, found the attacker’s command sitting in it, and ran that instead." (Today's Horror Story / The Problem) — from Docker’s official blogView official source →
Why an empty Cursor allowlist changed nothing
An allowlist checks a name: is this command on the list? That genuinely helps against the ninetieth approval prompt of the morning. The catch is that a command's name is not a description of its behavior.
Which is why emptying the list entirely — the strictest configuration Cursor offers — still left the attack intact. Docker notes that Cursor's current documentation labels the allowlist best-effort and warns it can be worked around. Pillar drew a harder conclusion: let agents run whatever they like inside an isolated environment, and retire the allowlist as an industry pattern.
"But the name of a command does not tell you what that command will do. The check reads the name, waves it through, and the setting that decides what actually happens was changed a minute earlier by something the check was never shown."/"Cursor’s documentation now describes the allowlist as best-effort and warns that bypasses are possible. Pillar went further and argued that agents should be handed full command execution inside an isolated environment, and that the industry ought to deprecate allowlists altogether." (The Problem) — from Docker’s official blogView official source →
A 2020 trick that only became dangerous now
The mechanism dates to 2020, when Elttam published research on turning environment variables into code execution, which Pillar's write-up cites. For six years it caused almost no trouble.
"Pillar’s write-up points back to Elttam’s 2020 research on environment variables, which showed how these settings could be turned into code execution." (The Scale of the Problem) — from Docker’s official blogView official source →
Agents removed all three obstacles at once
The 2020 version asked a lot of an attacker: a foothold on the target machine, several settings placed in a particular order, and every step run by hand. At that level of access there were faster ways to do damage.
Coding agents dissolved all three requirements at once. Instructions found inside a file the agent was pointed at get acted on. Steps chain with no checkpoint between them. And the whole sequence executes under the developer's own account. The prerequisite used to be a person at the keyboard; now it is a repository cloned before breakfast.
Docker files it alongside the s1ngularity supply chain attack. There, a tainted package made use of a session an agent already held; here, tainted prose makes use of an approval a command already had. Neither breaks in. Both spend authority granted on purpose, for a purpose nobody granted it for. Driving an agent with planted text is the same mechanism behind Agentjacking.
"It sat there for six years without troubling anybody very much. Pulling it off meant already being on someone’s machine, setting several things in the right order, running each step yourself, and anyone with that much access had faster ways to cause damage."/"Then coding agents arrived and removed every one of those obstacles at once. They act on instructions found in files they were told to read, they run several steps in a row without stopping to check, and they run as you. A technique that used to need somebody sitting at your keyboard now arrives in a repository you cloned this morning."/"It is the same shape as the s1ngularity attack from Part 4. There, a poisoned package borrowed an agent that was already logged in. Here, poisoned text borrows a command that was already approved. Neither one breaks anything. Both of them use permission that was handed over deliberately, for something nobody intended." (The Scale of the Problem) — from Docker’s official blogView official source →
The chain ends with SSH private keys leaving
The end of Pillar's demonstration is the victim's SSH private keys off the machine. Retrace it and the origin is a sentence in a file, acted on by an agent following its instructions to the letter. No memory corruption, no escalation, and not one log line a reviewer would look at twice.
Some variants never wait for an approval at all. Append a few lines to ~/.zshrc and the code fires at every new terminal, which means the repository can be long deleted and the payload is still running next month.
The report went in during August 2025 and the patch landed the following January. Cursor took it seriously and shipped a substantive change: anything its parser cannot classify now demands approval, which shuts the routes Pillar demonstrated. Docker treats those five months less as criticism of the vendor than as evidence of how ill-suited that layer is to fixing this.
"The full chain in Pillar’s research ends with the victim’s SSH private keys leaving the machine." (The Impact)/"No memory bug. No privilege escalation. Nothing in any log that looks the slightest bit out of place." (The Impact)/"One writes extra lines into ~/.zshrc, so the code runs again every time you open a terminal. You could finish the project, delete the repository, and still be running it next month." (Technical Breakdown: How the Attack Works / 2. The half you approve)/"Pillar reported it in August 2025 and the fix shipped that January. Cursor engaged with the report and made a real change, so anything the parser cannot classify now requires approval, which closes the paths that were demonstrated. Five months is a fair measure of how awkward this is to fix at the layer where it was found rather than a complaint about the vendor." (The Impact) — from Docker’s official blogView official source →
What Docker Sandboxes stop, and what they do not
Docker's preferred boundary is not the command line but the execution environment. Docker Sandboxes place the agent in an isolated microVM with a private kernel, a private filesystem, and networking that denies by default. Inside it the agent may do anything at all, sudo included — precisely Pillar's recommendation — and no allowlist exists to be tricked. The attack still succeeds.
Same payload, smaller reach
Fire the same payload inside a sandbox and it works. The environment is rewritten and the approved git branch still sets it off. What changes is the blast radius. The payload hunts for an SSH key and comes up empty, because the home directory lives on the far side of the boundary. The ~/.zshrc variant is dead on arrival: the copy it poisons is inside the box and dies with it.
Getting data back out is constrained as well. Web traffic has to pass a host-side proxy that weighs every request against policy; any other TCP destination has to be named by address and port; UDP and ICMP never leave.
The same payload on your laptop and inside a sandbox
| What happens | On your laptop | Inside a sandbox |
|---|---|---|
| The payload runs | Yes | Yes |
| Where it runs | Your machine, as you | A microVM with its own kernel |
| Your SSH key file | Can be read and copied | Not there |
| SSH authentication | Available, key included | Available, key stays outside |
The ~/.zshrc trick | Persists indefinitely | Gone with the sandbox |
| Sending data out | Open by default | Only where policy allows |
| Who sets the rules | Each developer | The organization |
| Evidence afterwards | None | A logged policy decision |
"Docker Sandboxes run AI coding agents in isolated microVMs, each with its own kernel, filesystem, and deny-by-default network, so a compromised dependency an agent pulls cannot reach the host, its credentials, or other workloads. Inside that box the agent can run anything, including with sudo, which is exactly what Pillar recommends. There is no allowlist to slip past."/"The injection still lands. The environment gets changed, git branch still triggers it, and the payload runs. Nothing about a sandbox stops that. Then the payload goes looking for your SSH key and does not find one. Your home directory sits on the other side of the boundary, so there is no ~/.ssh/id_rsa inside the box to copy."/"The ~/.zshrc trick fails outright, because that file lives on your host and a poisoned copy written inside the box disappears along with the box."/"Getting data out is harder than people expect. HTTP and HTTPS leave only through a proxy on your host that checks every request against your rules, anything else over TCP needs a rule naming the address and port, and UDP and ICMP are blocked outright." (How Docker Sandboxes Contain This at the Execution Layer; the table above is drawn from the comparison in What This Looks Like in Practice) — from Docker’s official blogView official source →
A forwarded SSH agent can still sign for you
To keep routine work such as git push functional, Sandboxes passes an SSH agent socket through to the box — which also means anything running in there can ask that agent to sign on its behalf. Stealing the key is off the table. Borrowing it for the lifetime of the sandbox is not, and the only brake is network policy, which has to name a destination address and port before SSH will connect anywhere.
Docker's security documentation names two further gaps, neither of them a sandbox failure. One is the workspace: mounted live from the host unless told otherwise, which keeps Git hooks and Makefile targets in range, and a tampered hook is invisible to git diff. Launching with --clone gives the agent a private copy and closes that door. The other is the shared skills store, mounted read-write from the host across sandboxes unless you opt out at creation, so whatever one sandbox writes into a skill becomes input for the next one that loads it. The exposure travels between sandboxes and stops there — an altered skill has no path of its own onto the host.
"It can still use the key. Sandboxes forwards an SSH agent socket into the box so that ordinary work like git push keeps working, which means code inside can ask that agent to authenticate on its behalf. It cannot take the key anywhere, but it can borrow it for as long as the sandbox runs."/"Your network policy is what limits that, since SSH needs a rule naming the exact destination address and port before it connects to anything."/"The first is your workspace, which is live on your host by default, so Git hooks and Makefile targets are still within reach and a poisoned hook will not turn up in git diff. Running with --clone hands the agent its own copy."/"Supported agents mount the same host-side store read-write unless you opt out at creation time, which is what lets an agent refine a skill and keep it. Every sandbox sharing that store sits inside one trust boundary, so a skill modified inside one becomes an input to the next that loads it. The store is sandbox state though, and a modified skill does not by itself execute on your host, so the risk runs sandbox to sandbox rather than sandbox to host."/"Isolation has its own seams. In July, Pillar published a series of sandbox escapes across four coding agents, and the mechanism was never a broken sandbox but a file written inside one that a tool outside later trusted. Both caveats above are that shape." (How Docker Sandboxes Contain This at the Execution Layer) — from Docker’s official blogView official source →
Kits move the boundary off each laptop
Part of why the allowlist lost is that it is a per-machine file, maintained by whoever owns the laptop, and an injection can simply reach around it. Docker's counter is the kit: one declarative file naming an agent's credentials, network policy, environment variables, startup commands and mounted files, authored once and handed to the whole team. It gets reviewed, carries a version, and shows up in diffs.
That matters most for the forwarded SSH agent. Write the network rule into the kit and untrusted work has nowhere to reach over SSH unless somebody declared the destination in advance. And once an organization switches governance on, its policy supersedes the local one — a local allow cannot outrank a deny set at the organization level.
Cursor's own product direction is covered separately in Cursor Origin.
"A kit is a declarative YAML artifact that extends a sandbox agent with credentials, network policies, environment variables, startup commands and files. Rather than every developer maintaining a personal allowlist, you write the boundary once, deny-by-default network plus only the destinations a task genuinely needs, and hand the same kit to everybody. It gets reviewed, versioned and diffed like any other file in the repository."/"A forwarded SSH agent is a live credential limited only by network policy, so leaving that policy to whoever remembers to run sbx policy deny is the same per-laptop weak point this whole post has been complaining about. A kit can bake the network rule in, so untrusted work has no SSH egress unless the destination was declared up front."/"If your organisation has governance switched on, the org policy replaces local policy and sbx policy allow and sbx policy deny will have no effect on your machine. You can spot it in the output of sbx policy ls, which begins with a Governance: Managed by <org> line when that is the case. Depending on how admins scope things, some rule types may be delegated back to local control, but a local allow will never beat an organisation-level deny." (Codify the Boundary with Kits / What This Looks Like in Practice) — from Docker’s official blogView official source →
Cursor's documentation and Pillar's disclosure live on different English pages. Converting both to markdown keeps their headings and lists intact, so you can still attribute each claim to the right document while you draft the internal rule.
What to change in AI coding agent security
The lesson of CVE-2026-22708 is that a truthful approval dialog and a safe one are different things. Built-ins executed outside the allowlist and redefined what the next approved command would do, clearing the list changed nothing, and Pillar's chain reached SSH private keys. Version 2.3 shuts the demonstrated routes, but a check that reads names still has no view of the meaning being rearranged behind it. Sharpening the list is the wrong investment. Put unread code behind an execution boundary before the first command runs, and keep that boundary in a file the team shares rather than on individual machines.



