The token was in the same heap
Oren Yomtov of Accomplish escaped OpenAI's Codex sandbox twice. One escape ran from read-only, the strictest mode: untrusted JavaScript called v8.getHeapSnapshot() and read the trusted context's authorisation token out of the heap the two of them shared. The other made apply_patch widen its own write permission to the filesystem root, then append a line to .zshrc. OpenAI fixed both in eight days. The property that produced them has no patch: a guard and the code it guards were two variables in one process. Bromure Agentic Coding runs the guard on your Mac and the agent in a VM.
The untrusted context never broke the sandbox. It asked Node for a snapshot of the heap, found the sandbox's own authorisation token sitting there as an ordinary string, and used it.
You point Codex at a repository somebody sent you. You are careful, so you run it
in read-only, the mode where the agent can look and cannot touch. The agent
reads the code. Some of that code is a payload, and a few seconds later a command
is running on your Mac, outside the sandbox, with no prompt on screen.
That is Heapjack, one of two Codex sandbox escapes Oren Yomtov of Accomplish published this month. BleepingComputer covered them on September 20. Yomtov reported both to OpenAI on August 12, and OpenAI fixed them within eight days: Heapjack in Codex Desktop build 26.818.21641, the other in Codex CLI 0.149.0. OpenAI's statement is the short kind: "We thank the researchers for contacting us and sharing their findings. We addressed both issues in August, but we're continually strengthening our sandboxes."
Why the first bug existed is the part that outlasts the patch.
One process, one heap, one token
Codex Desktop has a tool called node_repl that runs JavaScript. It keeps two
contexts inside it: a trusted one, which is allowed to ask the parent process to
do real things, and an untrusted one, which runs whatever the agent is
evaluating. The separation between them is the sandbox.
Both contexts live in the same Node process. In Yomtov's words: "One Node process, one V8 isolate, one heap shared by both contexts. The token is a string in that heap."
The token is how the parent process tells the two apart. A request arriving on the pipe with the right token is trusted and gets executed; one without it is not. It is a random UUID, which is a fine secret as long as the code you are keeping it from cannot read your memory.
It can. v8.getHeapSnapshot() is a public Node API, a profiling tool, and it
returns a dump of every object in the heap, strings included. The untrusted
context calls it, filters for things shaped like a UUID, and ends up with a
handful of candidates. Picking the real one out takes one more step: try each
against the parent and watch what comes back. A wrong token answers "not
authorized". The right one answers with a complaint about the rest of the
request. Yomtov keeps the string that produces the second reply.
With the token in hand, the untrusted context writes requests onto the same
stdout pipe the trusted one uses. The parent checks the token, finds it valid,
and runs the command unsandboxed. From there: system commands through open, the
Docker socket, edits to configuration files.
All of that from read-only. Quoting Yomtov again: "All of it ran at read-only,
the strictest sandbox mode, where the agent supposedly can't write anything."
A write permission that grows
Overpatch, the second escape, is plainer. Codex CLI has an apply_patch tool for
editing files, and it works out how much write access it needs from the paths
named in the patch it was handed. The paths come from the model, and the model is
reading the repository.
So a patch names /tmp in one entry with a change that does nothing. The change
fails. The permission grant it implied does not: write access widens up to the
filesystem root. A second entry in the same patch then writes through a symlink
into $HOME and appends a line to .zshrc.
Nothing has escaped yet. The escape happens later, when you open a terminal and
your shell runs that line as you, with no sandbox anywhere in the path. This one
ran in workspace-write, the everyday mode.
Two different bugs, two different products, one sentence from the write-up that covers both: "The thing doing the enforcement was sitting inside the thing being enforced."
The guard and the guarded, in one address space
A sandbox that lives in the same process as the code it restrains is a convention. It holds until somebody looks hard at it. The token is in the heap. The permission calculation reads attacker-supplied paths. The pipe is the same pipe. Each of those is fixable, and OpenAI fixed each in eight days. The next bug of this kind will be a different string in the same heap.
Yomtov's own conclusion is the architectural one: "That is why Accomplish runs the whole agent inside a VM… We treat everything in the guest as untrusted, root included, because nothing that decides what is allowed is running in there with it."
That is the design Bromure Agentic Coding ships. The agent runs in an Ubuntu 24.04 guest under Apple's Virtualization.framework, the hypervisor macOS itself provides. The guest is untrusted by construction, root included. The code that decides what is permitted runs as a host process on the other side of a vsock, and no token inside the guest authorises anything, because everything worth authorising sits outside the guest.
What each escape meets instead
Overpatch is the concrete one, so start there. The trick ends by appending to
.zshrc in $HOME. In a Bromure workspace the guest's $HOME is home.img, an
ext4 image attached to that VM. Your Mac's home directory is not inside it. What
the guest can reach on your Mac is the project folders you shared into it: up to
eight, chosen by you, listed in the workspace editor. The dotfiles your shell
reads when you open Terminal are not among them unless you put them there.
Heapjack is the harder case. Nothing in Bromure detects a heap-snapshot trick inside the guest, and nothing is meant to. What changes is where the escape arrives. In Codex the escaped code landed in the account that holds your SSH keys and your cloud credentials. In a Bromure workspace it lands in a Linux VM where:
The credentials are placeholders
sk-ant-api03-brm-…, a ghp_-shaped string, a ~/.aws profile whose secret
is invalid. They are structure-preserving, so tools accept them, and
worthless upstream. The real values live in the proxy's memory on the host
and are swapped in on the way out, scoped exact-or-subdomain to the host they
belong to.
Carrying one out trips the wire
The proxy scans headers and bodies of every outbound request for the workspace's own fakes. A fake headed somewhere it was not minted for is blocked with HTTP 451, with not a byte forwarded. The VM pauses, and a red Credential brokering row lands in the Security Timeline.
The network is a list you wrote
Egress is an ordered ruleset ending in default allow or default deny. The
virtual switch diverts the VM's port-80 and port-443 flows into the host
proxy with no environment variable involved and nothing the guest can unset.
Edits apply to a running session immediately.
A push is a question, asked on the Mac
Git forge credentials carry a write policy, and new workspaces default to
Prompt before write. git push is a write. The dialog names the exact
operation and renders on your desktop. An attacker inside the VM cannot
click it.
When it does go wrong, you recover with a menu item. The compromise flow wipes the workspace's disk and home and keeps your settings, tokens and SSH keys. The real credentials never left your Mac, so you rotate nothing. Reset disk re-clones the system image. No Bromure action touches your shared project folders, and the prompt says so.
The strictest mode was the one that lost
The developer in this story did everything right. They chose the strictest
setting the tool offers, on purpose, because the repository was not theirs.
read-only is a real feature and it did real work, until a string in a shared
heap turned it into a suggestion.
Modes, allowlists and approval prompts are worth having, and every one of them assumes the code enforcing them sits out of reach of the code it constrains. That is a claim about memory layout, and you can go and check it. Yomtov checked Codex twice, across two products, and the answer came back no both times.
So run the agent somewhere the answer stops mattering. Give it root, let it install what it likes, let it read the repository somebody sent you, and keep every credential, every destination decision and every push confirmation on the other side of a hypervisor, where the next escape arrives in a machine you were already willing to throw away.