Measured across 1,709 real-world denylists, most don't fully block what they were written to block.
Command filtering fails two separate ways, and they need two separate explanations. Bash rewrites text after a matcher reads it: r''m, rm$IFS-rf$IFS/, and $(echo rm) -rf / are three strings a regex clears and a shell runs as rm. That's the expansion problem. Separately, some binaries the matcher already trusts do the damage under their own real name—GTFOBins catalogs the ones with documented, legitimate functions that escape a restricted context. Neither failure needs the other to work. If a matcher is all that stands between a coding agent and a production host, that's a guess, not a control—for both reasons. Here's each mechanism, and where the control belongs—the adversarial case for how this series scopes access before a command is even typed.
Why do command denylists fail even when the rules look right?
TL;DR: Across 1,709 real-world denylists, 69.0–98.6% failed to fully block what they were written to block, depending on the operation. The cause is the shell and the OS, not sloppy rules.
A June 2026 Ohio State paper, "One Goal, Many Commands", evaluated 1,709 real-world denylists—13,332 rules from public repos. Between 69.0% and 98.6% incompletely blocked the operation they existed to block depending on which operation, and on their reference host, closing file reads alone would take an average of 217 more commands. Their conclusion: denylists are load-bearing and fragile, and need to be combined with other techniques.
That isn't a rule-quality problem. GTFOBins catalogs hundreds of ordinary Unix executables whose documented functions break out of restricted contexts—find -exec, :!sh from inside an editor. And a matcher inspects raw text while bash expands, unquotes, and rewrites that text before executing it. Adversa AI's GuardFall research (June 2026) found the agent-to-shell boundary exploitable in 10 of the 11 open-source coding agents they surveyed; their framing, quoted directly: "lexing is not evaluation." Quote removal, $IFS substitution, and base64 piped to sh all defeat a matcher that never sees what the kernel gets.
Do allowlists fix it?
TL;DR: No. They fail from the other end—allowing make, docker, or go test allows arbitrary execution once the agent can also edit files.
Vendor research from Formal, written against Claude Code, makes the same point: allowlisting some bash commands is often the same as allowlisting all. Permit go build and you permit -toolexec; permit find and you permit -exec; permit make and the agent's own file edits become the payload.
Two 2026 advisories show the pattern in shipping products. Cursor's terminal allowlist could be bypassed—certain shell built-ins ran under Auto-Run without appearing in the allowlist (fixed in Cursor 2.3). Pillar Security's July 2026 series also found Codex CLI's allowlist trusting git show by name when the invocation wasn't read-only (fixed in v0.95.0). Those instances are patched; the class isn't. An allowlist trusts a command's name, and the shell runs its arguments.
So is pattern matching useless?
TL;DR: No—it's the wrong layer to finish on. Deterministic rules and a baseline first, model judgment on the ambiguous slice, escalation to a human where that's still not enough, and the verdict cryptographically bound to the command that actually runs.
Alpacon's pipeline checks scope first: an action outside the session's declared scope is refused before it reaches any rule or model. Inside scope, deterministic rules and a behavioral baseline settle most commands; a still-ambiguous slice goes to a model, which can escalate to a human, and anything scored clearly dangerous is denied outright without waiting on either.
Scope first, deterministic rules next, model on ambiguity: deny-fast, so an out-of-scope action never reaches the expensive step. The Ohio State paper's own conclusion is that a static denylist is inadequate as the load-bearing layer, and it points at pairing one with capability-based sandboxing and an LLM auditor—a different control than ours, since sandboxing bounds the environment and we bound the action; what we take from it is the diagnosis, not an endorsement of our specific layering.
The one agent in the GuardFall survey that closed most of the surface did it differently: deterministic expansion resolution before execution. None of our layers resolve the expansion problem that way either—reading the string, however deterministically or however hard a model tries, still isn't reading what the shell will run. That gap matters for three of the five strings in the opening diagram—the expansion cases named above.
The fix isn't a cleverer parser. It's not needing one: what holds against an obfuscated command has to be true whether or not anyone decoded it correctly, and that's what the rest of this piece is actually about—starting with scope, which already ran before any matcher did. A session's declared scope is a ceiling on what the session can attempt at all; a command that falls outside it is refused for that reason alone, decoded or not. That ceiling doesn't help the harder case—an obfuscated destructive command inside a scope the session legitimately holds—which is where judgment quality, not string-reading, is the honest boundary.
Judgment happens centrally, at the platform, and the verdict is signed: Alpacon's analysis service returns an Ed25519 signature over the command line, its target host, and the time it was analyzed. In enforce mode the Alpacon agent on the host verifies that signature before it will execute, and a signed command is valid only for the host it was signed for.
Verification runs as a mode, the way most gates of this kind do: record that a command arrived unsigned, or refuse to run it. Where it refuses, the command that runs is provably the command that was authorized—not a lookalike that survived a filter, and not replayable to a second box. The signature solves impersonation and replay, not the expansion problem.
Alpacon doesn't intercept at execve. A shell escaped out of vi, or a subprocess spawned by a script that was itself signed, never comes back as a command to judge. That is kernel-level interception, which Alpacon doesn't do.
Scope narrows the gap. A session that never had vi in scope can't open it, so there is no editor to escape from, and the GTFOBins problem shrinks to the binaries a session legitimately needs. What's left is an escape from one of those, and there the input is still a command string. Alpacon can catch base64 | sh-style obfuscation, or an action that doesn't match the purpose the session declared. When it reads one of those wrong, a dangerous command gets approved—a different failure from the command being altered after approval, and only the second one is ruled out by the signature.
What holds when the classifier is wrong?
TL;DR: The layers whose guarantee doesn't rest on having read the string correctly—a declared scope the action has to fit inside, an approval that doesn't come from the channel that asked for it, and attribution that survives obfuscation.
Assume a matcher gets it wrong, because one eventually will. The layers worth having are the ones that don't get worse when it does.
→ A scope ceiling above RBAC. A session declares what it is for and inherits a ceiling above role permissions, checked before any rule or model runs. A session without sudo in scope is refused when it reaches for it, and that refusal holds whether or not anyone decoded the command correctly—the check is on what the session may attempt, not on what the string appears to say. The ceiling binds the session; a credential used outside one is covered by attribution instead.
→ Out-of-band approval, off the box the agent is typing on. A privileged session, and an escalation on a specific command inside it, route to a human over a channel separate from the one that requested it. The requesting channel is not itself an eligible approver. Obfuscation buys nothing here either: the approver is looking at a request, not parsing a string.
→ Attribution that survives the trick. Commands and file transfers are attributed to the token that ran them. A cleverly encoded command is still a command some specific credential issued, and that binding holds whether or not anyone understood the payload.
None of this is free, and none of it is automatic. Every gate named here—the scope refusal, the deny on a clearly dangerous command, the out-of-band approval, the signature check—ships as a mode: record the verdict, or act on it. Which mode a given deployment runs is a configuration question, not a datasheet line, and a layer running in the recording mode is exactly the record-not-a-control this piece has been arguing against. That is the thing worth asking a vendor, us included: not whether the product can block, but what it does by default in the deployment you would actually run.
FAQ
Should we delete the denylist? No—keep it as the cheap deterministic layer: fast, auditable, no model in the path. Blocking operators and chaining metacharacters on a machine-issued command is worth doing. The mistake is letting it be the last layer.
Doesn't a model in front of commands slow everything down? It isn't in front of everything—rules and a baseline settle most commands. The model handles the ambiguous slice, and escalates to a human only what it can't resolve; anything scored clearly dangerous is denied outright, without waiting on either.
The string an agent typed is untrusted input. Judge it centrally, bind the verdict to what actually runs—that's the execution-control layer inside AI-native PAM, not a better filter.
