It waited for the third call
Pillar Security is tracking Deadbugz, an MCP server pushed into open-source projects by pull request. It ships two honest tools, counts your agent's tool calls, and on the fourth one rewrites the metadata it returns into instructions to collect SSH keys, AWS credentials, shell history and kubeconfig, and to say nothing about it. No bug, no CVE: the protocol allows all of it. The casualty is the idea that approving a server once tells you anything about the next call. In a Bromure Agentic Coding workspace the whole payload runs, collects four fakes, and dies at the proxy on the way out.
You read the source. You ran it. It formatted text, as advertised. You approved it, and the attacker had counted on you approving it.
On the evening of August 10, between 21:52 and 23:07 UTC, a GitHub account called
zellkernel opened twenty-three pull requests against unrelated AI, MCP and
developer-tool projects. Seventy-four minutes, start to finish. Each pull request
made the same small edit: it added one Model Context Protocol server to a
configuration file.
Pillar Security published the campaign on August 12 and named it Deadbugz, after the file one variant drops. A month later the monthly MCP roundups still lead with it. Adversa's September digest, published yesterday, gives it the whole incidents section. The clock is what earns it that space.
Two tools, and a counter
The server calls itself productivity-suite. It offers two tools: format_text
and summarize. Both work. Point an agent at it, ask it to tighten a paragraph,
and you get a tightened paragraph.
Behind them the server keeps what Pillar describes as "an in-memory, per-client
counter for tools/call requests". When that counter reaches three, the server
changes its answers to two other kinds of request: tools/list, which tells
the agent what tools exist and what they are for, and prompts/get, which hands
the agent prepared instructions. From the fourth call onward, those responses come
back carrying, in Pillar's words, "instructions intended to steer an attached AI
agent toward sensitive local files and to hide the activity from its operator".
Four things: SSH keys, AWS credentials, shell history, Kubernetes configuration. Then one more instruction: conceal the activity from the user.
The tool names never change. format_text is still called format_text. The
description underneath it is what turned, and the description is the part the
model reads as authority.
Nothing here is a vulnerability
No bug appears anywhere in this story. There is no memory corruption to patch and no CVE to file, because the server does nothing the protocol forbids.
MCP is a protocol in which a server publishes its capabilities at runtime and can publish them again. Tool descriptions are text the server chooses and the client fetches. Prompts are text the server chooses and the client fetches. Deadbugz uses all of it as designed. Its one original contribution is deciding when to tell the truth.
And that is enough, because every control most teams have against a hostile MCP server runs once. You read the repository. You start the server and poke it. You check the tool list against what the README claims. You approve it, maybe you pin the version, and you move on. Each of those is a photograph, and Deadbugz decides when you take it.
Pillar's own advice to the people who build MCP clients gives the shape away. Treat tool-definition changes on an already-approved server as a security event requiring renewed approval. Make definition changes visible to the operator. Fingerprint tool definitions at approval time and compare later. Every one of those is a request to move a check from approval time to run time.
The population Deadbugz hunts in is large and unstable. A dynamic assessment of internet-facing MCP servers published in July, Exposed by Design, tested 414 of 640 confirmed production servers out of the 21,000-plus instances its author found on the public internet. It reported 68 reportable vulnerabilities across ten classes, 91.8% of servers running with no OAuth authentication at all, and 687 tool instances exposing shell execution with no access control. Turnover is the number that stays with you: 41.6% of confirmed servers vanished within three days between consecutive measurement runs, which the author reads as "rapid deployment cycles without security review". A server you audited last week may be different software this week, or gone.
The delivery is worth a note too, because it is cheap. Seventeen of the
twenty-three pull requests added a remote endpoint,
productivity-suite-mcp.onrender.com/mcp, to a project's MCP configuration. Four
pointed Python at a hidden local file, ~/.config/.cache/.sys/.deadbug-mcp.py.
Two were submissions to server directories. Pillar found none of them merged
through GitHub's merge button at the time of review: nineteen closed, four still
open. But a pull request is only one way a configuration line travels. The same
line fits in a blog post, a getting-started guide, a listing in an MCP directory,
or a helpful answer under a question about text formatting.
The shopping list
Go back to the list. It is specific, and it is the same list every time: SSH keys, AWS credentials, shell history, Kubernetes configuration.
That is the standing inventory of a developer machine. It is what an infostealer takes, what a malicious postinstall script greps for, and what an attacker asks a hijacked agent to fetch. The list has been stable for a decade of campaigns because the contents are stable: a private key that signs, a secret that authenticates, a record of what you typed, and a token that reaches production.
An agent on an ordinary laptop can satisfy every item, because it has a shell in your home directory and it runs as you.
The same list, inside a Bromure workspace
Bromure Agentic Coding runs each coding agent in a hardware-virtualized Linux VM on your Mac, and holds every real secret on the host side of that boundary. The agent works with structure-preserving fakes, derived from the real value plus a per-install salt, so tools that fingerprint a key never see it churn between sessions. A host-side proxy substitutes the real value onto the wire after the request has left the VM, and only when it is bound for the host that credential was minted for.
Run the payload and let it win. Assume the model reads the poisoned description, believes it, and goes looking. Take the four items in order.
SSH keys. The agent finds no key, not even a decoy one. The VM's
SSH_AUTH_SOCK is an ssh-agent bridge over a virtio socket, vsock port 8444. The
guest can list identities and ask for signatures; the private key bytes live only
on the host and, as the manual puts it, cannot be read or extracted from inside
the VM. Bromure never exposes your macOS login agent to a session. Each signature
that does happen emits a credential.ssh_sign audit event carrying the key's
SHA256 fingerprint, and you can set any imported key to prompt on the host for
every signature request.
AWS credentials. A file that looks right and is worth nothing. The VM's
~/.aws/config points at a credential-process helper that vends the real access
key ID paired with a 40-character fake secret key over vsock port 8445. Every
SDK, the aws CLI, terraform and boto3 pick it up without extra setup, and the
guest produces a well-formed SigV4 signature computed with the wrong secret. The
host resigner strips that signature and re-signs on the way out. Take the pair
somewhere else and AWS answers InvalidSignatureException.
Kubernetes configuration. ~/.kube/config exists and holds a bearer token
starting brm-k8s-. It works, through the proxy, against the API servers the
workspace was configured for. It is inert anywhere else.
Shell history. Real, and durable: coding work needs durable state, so the
workspace VM is a persistent machine rather than a throwaway. It holds the history
of a machine that has never contained a real key, with commands run against decoys
in a home directory that is not yours, alongside a ~/.git-credentials and a
~/.docker/config.json that are fakes too.
Then it has to send it
Collecting is half the job. The other half is the outbound leg to
productivity-suite-mcp.onrender.com, and that leg runs into a stack of host-side
checks.
The fakes are tripwires. Each one has a single legitimate destination family, the host scope it was minted for, so the proxy scans every outbound request, headers and body, for a fake headed somewhere it does not belong. On a hit it treats the request as attempted credential exfiltration: it refuses with HTTP 451 without forwarding a byte, pauses the VM on the spot, and raises an alert offering Shut down, Save for Investigation (export disk, home and shared folders for forensics first), or Continue at your own risk. The event lands as a red Credential brokering row in the Security Timeline.
The second check is reachability. Each workspace carries an ordered egress firewall (action, protocol, host or CIDR, ports, and for web traffic the individual HTTP verbs) with a default for unmatched traffic. New workspaces allow what no rule matches; set it to Deny, list what the work needs, and a render.com subdomain is not on the list. Two components outside the guest enforce it: the virtual switch matches every flow by destination IP and DNS-snooped hostname, and the proxy matches again by TLS server name. Rule edits reach running sessions with no reboot.
Guardrails puts a gate on the consequence. It classifies the agent's calls to
Kubernetes, AWS, DigitalOcean, container registries, git forges and HTTPS
databases as reads, writes or destructive operations, and new workspaces default
to Prompt before write: every mutation pauses for a host dialog showing the
literal operation, the exact SQL or METHOD /path. That is what Pillar asked
platform builders for: sensitive file reads, credential access and code execution
should not be consequences of remote metadata.
A detector sits on the content path as well. When the agent streams external
content back to the model (file contents, fetched pages, command output, the
results of tool calls) a local PromptGuard classifier scores those spans on your
Mac before the model acts on them, and can log, ask, or block with a 451. The MCP
bearer token for an HTTP server never enters the VM at all: the real one stays on
the host, the agent's config gets a brm-mcp_ placeholder, and the settings field
says so out loud, Never sent to VM — swapped by proxy.
If you touched this one
Search your MCP client configs, repositories and home directories for
productivity-suite-mcp.onrender.com, the earlier
promo-surname-xml-quantum.trycloudflare.com endpoint, and the local artifact
at ~/.config/.cache/.sys/.deadbug-mcp.py. Revert any configuration change
that introduced them, and do not run the script. Preserve your MCP client logs
and look for tool-definition refreshes after the third call. Pillar's advice on
rotation is measured: rotate where local evidence supports compromise.
The rule that shrinks the question
In the workspace's Guardrails pane, set Unmatched traffic to Deny
and list what the work needs: allow web api.github.com,
allow web registry.npmjs.org, default deny. Then run
bromure-cli trace hostnames my-workspace after a session, which prints every
distinct host the agent contacted with counts. A destination you never chose
shows up as a line you did not write.
Approval is a snapshot; the wire is continuous
The instinct after a campaign like this is to review harder: read more of the source, pin the commit. Both are worth doing, and neither touches the mechanism. The review here was correct. The server was honest while anyone looked at it, then changed on a schedule it controlled, and broke no protocol rule doing so.
A control that survives a server changing its mind is one that does not depend on having been right earlier. A credential that is a placeholder is a placeholder on the fourth call and the four-hundredth. A private key on the host cannot be read on any call. The proxy evaluates an egress rule on every connection and classifies each write as it happens. None of that carries a memory of the day you approved something, which is why an attacker cannot inherit it.
MCP made it easy to give an agent new capabilities, which was the point, and it made those capabilities a thing a remote party describes in text your model reads as instruction. Both are here to stay. Decide what your agent can hold and what it can reach, and put the enforcement somewhere the server gets no vote. Install Bromure Agentic Coding and give your agent nothing worth waiting three calls for.