openclaw - 💡(How to fix) Fix Feature: agents.list[].tools.deny_wrappers — per-agent denial of bash wrapper paths [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72062Fetched 2026-04-27 05:35:27
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

agents.list[].tools.deny (manual line 33635, codebase isToolAllowedByPolicyName) is excellent for filtering MCP tools by tool.name (e.g. github__*, supabase__*). However, bash wrappers invoked through tools.exec allowlist do NOT pass through that filter. They are matched against exec-approvals.json and the per-agent tools.exec.security: allowlist policy, which evaluates the binary (sh, bash) — not the script path argument.

This means an agent with full deny on github__* can still execute sh /data/.openclaw/workspace/scripts/cks-github.sh ... and reach the same external service, bypassing the architectural delegation rule the operator wanted to enforce.

We are requesting a new policy field — agents.list[].tools.deny_wrappers (or equivalent name) — that matches against the resolved script path of an exec invocation, with glob support.


Error Message

  • Error message clearly identifies the matched glob: Tool denied by policy: deny_wrappers (matched **/cks-github.sh).

Root Cause

The result: in our setup, the Maestro can reach cks-github.sh directly even with tools.deny: ["github__*"] applied, because tools.deny doesn't see exec invocations.

Fix Action

Fix / Workaround

Today the only "structural" workaround is moving the wrapper outside the shared workspace and filesystem-isolating per-agent CWD, which is a much larger architectural change (per-agent docker sandbox, per-agent workspace.dir, etc.).

Current workaround (what we ship today)

  • Manual OpenClaw line 29215: "Global tool allow/deny policy (deny wins). Case-insensitive, supports * wildcards. Applied even when Docker sandbox is off." — this is the precedent we want extended to exec paths.
  • Manual OpenClaw line 33635: agents.list[].tools.allow/deny officially supported.
  • Codebase TOOL_NAME_SEPARATOR = "__" (line 437233) — confirms tool.name is the dispatch key, not exec args.
  • Related issue #69570 — approval cascade loops are aggravated by lack of fail-fast wrapper denial.
  • Related issue #69386 — agent ignores stop commands during exec denial.
  • Related PR #57568 (closed without merge) — proposed 3-layer defense including hard guard regex; a deny_wrappers field would have given that PR a cleaner home for layer 3.

Code Example

{
  "agents": {
    "list": [
      {
        "id": "main",
        "tools": {
          "exec": {
            "security": "allowlist",
            "ask": "on-miss",
            "strictInlineEval": true
          },
          "deny": ["github__*", "supabase__*"],          // existing — MCP tool filter
          "deny_wrappers": [                              // NEW
            "**/cks-github.sh",
            "**/cks-supabase.sh",
            "**/cks-vercel.sh",
            "**/cks-sergio.sh"
          ]
        }
      },
      {
        "id": "cks-dev",
        "tools": {
          "exec": { "security": "allowlist", "ask": "on-miss" }
          // no deny_wrappers → can invoke cks-github.sh / cks-vercel.sh
        }
      }
    ]
  }
}

---

{
  "tools": {
    "deny_wrappers": [
      { "interpreters": ["sh", "bash", "python3", "node"], "match": "**/cks-*.sh" },
      { "interpreters": ["python3"], "match": "**/admin-*.py" }
    ]
  }
}

---

{
  "agents": {
    "list": [
      {
        "id": "main",
        "tools": {
          "deny": [
            "github__*",
            "env", "printenv",
            "find", "pwd", "whoami", "id", "uname",
            "ls", "cat", "grep", "head", "tail", "stat", "file"
          ]
        }
      }
    ]
  }
}
RAW_BUFFERClick to expand / collapse

Feature: agents.list[].tools.deny_wrappers — per-agent denial of bash wrapper paths

Summary

agents.list[].tools.deny (manual line 33635, codebase isToolAllowedByPolicyName) is excellent for filtering MCP tools by tool.name (e.g. github__*, supabase__*). However, bash wrappers invoked through tools.exec allowlist do NOT pass through that filter. They are matched against exec-approvals.json and the per-agent tools.exec.security: allowlist policy, which evaluates the binary (sh, bash) — not the script path argument.

This means an agent with full deny on github__* can still execute sh /data/.openclaw/workspace/scripts/cks-github.sh ... and reach the same external service, bypassing the architectural delegation rule the operator wanted to enforce.

We are requesting a new policy field — agents.list[].tools.deny_wrappers (or equivalent name) — that matches against the resolved script path of an exec invocation, with glob support.


Problem statement

In a multi-agent gateway where:

  • A "Maestro" agent (main) routes user requests
  • Domain-specialist leafs (cks-dev, cks-lab, cks-bridge, etc.) own external integrations (GitHub, Supabase, Sergio API)
  • Integrations are exposed as bash wrappers in a shared scripts directory: /data/.openclaw/workspace/scripts/cks-*.sh
  • Each wrapper sources .env, validates inputs, and emits structured output (this is the documented pattern in our ADR-004 — bash wrappers preferred over native MCPs for auditability and stable contracts)

…the Maestro is supposed to delegate to the domain leaf rather than execute the wrapper directly. The intent is architectural separation of concerns: leafs own credentials and rate limits, Maestro orchestrates.

Today, with OpenClaw 2026.4.21 (commit f788c88), the only enforcement options are:

OptionWhat it coversWhat it leaves open
agents.list[main].tools.deny: ["github__*"]MCP tool name filter (e.g. github__get_pull_request)Direct wrapper invocation sh /data/.openclaw/workspace/scripts/cks-github.shtool.name == "exec", not "github__*"
agents.list[main].tools.exec.security: allowlist + remove sh from allowlistBlocks ALL bash execBreaks legitimate operational scripts (logs, status checks, file inspection) — too coarse
agents.list[main].tools.exec.security: allowlist + custom approval per-scriptPer-hash approval on every wrapperMassive UX friction — every config edit invalidates approvals; doesn't compose with leaf inheritance
commands.bash: false (gateway-wide)Closes shell-builtin vector (we use this — see our ADR-017)Doesn't affect tools.exec, which is a separate path; doesn't enforce per-agent wrapper policy

The result: in our setup, the Maestro can reach cks-github.sh directly even with tools.deny: ["github__*"] applied, because tools.deny doesn't see exec invocations.


Concrete use case (CKS / OpenClaw deployment)

We run 5 agents on a self-hosted OpenClaw gateway:

  • main — orchestrator, model openai-codex/gpt-5.5 (OAuth Codex)
  • cks-dev — owns GitHub + Vercel wrappers
  • cks-lab — owns Supabase wrapper
  • cks-bridge — owns external partner API wrapper
  • cks-scout — research / ingest

We want the Maestro structurally unable to invoke cks-github.sh, cks-supabase.sh, cks-vercel.sh, cks-sergio.sh — only cks-dev / cks-lab / cks-bridge should reach those scripts.

Today the only "structural" workaround is moving the wrapper outside the shared workspace and filesystem-isolating per-agent CWD, which is a much larger architectural change (per-agent docker sandbox, per-agent workspace.dir, etc.).

A simple deny_wrappers glob would close the gap with a 1-line config change, matching the existing UX of tools.allow / tools.deny.


Proposed API

Schema

{
  "agents": {
    "list": [
      {
        "id": "main",
        "tools": {
          "exec": {
            "security": "allowlist",
            "ask": "on-miss",
            "strictInlineEval": true
          },
          "deny": ["github__*", "supabase__*"],          // existing — MCP tool filter
          "deny_wrappers": [                              // NEW
            "**/cks-github.sh",
            "**/cks-supabase.sh",
            "**/cks-vercel.sh",
            "**/cks-sergio.sh"
          ]
        }
      },
      {
        "id": "cks-dev",
        "tools": {
          "exec": { "security": "allowlist", "ask": "on-miss" }
          // no deny_wrappers → can invoke cks-github.sh / cks-vercel.sh
        }
      }
    ]
  }
}

Semantics

  • deny_wrappers is evaluated when tools.exec is invoked with sh|bash <script> (or any binary in interpreters: [...], see below).
  • The filter resolves the second positional argument (script path) and matches against the glob list.
  • Glob support: ** (recursive), * (any segment), exact paths.
  • Deny wins over tools.exec allowlist (consistent with tools.deny).
  • On match: same UX as a denied MCP tool — Tool denied by policy: deny_wrappers (cks-github.sh).

Optional refinement — interpreters

To handle non-sh cases (Python scripts, Node binaries):

{
  "tools": {
    "deny_wrappers": [
      { "interpreters": ["sh", "bash", "python3", "node"], "match": "**/cks-*.sh" },
      { "interpreters": ["python3"], "match": "**/admin-*.py" }
    ]
  }
}

Default if string-form is passed: interpreters: ["sh", "bash"].


Current workaround (what we ship today)

Per ADR-013 + ADR-020 (referenced in our internal docs), we expand agents.list[main].tools.deny to include the raw binaries that an LLM might use to perform discovery or reach a wrapper indirectly:

{
  "agents": {
    "list": [
      {
        "id": "main",
        "tools": {
          "deny": [
            "github__*",
            "env", "printenv",
            "find", "pwd", "whoami", "id", "uname",
            "ls", "cat", "grep", "head", "tail", "stat", "file"
          ]
        }
      }
    ]
  }
}

Problems:

  1. Doesn't actually block sh ... cks-github.sh — only stops discovery (the model can't list scripts before calling). Operationally fragile.
  2. Cripples legitimate operational use — Maestro genuinely needs ls, cat, grep for read-only diagnostics. We added them grudgingly because the alternative was infinite approval loops.
  3. Doesn't scale — any new wrapper in cks-*.sh requires updating raw binary deny + retraining the operator on what the Maestro can/can't do.
  4. Negative interaction with approval cascades — see issue #69570: when find/pwd/grep are denied, the model generates approval variants in a loop (every deny → another deny → another deny). A single deny_wrappers match would fail-fast at the wrapper level, before discovery starts.

Why tools.deny alone doesn't cover this

Reading the codebase (lines around 468251 in our scrape — tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy))):

  • tool.name for an MCP call: github__get_pull_request → matches github__*
  • tool.name for an exec call: exec (or bash/sh depending on path) → does NOT match github__*
  • The wrapper script is an argument to exec, never seen by isToolAllowedByPolicyName.

So tools.deny is doing exactly what its name says — it's a tool-name policy, not a path policy. We need a parallel mechanism for exec arguments.


Alternative considered: move wrappers to tools.allow only

You could imagine agents.list[].tools.exec.allowedScripts: [...] (positive list). We rejected that as a request because:

  1. Negative-by-default breaks 90% of agents. Most agents legitimately need ad-hoc scripts. Forcing every script into an allowlist explodes config surface.
  2. Asymmetric with tools.deny UX. Operators already know deny semantics. Adding a parallel positive list creates 4 layers (allow / deny / exec.allow / exec.deny / approval) which is hard to reason about.
  3. deny_wrappers composes cleanly with the existing tools.exec.security: allowlist + per-hash approval system. It's purely additive: "of the things otherwise permitted by exec policy, deny these specific paths."

References

  • Manual OpenClaw line 29215: "Global tool allow/deny policy (deny wins). Case-insensitive, supports * wildcards. Applied even when Docker sandbox is off." — this is the precedent we want extended to exec paths.
  • Manual OpenClaw line 33635: agents.list[].tools.allow/deny officially supported.
  • Codebase TOOL_NAME_SEPARATOR = "__" (line 437233) — confirms tool.name is the dispatch key, not exec args.
  • Related issue #69570 — approval cascade loops are aggravated by lack of fail-fast wrapper denial.
  • Related issue #69386 — agent ignores stop commands during exec denial.
  • Related PR #57568 (closed without merge) — proposed 3-layer defense including hard guard regex; a deny_wrappers field would have given that PR a cleaner home for layer 3.

Repro setup

ItemValue
OpenClaw version2026.4.21 (commit f788c88)
Containeropenclaw-ald8-openclaw-1 (Hostinger official Docker app)
Maestro modelopenai-codex/gpt-5.5 (OAuth Codex via ChatGPT Plus)
Leaf modelsnvidia/nemotron-3-super-120b-a12b (OpenRouter)
PlatformTelegram inbound + webchat
Sandboxembedded (no per-agent docker)

Repro of the gap:

  1. Configure agents.list[main].tools.deny: ["github__*"].
  2. Configure agents.list[main].tools.exec.security: allowlist with sh pre-approved (standard).
  3. From Telegram, send Mergea PR #247.
  4. Expected: Maestro delegates to cks-dev.
  5. Actual: Maestro generates approval for sh /data/.openclaw/workspace/scripts/cks-github.sh merge 247 — bypassing the deny because exec doesn't see github__*.

The deny is doing its job — the MCP github__merge_pr IS blocked. The problem is the parallel path through exec.


Acceptance criteria for the feature

  • agents.list[].tools.deny_wrappers schema field documented.
  • Glob matching consistent with existing tools.deny (case-insensitive, */**).
  • Filter applied at exec dispatch time, before approval prompt — denial fails fast (no approval cascade).
  • Error message clearly identifies the matched glob: Tool denied by policy: deny_wrappers (matched **/cks-github.sh).
  • Composes with tools.deny (both can be set, deny wins on either).
  • Per-agent — leafs without deny_wrappers retain access.
  • No regression on tools.exec.security: allowlist (deny is purely additive).

Operator impact (why we're asking)

We currently mitigate this gap with a 15-entry tools.deny list of raw binaries (env, printenv, find, pwd, whoami, id, uname, ls, cat, grep, head, tail, stat, file, plus github__*). This was applied 25-abr-2026 as defensive hardening after observing the Maestro perform discovery before reaching wrappers.

This list is brittle:

  • Cripples legitimate read-only operational use of the Maestro for diagnostics.
  • Has to be re-evaluated every release as the agent learns new discovery patterns.
  • Doesn't compose with the architectural goal — we want to say "leaf cks-dev owns GitHub", not "Maestro can't run find".

A deny_wrappers field would let us shrink the deny list back to ~3 entries (env, printenv, plus github__* for MCP) and express the architectural rule directly (Maestro can't reach domain wrappers).


Filed by: a self-hosted OpenClaw operator running 5 agents in production for ~3 months. Happy to provide config dumps, repro steps, or repro container if useful.

extent analysis

TL;DR

The proposed solution is to introduce a new policy field agents.list[].tools.deny_wrappers that matches against the resolved script path of an exec invocation, with glob support, to prevent the Maestro agent from bypassing the architectural delegation rule.

Guidance

  • The current tools.deny policy does not cover exec invocations, allowing the Maestro agent to bypass the intended delegation rule.
  • Introducing a deny_wrappers field would provide a way to explicitly deny access to specific script paths, composing with the existing tools.exec.security: allowlist policy.
  • The proposed deny_wrappers field should support glob matching, consistent with the existing tools.deny policy, to allow for flexible and concise configuration.
  • The filter should be applied at exec dispatch time, before approval prompt, to fail fast and prevent approval cascades.

Example

{
  "agents": {
    "list": [
      {
        "id": "main",
        "tools": {
          "deny": ["github__*"],
          "deny_wrappers": ["**/cks-github.sh", "**/cks-supabase.sh"]
        }
      }
    ]
  }
}

This example configuration denies access to GitHub-related MCP tools and specific script paths, ensuring the Maestro agent cannot bypass the delegation rule.

Notes

The introduction of deny_wrappers requires careful consideration of the existing tools.deny and tools.exec.security: allowlist policies to ensure consistent and effective access control.

Recommendation

Apply the proposed deny_wrappers field to the agents.list[].tools configuration to provide a more robust and flexible access control mechanism, allowing operators to express architectural rules directly and shrink the deny list.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Feature: agents.list[].tools.deny_wrappers — per-agent denial of bash wrapper paths [1 participants]