cd /news/ai-agents/four-loopholes-in-denylist-based-age… · home › topics › ai-agents › article
[ARTICLE · art-160166] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Four Loopholes in Denylist-Based Agent Command Approval: Why Blacklisting Shell Commands Fails at Scale

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.

by read4 min views2 publishedOct 29, 2026

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.

The 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.

On 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.

This is the security boundary breakdown.

Desktop agent apps intercept shell commands before execution using one of three methods:

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. The 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.

A denylist policy maintains a list of dangerous patterns and auto-approves everything else:

rm, curl, wget, eval). The problem: agents generate novel command patterns faster than humans can write denylist rules.

Loophole Example Why Denylist Fails
Command chaining ls && rm -rf /tmp/data Denylist checks ls (safe), misses the chainedrm
Argument injection `git log --pretty=format:"%H" \ xargs rm`
Environment variable smuggling CMD=rm && $CMD -rf /tmp/data Denylist sees CMD=rm (assignment, not execution), misses$CMD expansion
Encoding tricks `echo cm0gLXJmIC90bXAvZGF0YQ== \ base64 -d \

Each 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.

An allowlist policy inverts the logic: define the set of permitted command shapes and reject everything else.

Decision flow:

git status, npm install, ls <directory>). The allowlist implementation tested on October 12, 2026, used this structure:

ALLOWED_COMMANDS = {
    "git": ["status", "log", "diff", "branch"],
    "npm": ["install", "test", "run"],
    "ls": ["*"],  # Any arguments allowed
    "cat": ["*"],
    "echo": ["*"],
}

def is_allowed(command: str) -> bool:
    tokens = shlex.split(command)
    if not tokens:
        return False

    base_cmd = tokens[0]
    if base_cmd not in ALLOWED_COMMANDS:
        return False

    allowed_subcommands = ALLOWED_COMMANDS[base_cmd]
    if allowed_subcommands == ["*"]:
        return True

    if len(tokens) < 2:
        return False

    subcommand = tokens[1]
    return subcommand in allowed_subcommands

This blocks all four loophole categories:

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.

The friction shows up in two places:

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. The 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.

Allowlists require active curation. Every new command shape must be added explicitly. This creates operational overhead but also creates an audit trail.

Maintenance strategies:

.agent-config.json in the repository. The team reviews and updates it during code review. The 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.

Allowlists fail in predictable ways:

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. The 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, .).

Aspect Denylist Allowlist
Default behavior Auto-approve unless blocked Block unless explicitly allowed
Maintenance burden Add rules reactively after incidents Add rules proactively before use
Agent friction Low (most commands pass) Medium (new commands require approval)
Security posture Fails open (unknown = allowed) Fails closed (unknown = blocked)
Loophole resistance Poor (14 of 18 hostile examples passed) Strong (0 of 18 hostile examples passed)
Audit trail Sparse (only blocked commands logged) Complete (all allowed shapes documented)

Use an allowlist when:

npm, cat). Use a denylist when:

Do not use a denylist when:

The 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.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/four-loopholes-in-de…] indexed:0 read:4min 2026-10-29 · —