Skip to content
← Back to blog
AI Agent Security

A command blocklist guards the text, but the shell rewrites the text

July 7, 2026·8 min read

Most agent guardrails ship a command blocklist. You give the agent a shell tool, you list the strings you never want to see, and you match every proposed command against that list. It feels solid. It is not, and the reason is mechanical rather than clever.

A blocklist matches strings. It sees the text rm -rf and refuses it. But the shell does not run the text you filtered. It runs the text after it has expanded quotes, variables, and substitutions. An attacker only has to write a command that means rm -rf to the shell while looking like something else to your filter.

The rewrite tricks

None of these are exotic. They are ordinary shell behavior, and the shell resolves all of them at parse time, before the program ever starts:

r''m -rf puts an empty quoted string in the middle of the word. The shell strips it, so the process that runs is plain rm, but your filter saw r''m and did not match.

rm$IFS-rf uses $IFS, the internal field separator, which expands to whitespace. The shell reads it as rm -rf. Your filter read a single unbroken token.

$(echo rm) -rf is command substitution: the shell runs echo rm first and pastes the result into the command line. ${x:-rm} is parameter-default expansion, which yields rm whenever x is unset. Backslash escapes like r\m do the same job. Every one of these arrives at the operating system as the exact command your blocklist was built to stop.

This is not a corner case. Any guardrail that matches the proposed command as a string, without modeling how the shell will rewrite it, is exposed to this whole family of tricks. The only filters that reliably sidestep it are the ones that refuse shell metacharacters wholesale, which is a blunt tradeoff rather than a filter that understands the shell.

Layer one: normalize before you match

The screening fix is to stop matching the raw string. Before InnerWarden compares a proposed command against its patterns, a pure, non-executing normalize_command step de-obfuscates the rewrite: it removes empty quotes, turns $IFS back into a space, unwraps $(...), resolves ${x:-default}, and collapses backslash escapes. Critically, it does this by parsing, not by running anything. r''m -rf normalizes to rm -rf, and only then does pattern matching run.

Because that happens inside the PreToolUse hook, the rewritten command is denied before it executes at all. Same idea as any other InnerWarden guardrail: catch the mistake cheaply and early. This is the layer that does the everyday work, and it is the same argument we make in runtime guardrails, not prompt guardrails. A filter that does not model the shell is guarding a language it does not speak.

Layer two: see the real command at the syscall

Normalization is good, but no de-obfuscator is ever provably complete. The shell grammar is large, and a determined attacker can keep inventing forms. So the honest position is: assume text matching can eventually be bypassed, and make sure something below the text still sees the truth.

That something is the sensor. At execve, the kernel already holds the fully expanded argument vector. Whatever obfuscation the attacker used is gone by then, because the shell has already resolved it. The process really is rm with the real flags. That is the ground truth, and it does not care what your string filter thought.

There was a real bug in reading it. For short-lived processes like rm and find, our userspace collector used to rebuild the argument vector from /proc/PID/cmdline. But a fast command is already gone by the time userspace opens that file, so the read came back nearly empty. Across a test run, most of those short-lived execs showed argc=1: we could see that something ran, but we were argv-blind to what.

GuardFall, shipped in v0.16.0, fixes this by reading the arguments in-kernel, inside the eBPF program at execve entry, before the process can exit. On the same workload argc went from 1 to 3, and the real post-rewrite command became visible. A filter-aware data-destruction detector then flags it: a find_delete_bulk sub-kind that only fires on unfiltered destructive commands, so a normal cleanup does not trip it but a bulk delete that slipped past screening does.

Two layers because one is not enough

This is defense in depth stated plainly. Layer one normalizes the text and denies the rewrite before it runs, which handles the common case at near-zero cost. Layer two watches the syscall and catches the real command even when the text games win, because at execve there is no text left to game. On Linux both layers are live, sitting alongside the same kernel Execution Gate we describe in denied is denied. Screening is cross-platform; the in-kernel argv capture and gate are Linux-only.

We validated this on real hosts, not a slide. Rewritten commands that a text filter waved through produced rm_rf_user_data and find_delete_bulk incidents on the true command, argc climbed from 1 to 3, and no new false positives showed up. The recordings live on the proof page. The lesson is short: filters guard the text, the shell rewrites the text, so you normalize before you match and you watch the real command at the syscall.

Related reading

Keep following the attack path

Explore AI Agent Security
AI Security

The OWASP Agentic Top 10: What "Covered" Honestly Means

Most tools claim OWASP Agentic Top 10 coverage from the prompt layer. InnerWarden maps to all ten at the action layer with a reason chain, redaction, and a circuit breaker, and is honest about what is proven versus designed.

7 min readRead
AI Security

From Prompt Injection to Syscall: Why Prompt-Layer Defenses Guard the Wrong Layer

A poisoned input rewrites the agent's intent, and that intent lands on the host as execve, openat, or connect. Prompt-layer defenses live inside the context that just got poisoned. The damage becomes real at the syscall.

7 min readRead
AI Security

Denied Is Denied: Why the Kernel's No Beats the Model's No

A model's refusal is a prediction an attacker can steer. The operating system's refusal is not a decision at all. Where enforcement has to live when agents touch real machines.

6 min readRead
AI Security

Claude Code in Auto Mode: Brilliant, Fast, and Running as You

Auto mode is the right way to use a coding agent, and it executes commands with your permissions and no checkpoint. How to keep the speed and put a floor under it.

6 min readRead
AI Security

The MCP Attack Surface Nobody Sandboxed

Tool poisoning, injected tool output, and command injection through tools: how each MCP attack works, why prompt defenses guard the wrong layer, and how to inspect the path itself.

7 min readRead
AI Security

Runtime Guardrails, Not Prompt Guardrails

Agentic Runtime Security is consolidating in 2026 around prompt-side defences. The load-bearing layer is the one downstream: what the agent does, not what it says.

9 min readRead