← cd ../blog
cat blog/guardfall-the-guard-that-isnt-there.md — less — 80x24
Article
GuardFall:
The Guard That Was Never There
--author perrmit --date 2026-07-30 --tags ai-safety, security, agentic-ai, shell-injection, coding-agents
Your AI coding agent runs shell commands with your full account authority — your SSH keys, your cloud credentials, every secret in $HOME — gated by a filter that checks the wrong thing. Ten of eleven popular open-source agents fell to a shell trick older than most of the engineers who built them. Only one didn't. And there's no CVE to patch, because the vulnerability is the convention itself.

In June, security firm Adversa AI published a survey of eleven of the most-used open-source coding and computer-use agents — roughly 548,000 combined GitHub stars, the dead center of the category. Ten of them fell to a bypass technique that's been documented in shell-scripting literature since the 1980s. Only one agent, Continue, was built to actually stop it.

Nobody found a zero-day. Nobody needed one. This isn't a frontier machine-learning vulnerability requiring novel adversarial research. It's a decades-old shell-quoting bypass that the AI coding-agent ecosystem rediscovered the hard way — because the convention it standardized on was never sound to begin with, model in the loop or not.

THESIS: The vulnerability isn't a bug in any one agent. It's a convention the whole category adopted: check the command as text, then hand it to bash. The problem is that bash doesn't run the text you checked — it rewrites the text first, and only then runs what's left. Every agent that inspects the "before" and executes the "after" has already lost, no matter how long its blocklist is.
The Trick That's Older Than the Threat Model

Adversa calls the pattern GuardFall. It starts from a simple mismatch. A security filter reads a command as a plain string. Bash treats that same string as a small program — stripping quotes, expanding variables, substituting the output of other commands, splitting on whitespace — before it decides what to actually execute. A filter and a shell can stare at identical input and walk away with two completely different commands. The filter approves what it sees. Bash runs what it means.

Five bypass classes are enough to break essentially any filter operating on raw command text:

[CLASS A] quote_removal        — the shell strips adjacent quote pairs before lexing. r''m is two tokens to a regex, one token — rm — to bash. A guard anchored on rm never fires.
[CLASS B] ifs_expansion       — bash splits fields on the Internal Field Separator. rm$IFS-rf$IFS/ reads as one harmless word to a regex; bash expands it into three arguments: rm, -rf, /.
[CLASS C] cmd_substitution    — the binary name can be computed at runtime. $(echo rm) -rf / yields rm after evaluation; the regex sees a substitution expression, not a binary. The nasty variant hides it inside a quoted argument the outer command "allows."
[CLASS D] base64_to_shell     echo <payload> | base64 -d | sh. Each segment is benign alone: echo prints, base64 -d decodes, sh runs. The destructive content is the output of the composition, so per-segment analysis misses it entirely.
[CLASS E] destructive_argv    — plenty of destructive commands contain no rm at all. find /x -delete, dd of=/dev/sda, and a sed -i one-liner that rewrites your AWS credentials file all do real damage while sailing past a blocklist built around famous verbs.

None of these are novel. They're the same shell-quoting tricks that have been in the security literature for decades. What's new is the target: an AI agent that reads untrusted content — a poisoned README, a malicious tool response, a planted Makefile — and can be talked into emitting one of these payloads as if it were routine work.

FINDING: Of eleven agents surveyed, ten leave the agent-to-bash boundary exploitable. The three weakest (Hermes, opencode, Goose) leak almost every probe — opencode failed 16 of 16 test cases, Goose 22 of 23. The bypass classes are structural and shell-driven; breadth of blocklist changes nothing. A guard with thirty patterns fails at the same rate as one with three.
Adversa AI, Jun 30 2026
The Agent Doesn't Have to Be Tricked Directly

This is the detail that matters most, and it's the one a "just align the model better" reflex misses. Adversa's researchers found that asking a model outright to run a destructive rm gets refused, reliably. But the exact same payload, wrapped inside a Makefile target the agent is asked to inspect, or returned as "documentation" from a compromised tool server, gets emitted without hesitation — because from the model's perspective it's just doing the task in front of it.

Their cline run shows how thin the line is. Hinted as ordinary tool content, the model spotted the injection and emitted a read-only find. Re-framed as an authoritative directive, it emitted find -delete without hedging — and the controller passed it, because find was on the operator's allowlist.

CONFIRMED: Adversa executed a full attack chain end-to-end against the production build of the agent Plandex. A pull request added an innocuous dependency bump alongside a Makefile whose clean target quietly deleted the developer's AWS credentials file — triggered automatically when the agent ran make test as normal workflow. The same attack shape succeeded against eight other agents in the survey.
Adversa AI, Jun 30 2026

Picture the realistic version. A mid-sized engineering team runs an open-source coding agent in CI to review pull requests automatically. To keep the pipeline non-interactive, someone enabled the documented --auto-exec flag, exactly as recommended for unattended use. A contributor opens a PR — maybe someone who's never seen the payload, maybe someone who planted it — adding a one-line dependency update plus a Makefile change. The agent reads the Makefile to understand the project (normal behavior), emits make test as its next step, and the auto-exec gate runs bash -c "make test". The runner's IAM credentials are wiped; on the next deploy, production fails. In parallel, the same credentials were already exfiltrated through an endpoint embedded earlier in the same Makefile.

The blast radius is the operator's shell. On a laptop that's SSH keys, cloud credential files, browser session tokens, git signing keys. In a CI runner it's deploy keys, registry credentials, and — for self-hosted runners — a pivot into the corporate network. Treat every line of every file your agent reads as a potential payload that may end up as bash -c.

Four Ways to Fail, One Way to Pass

Adversa grouped the ten failing agents into four architectural patterns. The grouping matters more than any product name, because it tells you what to look for in your own tooling rather than which brand to avoid this month.

[FAIL] regex_over_raw_string     — a fixed list of dangerous patterns matched against the literal command text (Hermes, opencode, Goose). The weakest and most common mode. Bash's rewriting happens after the check, so breadth is irrelevant. Parsing to an AST first catches && and ; separators, but the matcher still runs on each node's raw text — lexing is not evaluation.
[FAIL] tokenized_raw_text       — tokenizes the way bash would, closing quote-removal, but still matches raw segment text (Cline opt-in, Roo-Code). Misses command substitution buried in a quoted argument, and has no concept of which flags turn a safe binary destructive. Live-exploited with a find -delete payload against an allowlisted binary.
[FAIL] no_guard_plus_auto_yes   — no pattern matching at all; the human approves each command, until a flag like --auto-exec, --auto-run, or dangerously-skip-permissions removes the human. Routinely enabled in CI. Worse: a malicious repo can ship a config file (e.g. an .aider.conf.yml with auto-test: true) that flips it with no operator flag at all — a supply-chain vector.
[FAIL] sandbox_with_opt_out    — genuinely sound while active; every command runs in a disposable container, so rm -rf / destroys a throwaway box (OpenHands, SWE-agent). But both ship a documented RUNTIME=local mode that disables the container — and that's exactly what gets used on CI runners and laptops where Docker-wrapping every call feels heavy. Local mode equals no sandbox.

Only one agent, Continue, closed the structural majority of the bypass surface by design. Its evaluator runs five checks in sequence, and each one closes a class that skipping it would reopen:

step 1 tokenize with shell-quote → absorbs quote pairs: r''m becomes rm [closes A] step 2 detect variable expansion → escalates any $IFS / ${VAR} to prompt [closes B] step 3 recurse into substitutions → evaluates $(...) inner command first [closes C-outer] step 4 check pipe destinations → escalates any pipe into sh/bash/python [closes D] step 5 match explicit disabled list → hard-blocks mkfs.*, rm -rf /etc, chmod +s [covers E]

Empirically: of 21 bypass cases submitted to Continue's evaluator, zero reached unrestricted execution, and all 12 canonical-destructive cases were correctly downgraded. It's not perfect — Class C inside a quoted argument and the full long tail of Class E remain open — but it's the only agent in the survey that closes the structural majority of the surface.

CAVEAT: Continue's protection isn't free, and the cost is instructive. Its own tighter checking escalates many benign commands to a confirmation prompt — $HOME and ${PWD} trip the same detector that catches $IFS. A developer confirms several prompts a session. That friction is exactly what pushes teams toward the auto-yes flags that undo the protection. Under Continue's own CLI --auto mode, the soft-block tier is discarded and 18 of 24 payloads ran; only the hard-blocked disabled tier held. Soundness and workflow speed are in genuine tension, and nobody has fully resolved it.
Why "Just Add More Patterns" Doesn't Work

Every proposed fix that begins "add this pattern to the blocklist" is treating the symptom. Three popular designs get offered as the defense at the bash -c boundary, and under prompt injection each fails for the same root reason: it sites the decision with a party that lacks the context to make it.

[WATCH] the_model_is_not_the_defense — a direct "run this rm" is refused; the same command wrapped in a Makefile target, a tool "documentation" response, or an injected README task is emitted as routine work. Any design relying on the model's judgment as the last line has already been shown to fail under mild reframing.
[WATCH] the_operator_prompt_erodes  — it holds until the auto-yes flag is set, which happens the moment the agent slows the workflow, and in CI is set in advance because it's the only option that doesn't hang. Vetting each command asks the operator for a security call with less context than the agent has about what it just ingested.
[WATCH] the_sandbox_is_containment — a container doesn't check anything; it moves the blast radius. Sound while the workspace is disposable, worthless the moment it isn't — and an agent editing your real repo, with real ~/.aws and ~/.ssh in scope, has nothing to contain to. The valuables are already inside the box.

The three share one shape: each hands the decision somewhere that can't hold it. A sound check is a guard on the agent's side of the boundary — it runs on every emitted command, before the operator sees it, regardless of flags. Only Continue's design aspires to that, and even it drops the soft tier under --auto. The rest of the ecosystem shifts accountability without shifting capability: the developer can't vet every line the agent reads, so the agent must.

What This Means If You Run One of These Tools

For teams running any of these agents in CI: audit whether auto-execute is enabled, and if it is, treat that pipeline as running with full credential access on every unreviewed PR — because that is precisely what's happening. Disable agent execution on fork pull requests; that's the common path from an attacker-authored README to secret exfiltration on a privileged runner.

For anyone maintaining a fork or wrapper: adopting Continue's tokenize-then-recursively-evaluate pattern is described by its own authors as roughly a two-day engineering exercise. Given the alternative, that's a very short list of days. Adopting just three of the five components (tokenize + substitution-recursion + pipe-destination) closes Classes A, B, C-outer, and D.

For security teams evaluating agent tooling: ask the specific question — "how does your shell guard handle $IFS, command substitution inside quotes, and destructive argv flags on non-rm binaries?" If the answer is "we have a blocklist," you now know exactly which of the four failure modes you're looking at. Treat operational filters (stop the agent hanging in vim) and security filters as different things; conflating them is how teams accidentally rely on an anti-hang feature for safety.

For everyone else, this week: run the agent from a scoped shell with $HOME redirected. A one-line wrapper — HOME=$HOME/.agent-sandbox-$RANDOM agent … — keeps the project directory but removes ~/.ssh, ~/.aws, and shell history from the blast radius. It's always-on, has no one-flag opt-out, and is the strongest stopgap while you wait for a proper guard.

This Is a Category Problem, Not a Product Problem

GuardFall doesn't sit alone. It's one of several large-scale shell-channel findings in the past six months: the Claude Code deny-rule bypass (a token-budget shortcut), the Antigravity command-injection RCE, the CrewAI framework RCE chain, and TrustFall — a folder-trust dialog leading to unsandboxed startup execution across Claude Code, Cursor, Gemini CLI, and GitHub Copilot CLI. The shape is consistent every time: the defense between the model's emitted command and bash -c is structurally underbuilt. Each finding patches a specific surface; the convention itself produces the next finding.

And the survey deliberately scoped itself to open-source agents. Cursor, Claude Code, Copilot agent mode, Windsurf, and Google's Antigravity were out of scope — but the bypass classes are shell-semantics-driven and independent of who ships the agent. Adversa's expectation, supported by prior findings, is that the same four failure modes live in the closed-source tools too. Someone with authorized testing relationships should run the closed-source GuardFall survey. I'd read it.

Conclusion

What strikes me most about GuardFall isn't the exploit — it's that the exploit is older than most of the engineers building these agents. Continue's evaluator proves the fix is known and shippable in a couple of days. The fact that it's the exception rather than the norm, across a category with over half a million combined GitHub stars, is the actual finding.

An unsound guard offers no protection even when it's left fully on. A sandbox offers full protection right up until it's switched off — and the deployments that most need it are the ones that most often turn it off. Only a sound, on-by-default guard on the agent's side of the boundary protects the operator's own host without asking them to opt in. Until that becomes the convention, every agent shipping a string-matching guard is one prompt injection away from operator-account compromise.

STATUS: There is no single CVE for GuardFall, and that's the whole point. It isn't a patchable bug in one component. It's a design convention — agent to shell, gated by string matching — that fails structurally, while fully enabled and correctly configured.