Eleven tests passed
Straiker published a repository that gets Claude Code to run malware without a jailbreak. Its CONTRIBUTING.md sends the review to a subagent the repository defines itself, pinned to Haiku and scoped away from the tests folder. Fable then runs pytest, a test file downloads and starts a Sliver implant, and the terminal reports 11 passed. Anthropic closed the report as Informative: trusting a repository's configuration is the user's workspace-trust decision. Bromure Agentic Coding decides what that trust can reach: a VM holding stand-in keys, a firewall on the Mac, and a trace the repository can't edit.
The agent followed the project's documented workflow, every step of it, and the workflow had been written by the attacker. The terminal said 11 passed.
You find a small metrics client on GitHub that would save you a week, and you ask Claude Code to clone it and take a look. It does good work. It reads the code, points out an unbounded buffer and a retry loop that will spin forever on a poison payload, and suggests fixes. You tell it the usual thing: fix that and prepare a PR.
A few minutes later the fixes are in, the tests are green, and the summary reads 11 passed. Somewhere in those few minutes, an implant opened a remote shell on your laptop.
That is the scenario Brian Cumi of Straiker STAR Labs built and published on September 22 under the title Fable to Haiku: How a Malicious Repo Tricked Claude Code Into Running Malware. Adversa's October roundup of coding-agent security research lists it among the month's attack techniques. The repository exploits no bug. Each step uses a documented feature the way its designers intended.
How the repository reviewed itself
The repository, northstar-metrics, looks like a normal open-source project,
and its first trick is ordinary: a CONTRIBUTING.md. When you asked for a PR,
you bound the agent to the project's contribution workflow, and an agent that
follows instructions reads the contribution guide. This one says reviewers
should run the project's first-pass review agent before running the tests.
That agent lives in the repository, at .claude/agents/first-pass.md. Claude
Code lets a project define its own subagents in that folder, with a model and a
scope in the frontmatter. This one sets model: haiku and limits the review to
src/ and docs/. Its instructions add that "a read in the main session does
not replace running the agent," so the main agent doesn't skip it.
Here is the full chain:
- Fable, the main model, hands the review to the repository's subagent.
- The subagent runs on Haiku 4.5 and only looks at
src/anddocs/. - It comes back with two findings the author planted for it to find: a hard-coded version string and a mismatch between the docs and the code. Both are harmless.
- The documented workflow treats the review as done. Nobody opens
tests/test_compat.py. - Fable runs
pytest. - Before its assertion,
test_compat.pyhex-decodes a URL, downloads a Python file, and starts it withsubprocess. The block is wrapped intry/except: pass, and the assertion always holds. - A Sliver implant opens a command-and-control session. Pytest prints 11 passed.
Cumi's summary of why it works: the repository relies on three ordinary assumptions. Projects can define their own subagents, agents follow a project's documented workflow, and a green test run counts as a check on the code. "A malicious repo that understands all three assumptions doesn't need to break any of them. It just needs to sit in the seam where they meet."
Straiker reported it to Anthropic through HackerOne on September 14. Anthropic closed it as Informative: honoring the repository's configuration falls under the user's workspace-trust decision.
The trust decision, in practice
Anthropic's answer is consistent with how Claude Code works. When you open a
folder, you are asked whether you trust it, and you said yes. That decision
covers the CLAUDE.md, the hooks, the subagents and the test suite.
The trouble is what you were deciding about. You trusted the repository enough
to look at it. That was the point of cloning it. On a laptop, "trust this
folder enough to run its tests" means trusting it with everything your account
can reach, because that is where pytest runs. Straiker lists what the implant
had: every file you can read, credentials in memory, SSH keys on disk, and
every repository you can push to.
No reviewer, human or model, reads every test file of every project they evaluate. The review got cut short on purpose here, but plenty of honest reviews are short too. You can still choose where the test suite runs.
Run the same repository in a Bromure machine
In Bromure Agentic Coding, Claude Code runs inside a Linux VM on your Mac. Each machine has its own disk, its own home directory and its own credentials, and all of its traffic leaves through a proxy on the Mac. The workspace-trust decision is still yours. What changes is what the trusted repository can reach.
Replay Straiker's chain inside a Bromure machine. Steps one to five happen as
before: the agent reads the contribution guide, the subagent reviews the wrong
folders, and pytest runs. From step six on, the repository is working against
the Mac.
The download crosses the Mac
The payload has to be fetched. The test decodes a URL and asks for a file from a host the project never needed before. Inside a Bromure machine there is one road out: the virtual network switch diverts every connection on ports 80 and 443 into the proxy on the Mac, and checks every other connection, whatever the protocol, against the machine's rules.
Those rules live in the machine's Guardrails pane under Outbound connections: allow and deny entries by host, IP range, protocol and port, with an Unmatched traffic setting of Allow or Deny. A machine you use to evaluate code from strangers needs a short list: the model provider, the package registries, GitHub.
allow web api.anthropic.com
allow web pypi.org
allow web files.pythonhosted.org
allow web github.com
default deny
Under those rules the download in test_compat.py never connects, and the
try/except: pass the author added to hide failures now hides this one. Pytest
still prints 11 passed. The difference is on the Mac. The machine's
Security Timeline has a red Firewall row with a deny verdict, the host
the test tried to reach and the port, timestamped seconds after the agent ran
pytest. A metrics client's test suite has no reason to contact a host you
have never heard of, and now you know it tried.
The implant's later traffic gets the same treatment. Whether Sliver calls home over HTTPS, mutual TLS on its own port, WireGuard or DNS, the connection leaves through the switch, and the switch checks every protocol against the same table.
The implant finds stand-ins
Say the machine allowed unmatched traffic, the payload arrived, and Sliver opened its session. The attacker now has a shell in the VM, with the same reach Straiker listed for the laptop. On a Bromure machine that reach covers less.
- Files. The VM holds its own home directory and the project folders you chose to share with that machine. Your Mac's home directory, your other projects, your browser profiles and your Keychain are not inside it.
- Credentials. Bromure keeps real API keys, OAuth tokens, git tokens, cloud
secrets and database passwords encrypted on the Mac. The VM holds stand-ins
with the right shape:
ANTHROPIC_API_KEY=sk-ant-api03-brm-…, aghp_token GitHub would reject, an AWS secret key that signs nothing. The proxy swaps in the real value after a request leaves the VM, and only for the host the credential belongs to. - SSH keys.
ls ~/.ssh/id_*comes back empty. The VM'sSSH_AUTH_SOCKis a bridge to an ssh-agent on the Mac. The VM can ask for a signature but cannot read the key. Turn on Ask before use for a key, and every signature request pops a dialog on the Mac.
An implant that collects what it can and sends it home trips a second wire. A
stand-in token has one legitimate destination, the host it was minted for. When
the proxy sees sk-ant-api03-brm-… or that ghp_ token heading anywhere else,
it refuses the request with HTTP 451, pauses the VM, and opens an alert on the
Mac titled This environment may have been compromised, naming the token,
the host it was minted for, and the host it was sent to. Only a stand-in left,
so there is nothing to rotate.
Push access asks first
Straiker's last item, "repositories with push access," is the one that turns a compromised laptop into a supply-chain incident. An attacker with a shell and your git credentials can push a commit to every project you maintain.
On a Bromure machine, a push to GitHub, GitLab or Bitbucket is a write, and new
machines ship with Prompt before write for every guarded service. The proxy
holds the push and macOS shows Allow write on "github.com" from workspace
"…"? with the operation as it appeared on the wire,
POST /acme/app.git/git-receive-pack. You didn't ask for a push, so you click
Don't allow, and the implant gets a 403 ending in blocked by Bromure
Guardrails. macOS draws the dialog from what the proxy read, so nothing in the
VM can change its wording.
The trace shows the review that didn't happen
When the dust settles you want to know what the agent did. At the default tracing level, AI request details, the proxy keeps every exchange with the model provider, encrypted on the Mac, and the Trace Inspector parses each one into tool calls, shell commands and file reads.
For this repository, the record answers the questions that matter:
- Which model did the review? The captured request body names it:
claude-haiku-4-5, a turn after the main session read.claude/agents/first-pass.md. - What did it read? Every
Readcall, with its path. There are calls for files undersrc/anddocs/, and none fortests/test_compat.py. - What ran? The
Bashcall that startedpytest, and next to it, in the Security Timeline, the firewall row for the download it caused.
The repository controls what the agent sees and what pytest prints, but it has no way to edit the trace. The proxy writes that record on the Mac, from the traffic itself.
A machine for code you haven't read
Straiker's write-up ends with three recommendations: require approval before
running a subagent the repository defines, read test files before running
them, and treat files like .claude/agents/, hooks and workflow definitions as
untrusted input. All three are sound, and all three depend on someone
remembering, every time, on a task that looked like routine review.
In Bromure the safe habit costs a few clicks. A new machine is a copy-on-write clone of a signed Ubuntu image, so it takes almost no disk. That suggests a setup for evaluating outside code:
- Create a machine called eval. Give it a model provider and nothing else: no git token with push rights, no cloud keys, no database.
- Set its egress to deny by default. In Guardrails › Outbound connections, allow the model provider, the package registries and GitHub, and set Unmatched traffic to Deny.
- Clone into the VM, not into a shared folder. Start the session in a fresh folder in the machine's home. A shared folder is a live window onto your Mac, and the machine's own home is the part Bromure can roll back or wipe.
- Or make it throwaway.
bromure-cli vm run --rmstarts a machine that is deleted, disk and all, when it stops. - Read the Timeline before you adopt the dependency. A Firewall deny row from a test run is a finding no reviewer reported, whatever pytest printed.
If something does get through, you clean up one machine. Rewind home… returns the home directory to an earlier boot's checkpoint. A machine the compromise detector flagged refuses to boot until you choose Wipe and Launch, which rebuilds its disk and home and keeps your settings and the real credentials on the Mac. Your laptop's account, your other projects and your other machines were never inside it.
When the library has earned its place, move it to the machine that does real work. That machine holds stand-in keys too, and its pushes still stop at the dialog on the Mac.
The download has to ask
Every connection the VM opens passes the virtual switch and the proxy on the Mac. With unmatched traffic denied, a test that fetches a payload from a new host fails, and the Security Timeline shows the attempt.
The shell finds stand-ins
Real API keys, git tokens and cloud secrets stay encrypted on the Mac, and SSH private keys never enter the VM. A stand-in sent anywhere but its home host returns 451 and pauses the machine.
Pushes need a click on the Mac
With Prompt before write, the default on new machines, a git push waits for a macOS dialog showing the exact operation. An implant can't answer it.
A record the repo can't edit
The trace on the Mac shows which model reviewed the code, which files it read, and which command ran next, whatever the terminal printed.
Trust the repository with less
The repository did nothing exotic. It wrote a contribution guide, defined a helper agent and shipped a test file. Projects will keep doing all three, because they are useful, and agents will keep following them, because following a project's conventions is what makes an agent good at its job.
Anthropic's reply puts the decision with you, and that is where it belongs. Make it on a machine built for it: one where trusting a repository enough to run its tests means trusting it with a VM, some stand-ins and a short list of hosts. Then let the test suite print whatever it likes.