# Your control plane speaks Claude Code. Your agents don't.

A live repro from this week: a recursive root delete sent as lowercase `bash` sailed past our hardline floor, because every name-keyed layer matched Claude Code's tool spellings exactly. The fix was one alias table. The lesson is about every control layer that keys on tool names.

While wiring the [DeepSeek Harness plugin](/blog/deepseek-harness-acp-integration) this week, the first live test sent this through our gateway:

```json
{ "tool_name": "bash", "tool_input": { "command": "rm -rf / --no-preserve-root" } }
```

The answer came back **allow**.

Our hardline floor — the unconfigurable layer that blocks recursive root deletes, filesystem formats, and fork bombs no matter what any policy says — never fired. Neither did per-binary command classification, domain extraction, exfiltration detection, or any `Bash.*` policy tier. The call evaluated correctly at the generic tool tier and skipped everything that knows what a *shell* is.

The bug is one character of case. Claude Code spells its shell tool `Bash`. DeepSeek Harness spells it `bash`.

## The class, not the incident

Every layer that made our gateway smart about shell commands was keyed on Claude Code's tool names — `Bash`, `Read`, `Write`, `Edit`, `WebFetch`. Reasonable when Claude Code was the only harness we governed. Then came Codex, Cursor, Hermes, opencode, and now dsh, and each spells its tools differently: `bash`, `run_shell_command`, `run_terminal_cmd`, `str_replace_editor`, `web_fetch`.

Here's the part worth being honest about: nothing failed loudly. A lowercase `bash` call wasn't rejected — it was governed *generically*. Policy still ran, the audit row still wrote, denies still denied. Everything looked fine in the dashboard. The name-specific intelligence just silently didn't apply. A coverage gap that presents as normal operation is the worst kind, because no error budget, no alert, and no user report will ever surface it. Ours surfaced only because we ran the harness's real spellings against production and checked what the hardline floor did with them.

## The fix is one table, applied once

The tempting fix is per-client: teach the dsh plugin to send `Bash`, teach the opencode plugin to send `Bash`, and so on. That fixes two harnesses and guarantees the next one reopens the gap. It also makes the audit trail lie — the harness didn't call a tool named `Bash`.

The right fix was one conservative alias table at the entry to classification, where every consumer — hardline floor, classifiers, policy tiers, both governance endpoints — already reads:

```ts
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
};
```

Exact matches only, deliberately. A fuzzy rule that "helpfully" canonicalizes a tenant's unrelated custom tool named `bash_helper` would apply shell-grade policy to something that isn't a shell. Unknown names keep today's generic treatment, and policy keys written against native spellings keep working because the lookup falls back to the original name.

One change, every harness covered — including the ones that don't exist yet, the day someone adds one line to a table instead of re-auditing five layers. The repro that opened this post now comes back `deny` with the hardline reason attached, and it did before the harness plugin ever shipped to a user.

## If you run any name-keyed control layer

This isn't about our gateway. The same shape lives in every allowlist, SIEM rule, seccomp-style tool filter, and prompt-injection scanner that matches on tool names: the rules encode the dialect of the first agent you governed. Three questions to ask of yours:

1. **What happens to a name your rules don't match?** If the answer is "generic handling" rather than "loud unknown," you have silent partial coverage by construction.
2. **Have you tested with each harness's real spellings?** Not the docs — the actual wire traffic. Our gap survived until we sent dsh's genuine payloads at production.
3. **Where does the dialect get normalized?** If the answer is "in each client," every new client is a chance to forget. Normalize once, at the choke point, and record both the native name and the canonical one.

The general principle: a control layer keyed on names is only as covered as its name table is current, and name tables rot silently. Test with real dialects, normalize at the choke point, and treat "we've never seen that name" as a signal, not a shrug.

*The full coverage picture per harness — what intercepts, what can ask, what can rewrite — lives on the [harness coverage matrix](/coverage).*
