Skip to content

[Bug]: zeroclaw agent -m ignores autonomy level=full — shell commands blocked in non-interactive (Docker/CI) mode #851

Description

@JohannStraussII

Summary

zeroclaw agent -mblocks shell tool execution when running non-interactively (Docker container, bash loop, CI). Even with[autonomy] level = "full", the SecurityPolicy's is_command_allowed() rejects commands that contain semicolons (;`) in quoted SQL arguments, and the approval prompt reads EOF from stdin and denies the command.

Affected component

security/sandbox

Severity

S1 - workflow blocked

Current behavior

Running zeroclaw agent -m "search leads" inside a Docker container with level = "full":

  • Shell tool calls show [Y]es / [N]o / [A]lways for shell: prompt on stderr
  • stdin reads EOF → treated as denial → command never executes
  • LLM hallucinates <tool_result> with fake data, DB file remains 0 bytes
  • is_command_allowed() also blocks SQL commands containing ; because it splits on ;
    without respecting quoted strings (e.g. sqlite3 db.db "CREATE TABLE t(id INT); INSERT INTO t VALUES(1);")

Expected behavior

With level = "full", shell commands execute immediately without interactive approval.
Semicolons inside quoted arguments should not be treated as command separators.

Steps to reproduce

# config.toml
[autonomy]
level = "full"
allowed_commands = ["sqlite3", "python3", "echo"]
forbidden_paths = []

# Non-interactive — no TTY (Docker container, CI, bash loop)
docker exec mycontainer bash -c \
  'ZEROCLAW_WORKSPACE=/root/.zeroclaw/workspace zeroclaw agent -m \
   "run: sqlite3 /tmp/test.db \"CREATE TABLE t(id INT); INSERT INTO t VALUES(1); SELECT * FROM t;\""'

# stderr shows:
# 🔧 Agent wants to execute: shell
#    command: sqlite3 /tmp/test.db "CREATE TABLE t(id INT); INSERT INTO t..."
#    [Y]es / [N]o / [A]lways for shell:   ← blocks, reads EOF, denies

# DB file remains empty:
# sqlite3 /tmp/test.db ".tables"  →  (no output, 0 bytes)
Root cause (found in source)
Issue 1 — approval prompt reads stdin even with level = "full":

needs_approval() in src/approval/mod.rs correctly returns false for Full:


if self.autonomy_level == AutonomyLevel::Full {
    return false;
}
But is_command_allowed() in src/security/policy.rs blocks the command before
approval is even checked, because it splits on ; without respecting quoted strings:


for sep in ['\n', ';', '|'] {
    normalized = normalized.replace(sep, "\x00");
}
sqlite3 db.db "INSERT INTO t VALUES(1);" → splits into sqlite3 db.db "INSERT INTO t VALUES(1)
and empty string → second segment fails allowed_commands check → is_command_allowed returns false
→ validate_command_execution errors → shell tool returns failure before approval prompt.

Issue 2 — approval prompt fires anyway due to the LLM retrying:

The LLM retries with a simpler command, eventually hitting needs_approval() which — despite
level = "full" returning false — still shows the prompt because the security error happens
at the tool execution layer, not the approval layer, causing confusing mixed output.

My config.toml

[autonomy]
level = "full"
workspace_only = false
allowed_commands = ["curl", "python3", "sqlite3", "git", "npm", "echo", "ls", "cat", "grep", "find", "pwd", "wc", "head", "tail"]
forbidden_paths = []
max_actions_per_hour = 20
max_cost_per_day_cents = 500
require_approval_for_medium_risk = true
block_high_risk_commands = true
auto_approve = ["file_read", "memory_recall"]
always_ask = []
Suggested fix
In src/security/policy.rs, is_command_allowed() — skip shell operator guards for Full autonomy,
and fix semicolon parsing to respect quoted strings:


pub fn is_command_allowed(&self, command: &str) -> bool {
    if self.autonomy == AutonomyLevel::ReadOnly {
        return false;
    }
    // Full autonomy: trust the allowlist, skip pipe/semicolon/redirect guards
    if self.autonomy == AutonomyLevel::Full {
        return true;
    }
    // ... rest of existing checks
}
And in validate_command_execution(), skip risk gating for Full:


let risk = self.command_risk_level(command);

// Full autonomy: skip medium/high risk gating
if self.autonomy == AutonomyLevel::Full {
    return Ok(risk);
}

Impact

Blocks all autonomous agent operation in non-interactive environments (Docker, CI, bash loops).
level = "full" is effectively broken for the primary use case of headless agents.

Logs / stack traces

Logs

INFO zeroclaw::agent::loop_: Memory initialized backend="sqlite"
WARN zeroclaw::skills: failed to run git clone for open-skills: No such file or directory
🔧 Agent wants to execute: shell
   command: sqlite3 /workspace/leads.db "CREATE TABLE leads(...); INSERT INTO..."
   [Y]es / [N]o / [A]lways for shell:
DB after run: -rw-r--r-- 1 root root 0 Feb 18 20:00 /workspace/leads.db

ZeroClaw version

v0.1.0 (commit b43e9eb)

Rust version

rustc 1.93.0 (ARM64, EC2 Graviton2)

Operating system

Linux (Docker container, Alpine/Ubuntu)

Regression?

No, first-time setup

Pre-flight checks

  • I reproduced this on the latest main branch or latest release.
  • I redacted secrets/tokens from logs.
  • I removed personal identifiers and replaced identity-specific data with neutral placeholders.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions