← Back to all posts
Published on · by Renaud Deraison

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:

  1. Fable, the main model, hands the review to the repository's subagent.
  2. The subagent runs on Haiku 4.5 and only looks at src/ and docs/.
  3. 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.
  4. The documented workflow treats the review as done. Nobody opens tests/test_compat.py.
  5. Fable runs pytest.
  6. Before its assertion, test_compat.py hex-decodes a URL, downloads a Python file, and starts it with subprocess. The block is wrapped in try/except: pass, and the assertion always holds.
  7. 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.

On a laptop: the repository writes the review, the tests run as younorthstar-metrics (the repo)CONTRIBUTING.md"run the first-pass agent".claude/agents/first-pass.mdmodel: haikuscope: src/ docs/tests/test_compat.pyoutside the review's scopeno jailbreak, no CVE:only documented featuresClaude Code1. Fable delegates the review2. Haiku reads src/ and docs/3. two planted, harmless findings4. review marked complete5. $ pytest===== 11 passed =====Your laptop, as you6. download payload, run it7. Sliver session opensThe implant can reachevery file you can read~/.ssh/id_ed25519~/.aws .env tokenspush to every repo
Straiker's chain on a laptop. The repository steers the review to a cheaper model that never sees the tests, pytest runs the payload as the developer, and the implant inherits the developer's reach. The only thing the user sees is the last line.

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-…, a ghp_ 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's SSH_AUTH_SOCK is 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 Read call, with its path. There are calls for files under src/ and docs/, and none for tests/test_compat.py.
  • What ran? The Bash call that started pytest, 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.

In Bromure: the tests run in a VM, and the exits are on the MacMachine VM: "eval"~/northstar-metrics$ pytest → test_compat.pyWhat an implant would findANTHROPIC_API_KEY=sk-ant-api03-brm-…GH_TOKEN=ghp_… (stand-in)~/.ssh: no private keysthis machine's home onlyno Mac home, no other projectsevery connection leaves via the switchSwitch + proxy, on the MacFirewall: payload host → denyred row in the Security TimelineStand-in sent off-scope → 451, VM pausedTrace: model claude-haiku-4-5,Read src/… docs/…, Bash pytestmacOS dialog: Allow write on …?POST /acme/app.git/git-receive-packreal keys swapped in only for their hostInternetapi.anthropic.compypi.orggithub.compayload host:never contactedKept on the Mac, outside the VMAPI keys and OAuth tokens · git, cloud and database credentials · SSH private keys · encrypted traces · Security Timeline
The same repository in a Bromure machine. The review still goes to Haiku and pytest still runs, but the VM holds stand-in credentials and no private keys, the payload download stops at the egress firewall on the Mac, pushes wait for a macOS dialog, and the trace records which model reviewed what.

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:

  1. Create a machine called eval. Give it a model provider and nothing else: no git token with push rights, no cloud keys, no database.
  2. 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.
  3. 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.
  4. Or make it throwaway. bromure-cli vm run --rm starts a machine that is deleted, disk and all, when it stops.
  5. 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.