A lowercase bash slipped past our rm -rf / block
While wiring up the DeepSeek Harness plugin, our first live test sent this through the gateway:
{ "tool_name": "bash", "tool_input": { "command": "rm -rf / --no-preserve-root" } }
The answer came back allow.
Our hardline block never fired. That’s the layer that always denies recursive deletes of root or home, filesystem formats, and fork bombs, whatever any policy says. Neither did the per-command classification, the exfiltration checks, or any Bash.* rule. The call was checked as a generic tool and skipped everything that knows what a shell is.
The cause was one character of case. Claude Code names its shell tool Bash. DeepSeek Harness names it bash.
It wasn’t one bug
Every shell-aware check in our gateway matched Claude Code’s tool names: Bash, Read, Write, Edit, WebFetch. That was fine when Claude Code was the only harness we covered. Then came Codex, Cursor, Hermes, opencode and dsh, and each spells its tools differently: bash, run_shell_command, run_terminal_cmd, str_replace_editor, web_fetch.
Nothing failed loudly. The lowercase bash call wasn’t rejected. It got generic treatment. Policy still ran, the audit row still wrote, denies still denied. The dashboard looked normal. The shell-specific checks just didn’t apply, and no alert or user report would ever have told us. We found it because we sent the harness’s real tool names at production and looked at what the hardline block did.
The fix: one table, applied once
The tempting fix is per-plugin: teach the dsh plugin to send Bash, teach the opencode plugin to send Bash. That fixes two harnesses, and the next one reopens the gap. It also makes the audit log wrong, because the harness never called a tool named Bash.
Instead we added one alias table at the entry to classification. The hardline block, the classifiers, the policy rules and both governance endpoints already read from there. Here’s an excerpt:
const HARNESS_TOOL_ALIASES: Record<string, string> = {
bash: "Bash", pwsh: "Bash", shell: "Bash",
run_shell_command: "Bash", run_terminal_cmd: "Bash",
read: "Read", write: "Write", edit: "Edit",
str_replace_editor: "Edit", apply_patch: "Edit",
web_fetch: "WebFetch", web_search: "WebSearch",
// exact matches only; unknown names pass through untouched
};
Matches are exact on purpose. A fuzzy rule would treat a customer’s unrelated tool called bash_helper as a shell. Unknown names keep the generic treatment, and rules written against a harness’s own spelling still work, because the original name is always tried too.
The repro above now comes back deny with the hardline reason, and it did before the dsh plugin shipped to anyone. New harnesses are one line in a table instead of an audit of five layers. The table has grown since this post; the excerpt shows the idea.
If you run rules that match on tool names
This applies to any allowlist, SIEM rule, tool filter or injection scanner that matches on tool names. The rules end up written in the dialect of the first agent you covered. Three questions:
- What happens to a name your rules don’t match? If the answer is “generic handling” instead of “flagged as unknown,” you have partial coverage and no signal.
- Have you tested each harness’s real tool names? Use actual traffic, not the docs.
- Where do names get normalized? If it’s in each client, every new client is a chance to forget. Do it once at the choke point, and log both the native name and the canonical one.
Which harnesses are checked, which can ask, and which can rewrite results is on the harness coverage matrix.
Running dsh? One command adds a native plugin. Each tool call is checked and recorded. A block is reported to the agent but not enforced by the plugin yet.
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex
What the installer writes · dsh guide · free up to 5 agents