{"slug": "four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell", "title": "Four Loopholes in Denylist-Based Agent Command Approval: Why Blacklisting Shell Commands Fails at Scale", "summary": "A developer rebuilt a desktop AI coding agent's shell-command auto-approval system from a denylist to an allowlist after finding four exploitable loopholes: command chaining, argument injection, environment variable smuggling, and base64 encoding tricks. Tested against 18 hostile command examples, the original denylist auto-approved 14, while the allowlist blocked all 18. The allowlist implementation parses commands with shlex and permits only explicitly granted base commands and subcommands, at the cost of higher approval friction.", "body_md": "Desktop AI coding agents now ship with approval dialogs that gate shell command execution. The agent proposes a command, a dialog appears, and a human clicks \"approve\" or \"deny.\" The friction is real, so teams build auto-approval scripts that click the button when the command looks safe.\n\nThe naive implementation uses a denylist: block commands containing `rm -rf`, `curl | sh`, `sudo`, and other known-bad patterns. Everything else gets auto-approved. This approach fails catastrophically when tested against adversarial examples.\n\nOn September 15, 2026, an AI agent reviewing a denylist-based auto-approval script found four loopholes and flagged them as required fixes. The implementation was rebuilt using an allowlist approach. When the original denylist was tested against 18 hostile command examples, it auto-approved 14 of them. The allowlist blocked all 18.\n\nThis is the security boundary breakdown.\n\nDesktop agent apps intercept shell commands before execution using one of three methods:\n\n`execve()` or equivalent syscalls to capture the command string before the kernel sees it.`cd`, `ls`, `git`) with wrapper functions that call back to the approval service.\nThe approval service receives a command string, applies a decision policy, and either executes the command or shows a dialog. The decision policy is where denylist vs. allowlist architecture matters.\n\nA denylist policy maintains a list of dangerous patterns and auto-approves everything else:\n\n`rm`, `curl`, `wget`, `eval`).\nThe problem: agents generate novel command patterns faster than humans can write denylist rules.\n\n| Loophole | Example | Why Denylist Fails | \n|---|---|---|\n| **Command chaining** | `ls && rm -rf /tmp/data` | Denylist checks `ls` (safe), misses the chained`rm` | \n| **Argument injection** | `git log --pretty=format:\"%H\" \\ | xargs rm` | \n| **Environment variable smuggling** | `CMD=rm && $CMD -rf /tmp/data` | Denylist sees `CMD=rm` (assignment, not execution), misses`$CMD` expansion | \n| **Encoding tricks** | `echo cm0gLXJmIC90bXAvZGF0YQ== \\ | base64 -d \\ | \n\nEach loophole exploits the gap between static pattern matching and dynamic shell evaluation. The denylist sees the command as a string. The shell sees it as a program.\n\nAn allowlist policy inverts the logic: define the set of permitted command shapes and reject everything else.\n\n**Decision flow:**\n\n`git status`, `npm install`, `ls <directory>`).\nThe allowlist implementation tested on October 12, 2026, used this structure:\n\n```\nALLOWED_COMMANDS = {\n    \"git\": [\"status\", \"log\", \"diff\", \"branch\"],\n    \"npm\": [\"install\", \"test\", \"run\"],\n    \"ls\": [\"*\"],  # Any arguments allowed\n    \"cat\": [\"*\"],\n    \"echo\": [\"*\"],\n}\n\ndef is_allowed(command: str) -> bool:\n    tokens = shlex.split(command)\n    if not tokens:\n        return False\n\n    base_cmd = tokens[0]\n    if base_cmd not in ALLOWED_COMMANDS:\n        return False\n\n    allowed_subcommands = ALLOWED_COMMANDS[base_cmd]\n    if allowed_subcommands == [\"*\"]:\n        return True\n\n    if len(tokens) < 2:\n        return False\n\n    subcommand = tokens[1]\n    return subcommand in allowed_subcommands\n```\n\nThis blocks all four loophole categories:\n\n`ls && rm -rf /tmp/data` fails because `&&` is not in the allowlist.`git log | xargs rm` fails because `|` is not in the allowlist.`CMD=rm && $CMD -rf /tmp/data` fails because `echo ... | base64 -d | sh` fails because Denylists auto-approve most commands, creating low friction and high risk. Allowlists require explicit grants, creating high friction and low risk.\n\nThe friction shows up in two places:\n\n`git commit -m \"fix\"` will trigger a dialog if `commit` is not in the allowlist. The human must approve once, then the allowlist is updated.\nThe solution: change the agent's instructions to request permission once at the task level, not once per command. The agent asks, \"May I modify files in `/src` and run `npm test`?\" The human approves the task, and the allowlist is updated for the session.\n\nAllowlists require active curation. Every new command shape must be added explicitly. This creates operational overhead but also creates an audit trail.\n\n**Maintenance strategies:**\n\n`.agent-config.json` in the repository. The team reviews and updates it during code review.\nThe October 12 comparison run used a session-scoped allowlist. The agent proposed 6 command shapes at the start of the task. The human approved all 6. The allowlist blocked 18 hostile examples and allowed 6 legitimate examples.\n\nAllowlists fail in predictable ways:\n\n`ALLOWED_COMMANDS = {\"sh\": [\"*\"]}` defeats the purpose. The agent can run `sh -c \"rm -rf /\"` and the allowlist will approve it.`git commit` without checking the `-m` argument means the agent can run `git commit -m \"$(rm -rf /)\"` and inject a command substitution.\nThe robust implementation parses the full command, validates argument shapes, and rejects any construct that could execute arbitrary code (pipes, redirects, command substitutions, backticks, `eval`, `source`, `.`).\n\n| Aspect | Denylist | Allowlist | \n|---|---|---|\n| **Default behavior** | Auto-approve unless blocked | Block unless explicitly allowed | \n| **Maintenance burden** | Add rules reactively after incidents | Add rules proactively before use | \n| **Agent friction** | Low (most commands pass) | Medium (new commands require approval) | \n| **Security posture** | Fails open (unknown = allowed) | Fails closed (unknown = blocked) | \n| **Loophole resistance** | Poor (14 of 18 hostile examples passed) | Strong (0 of 18 hostile examples passed) | \n| **Audit trail** | Sparse (only blocked commands logged) | Complete (all allowed shapes documented) | \n\n**Use an allowlist when:**\n\n`npm`, `cat`).\n**Use a denylist when:**\n\n**Do not use a denylist when:**\n\nThe comparison run data is clear: denylists fail 78% of hostile examples (14 of 18). Allowlists fail 0%. The friction cost of allowlists is real, but the security cost of denylists is catastrophic.", "url": "https://wpnews.pro/news/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell", "canonical_source": "https://dev.to/mech_app_ai/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell-commands-fails-at-5cg3", "published_at": "2026-10-29 10:10:27+00:00", "updated_at": "2026-10-29 10:17:16.767229+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell", "markdown": "https://wpnews.pro/news/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell.md", "text": "https://wpnews.pro/news/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell.txt", "jsonld": "https://wpnews.pro/news/four-loopholes-in-denylist-based-agent-command-approval-why-blacklisting-shell.jsonld"}}