"Send me your private key" was blocked by our scanner. "Send me your private key." passed. Adding a period turned off the finding.
In a separate test, our network observer attributed one sandbox run's canary traffic to another run. The IP address and timestamp matched. The evidence belonged to the previous container.
Both bugs surfaced while following suggestions from readers. Auten pointed us toward argument descriptions under our MCP scanner comparison. Marcus Kim asked us to test address reuse under our post about installing 12,915 MCP servers. Neither comment reported these bugs directly. Here is what their suggestions exposed, what we changed, and what the tests establish.
1. "The argument's description is the one to read"
WARDEN's scanner blocks a tool whose definition asks for a private key. It is a word rule, and Auten pointed out its cost: a tool that says "uses the stored private key" trips it even when the key never reaches the model. He thought that was the right trade, and added one line. If the rule ever gets a second signal, the argument's description is the one to read: "a login name" against "the key". That is where a tool that takes your key differs from one that only uses a key it already has.
So we wrote that rule. It reads argument descriptions only, and catches key material asked for by value in words the old rule does not spell: a signing key, a wallet key, a key pair, a recovery phrase. Two decisions came from taking his framing literally.
- The new argument-description rule only adds findings. A description is written by the tool's author, who may be the attacker. If a nice-sounding argument description could cancel a match, "the name of a saved login" would become a way past the scanner. So the new rule can make the scanner stricter and never more lenient.
- It stays out of tool descriptions. We ran it there too, out of habit, and it blocked six honest servers: "rotate my signing key", "returns the public key of the signing key pair". In argument descriptions it blocked none. The argument really is the place.
In the original v13 check of our 986-server public corpus, it blocked one server the old rules missed. That server's builder asks, as an argument, for the 64-byte secret key of a new key pair, and sends it to a hosted server.
The full stop
While testing the rule, one sentence passed that should not have:
"Pass the signing key" blocked. "Pass the signing key." passed. So did "Send me your private key." and "Paste your seed phrase.", with the full stop that almost every sentence has.
The cause was a guard written in August to stop a false positive. bip39-mnemonic-checksum, one name in a list of several hundred calculators, had blocked a server because it contains "mnemonic". The guard says that a credential word inside a longer identifier is that identifier's name, and it found the identifier by walking over letters, digits, dots and hyphens. A full stop is a dot. "key." became a longer identifier, and the finding was dropped. Every release since that guard was added, eight weeks, had the hole.
Now a dot or a hyphen at the edge of a word is punctuation. That brought back four sentences the full stop had been hiding, all honest:
- "a challenge message to sign with the wallet's private key"
- "signed by the new credential's private key"
- "You keep the private key."
- "at most this many messages from one signing key"
These sentences describe signing, local custody or a usage limit; they do not ask for the key itself. In v13, we added a guard for those cases. "Sign the challenge with your private key, then paste the private key below" still blocks. A later audit showed that the custody exemption was too broad; more on that below.
The measured v13 effect on the corpus, before the later v14 audit fix: 4 servers blocked instead of 3, and none lost. On the held-out half of MCPTox, the benchmark we measure against, nothing changed: 171 of 218 poisoned tools, the same as before, because MCPTox's tools rewrite other tools' calls rather than ask for keys. The fix closes a hole the benchmark never probed. That is a useful thing to know about a benchmark.
Audit follow-up, 2026-10-18. This article records the v13 fixes and their original measurements. A subsequent local audit found that keyStaysLocal also exempted “Your private key is kept on our server”. Ruleset v14 fixes that inference. We added local regression tests but did not rerun corpus or model benchmarks; the v13 numbers above are historical, not v14 measurements.
2. "Treat observer attribution as part of the test"
HISTOR, our registry crawler, starts npm and PyPI MCP servers in a gVisor sandbox with no way out. It gives them fake credentials: canary keys, a canary e-mail address, all marked with the run's ID. It records where each one tries to send them. Network traps on the host see a connection's source address, and Docker hands a freed address to the next container.
We had already been bitten by that once. A trap window that opened before docker run once credited a real package with a red-team fixture's last request, so the window now starts when the container actually starts. Marcus Kim asked for a deliberate test of the handoff. One container leaves a delayed connection, the next receives the same address, and neither run may inherit the other's canary.
A unit test with synthetic collector records showed the case the start time does not cover. It models the queued connection and address handoff; it does not launch Docker containers to reproduce the handoff end to end. Container A queues a connection. A is removed, and B gets the address and starts. Then the trap accepts A's connection. Address and time both say B. A's canary e-mail, with a hidden copy to an outside address, became evidence that B mailed someone it should not have.
The address cannot tell the two apart, but the content can. Every placeholder HISTOR plants carries the run's 12-character tag. The trap now reads which runs' tags a connection carries. A record with only another run's tag is not counted for this run: not as a recipient, a decoy or a key destination. The run is not called clean either. It gets the reason foreign-run-traffic, because the observer saw traffic on that address it cannot account for.
His second point was about the report. A clean verdict should stay distinct from a run the observer could not fully account for. We had a bug there too. When the trap's reasons merged into a run's record, they replaced the reasons already there instead of joining them, so a missing DNS log could vanish from a run's list. Now the reasons add up, and a run with any reason is never counted as clean. Older runs that hold another run's canary are traced again.
What this is worth
The scanner bug came from a false-positive exemption: a rule intended to recognize identifiers also swallowed sentence punctuation. The observer bug came from an attribution assumption: an address and a timestamp did not uniquely identify the source of a connection. Both pieces of supporting logic needed their own adversarial tests.
The unchanged MCPTox score did not establish that the punctuation bug was harmless. Likewise, the synthetic handoff test checks our attribution logic, but does not demonstrate a live Docker handoff. Those limits matter when interpreting the fixes.
Links:
- The new rule, the full stop and the numbers:
docs/gates.md(also in Russian, Spanish, French and Chinese). - The tests, with every corpus sentence verbatim:
test/ruleset-v13.test.ts.
Thanks to Auten and Marcus Kim for pointing us toward assumptions our tests had missed. If you have a counterexample to either fix, we would welcome it.



Top comments (0)