| --- |
| summary: "Exec approvals, allowlists, and sandbox escape prompts" |
| read_when: |
| - Configuring exec approvals or allowlists |
| - Implementing exec approval UX in the macOS app |
| - Reviewing sandbox escape prompts and implications |
| title: "Exec Approvals" |
| --- |
| |
| # Exec approvals |
|
|
| Exec approvals are the **companion app / node host guardrail** for letting a sandboxed agent run |
| commands on a real host (`gateway` or `node`). Think of it like a safety interlock: |
| commands are allowed only when policy + allowlist + (optional) user approval all agree. |
| Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals). |
| Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used. |
|
|
| If the companion app UI is **not available**, any request that requires a prompt is |
| resolved by the **ask fallback** (default: deny). |
|
|
| ## Where it applies |
|
|
| Exec approvals are enforced locally on the execution host: |
|
|
| - **gateway host** → `openclaw` process on the gateway machine |
| - **node host** → node runner (macOS companion app or headless node host) |
|
|
| Trust model note: |
|
|
| - Gateway-authenticated callers are trusted operators for that Gateway. |
| - Paired nodes extend that trusted operator capability onto the node host. |
| - Exec approvals reduce accidental execution risk, but are not a per-user auth boundary. |
| - Approved node-host runs bind canonical execution context: canonical cwd, exact argv, env |
| binding when present, and pinned executable path when applicable. |
| - For shell scripts and direct interpreter/runtime file invocations, OpenClaw also tries to bind |
| one concrete local file operand. If that bound file changes after approval but before execution, |
| the run is denied instead of executing drifted content. |
| - This file binding is intentionally best-effort, not a complete semantic model of every |
| interpreter/runtime loader path. If approval mode cannot identify exactly one concrete local |
| file to bind, it refuses to mint an approval-backed run instead of pretending full coverage. |
|
|
| macOS split: |
|
|
| - **node host service** forwards `system.run` to the **macOS app** over local IPC. |
| - **macOS app** enforces approvals + executes the command in UI context. |
|
|
| ## Settings and storage |
|
|
| Approvals live in a local JSON file on the execution host: |
|
|
| `~/.openclaw/exec-approvals.json` |
|
|
| Example schema: |
|
|
| ```json |
| { |
| "version": 1, |
| "socket": { |
| "path": "~/.openclaw/exec-approvals.sock", |
| "token": "base64url-token" |
| }, |
| "defaults": { |
| "security": "deny", |
| "ask": "on-miss", |
| "askFallback": "deny", |
| "autoAllowSkills": false |
| }, |
| "agents": { |
| "main": { |
| "security": "allowlist", |
| "ask": "on-miss", |
| "askFallback": "deny", |
| "autoAllowSkills": true, |
| "allowlist": [ |
| { |
| "id": "B0C8C0B3-2C2D-4F8A-9A3C-5A4B3C2D1E0F", |
| "pattern": "~/Projects/**/bin/rg", |
| "lastUsedAt": 1737150000000, |
| "lastUsedCommand": "rg -n TODO", |
| "lastResolvedPath": "/Users/user/Projects/.../bin/rg" |
| } |
| ] |
| } |
| } |
| } |
| ``` |
|
|
| ## Policy knobs |
|
|
| ### Security (`exec.security`) |
|
|
| - **deny**: block all host exec requests. |
| - **allowlist**: allow only allowlisted commands. |
| - **full**: allow everything (equivalent to elevated). |
|
|
| ### Ask (`exec.ask`) |
|
|
| - **off**: never prompt. |
| - **on-miss**: prompt only when allowlist does not match. |
| - **always**: prompt on every command. |
|
|
| ### Ask fallback (`askFallback`) |
|
|
| If a prompt is required but no UI is reachable, fallback decides: |
|
|
| - **deny**: block. |
| - **allowlist**: allow only if allowlist matches. |
| - **full**: allow. |
|
|
| ## Allowlist (per agent) |
|
|
| Allowlists are **per agent**. If multiple agents exist, switch which agent you’re |
| editing in the macOS app. Patterns are **case-insensitive glob matches**. |
| Patterns should resolve to **binary paths** (basename-only entries are ignored). |
| Legacy `agents.default` entries are migrated to `agents.main` on load. |
|
|
| Examples: |
|
|
| - `~/Projects/**/bin/peekaboo` |
| - `~/.local/bin/*` |
| - `/opt/homebrew/bin/rg` |
|
|
| Each allowlist entry tracks: |
|
|
| - **id** stable UUID used for UI identity (optional) |
| - **last used** timestamp |
| - **last used command** |
| - **last resolved path** |
|
|
| ## Auto-allow skill CLIs |
|
|
| When **Auto-allow skill CLIs** is enabled, executables referenced by known skills |
| are treated as allowlisted on nodes (macOS node or headless node host). This uses |
| `skills.bins` over the Gateway RPC to fetch the skill bin list. Disable this if you want strict manual allowlists. |
|
|
| Important trust notes: |
|
|
| - This is an **implicit convenience allowlist**, separate from manual path allowlist entries. |
| - It is intended for trusted operator environments where Gateway and node are in the same trust boundary. |
| - If you require strict explicit trust, keep `autoAllowSkills: false` and use manual path allowlist entries only. |
|
|
| ## Safe bins (stdin-only) |
|
|
| `tools.exec.safeBins` defines a small list of **stdin-only** binaries (for example `jq`) |
| that can run in allowlist mode **without** explicit allowlist entries. Safe bins reject |
| positional file args and path-like tokens, so they can only operate on the incoming stream. |
| Treat this as a narrow fast-path for stream filters, not a general trust list. |
| Do **not** add interpreter or runtime binaries (for example `python3`, `node`, `ruby`, `bash`, `sh`, `zsh`) to `safeBins`. |
| If a command can evaluate code, execute subcommands, or read files by design, prefer explicit allowlist entries and keep approval prompts enabled. |
| Custom safe bins must define an explicit profile in `tools.exec.safeBinProfiles.<bin>`. |
| Validation is deterministic from argv shape only (no host filesystem existence checks), which |
| prevents file-existence oracle behavior from allow/deny differences. |
| File-oriented options are denied for default safe bins (for example `sort -o`, `sort --output`, |
| `sort --files0-from`, `sort --compress-program`, `sort --random-source`, |
| `sort --temporary-directory`/`-T`, `wc --files0-from`, `jq -f/--from-file`, |
| `grep -f/--file`). |
| Safe bins also enforce explicit per-binary flag policy for options that break stdin-only |
| behavior (for example `sort -o/--output/--compress-program` and grep recursive flags). |
| Long options are validated fail-closed in safe-bin mode: unknown flags and ambiguous |
| abbreviations are rejected. |
| Denied flags by safe-bin profile: |
|
|
| <!-- SAFE_BIN_DENIED_FLAGS:START --> |
| |
| - `grep`: `--dereference-recursive`, `--directories`, `--exclude-from`, `--file`, `--recursive`, `-R`, `-d`, `-f`, `-r` |
| - `jq`: `--argfile`, `--from-file`, `--library-path`, `--rawfile`, `--slurpfile`, `-L`, `-f` |
| - `sort`: `--compress-program`, `--files0-from`, `--output`, `--random-source`, `--temporary-directory`, `-T`, `-o` |
| - `wc`: `--files0-from` |
| <!-- SAFE_BIN_DENIED_FLAGS:END --> |
|
|
| Safe bins also force argv tokens to be treated as **literal text** at execution time (no globbing |
| and no `$VARS` expansion) for stdin-only segments, so patterns like `*` or `$HOME/...` cannot be |
| used to smuggle file reads. |
| Safe bins must also resolve from trusted binary directories (system defaults plus optional |
| `tools.exec.safeBinTrustedDirs`). `PATH` entries are never auto-trusted. |
| Default trusted safe-bin directories are intentionally minimal: `/bin`, `/usr/bin`. |
| If your safe-bin executable lives in package-manager/user paths (for example |
| `/opt/homebrew/bin`, `/usr/local/bin`, `/opt/local/bin`, `/snap/bin`), add them explicitly |
| to `tools.exec.safeBinTrustedDirs`. |
| Shell chaining and redirections are not auto-allowed in allowlist mode. |
|
|
| Shell chaining (`&&`, `||`, `;`) is allowed when every top-level segment satisfies the allowlist |
| (including safe bins or skill auto-allow). Redirections remain unsupported in allowlist mode. |
| Command substitution (`$()` / backticks) is rejected during allowlist parsing, including inside |
| double quotes; use single quotes if you need literal `$()` text. |
| On macOS companion-app approvals, raw shell text containing shell control or expansion syntax |
| (`&&`, `||`, `;`, `|`, `` ` ``, `$`, `<`, `>`, `(`, `)`) is treated as an allowlist miss unless |
| the shell binary itself is allowlisted. |
| For shell wrappers (`bash|sh|zsh ... -c/-lc`), request-scoped env overrides are reduced to a |
| small explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`). |
| For allow-always decisions in allowlist mode, known dispatch wrappers |
| (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper |
| paths. Shell multiplexers (`busybox`, `toybox`) are also unwrapped for shell applets (`sh`, `ash`, |
| etc.) so inner executables are persisted instead of multiplexer binaries. If a wrapper or |
| multiplexer cannot be safely unwrapped, no allowlist entry is persisted automatically. |
|
|
| Default safe bins: `jq`, `cut`, `uniq`, `head`, `tail`, `tr`, `wc`. |
|
|
| `grep` and `sort` are not in the default list. If you opt in, keep explicit allowlist entries for |
| their non-stdin workflows. |
| For `grep` in safe-bin mode, provide the pattern with `-e`/`--regexp`; positional pattern form is |
| rejected so file operands cannot be smuggled as ambiguous positionals. |
|
|
| ### Safe bins versus allowlist |
|
|
| | Topic | `tools.exec.safeBins` | Allowlist (`exec-approvals.json`) | |
| | ---------------- | ------------------------------------------------------ | ------------------------------------------------------------ | |
| | Goal | Auto-allow narrow stdin filters | Explicitly trust specific executables | |
| | Match type | Executable name + safe-bin argv policy | Resolved executable path glob pattern | |
| | Argument scope | Restricted by safe-bin profile and literal-token rules | Path match only; arguments are otherwise your responsibility | |
| | Typical examples | `jq`, `head`, `tail`, `wc` | `python3`, `node`, `ffmpeg`, custom CLIs | |
| | Best use | Low-risk text transforms in pipelines | Any tool with broader behavior or side effects | |
|
|
| Configuration location: |
|
|
| - `safeBins` comes from config (`tools.exec.safeBins` or per-agent `agents.list[].tools.exec.safeBins`). |
| - `safeBinTrustedDirs` comes from config (`tools.exec.safeBinTrustedDirs` or per-agent `agents.list[].tools.exec.safeBinTrustedDirs`). |
| - `safeBinProfiles` comes from config (`tools.exec.safeBinProfiles` or per-agent `agents.list[].tools.exec.safeBinProfiles`). Per-agent profile keys override global keys. |
| - allowlist entries live in host-local `~/.openclaw/exec-approvals.json` under `agents.<id>.allowlist` (or via Control UI / `openclaw approvals allowlist ...`). |
| - `openclaw security audit` warns with `tools.exec.safe_bins_interpreter_unprofiled` when interpreter/runtime bins appear in `safeBins` without explicit profiles. |
| - `openclaw doctor --fix` can scaffold missing custom `safeBinProfiles.<bin>` entries as `{}` (review and tighten afterward). Interpreter/runtime bins are not auto-scaffolded. |
|
|
| Custom profile example: |
|
|
| ```json5 |
| { |
| tools: { |
| exec: { |
| safeBins: ["jq", "myfilter"], |
| safeBinProfiles: { |
| myfilter: { |
| minPositional: 0, |
| maxPositional: 0, |
| allowedValueFlags: ["-n", "--limit"], |
| deniedFlags: ["-f", "--file", "-c", "--command"], |
| }, |
| }, |
| }, |
| }, |
| } |
| ``` |
|
|
| ## Control UI editing |
|
|
| Use the **Control UI → Nodes → Exec approvals** card to edit defaults, per‑agent |
| overrides, and allowlists. Pick a scope (Defaults or an agent), tweak the policy, |
| add/remove allowlist patterns, then **Save**. The UI shows **last used** metadata |
| per pattern so you can keep the list tidy. |
|
|
| The target selector chooses **Gateway** (local approvals) or a **Node**. Nodes |
| must advertise `system.execApprovals.get/set` (macOS app or headless node host). |
| If a node does not advertise exec approvals yet, edit its local |
| `~/.openclaw/exec-approvals.json` directly. |
|
|
| CLI: `openclaw approvals` supports gateway or node editing (see [Approvals CLI](/cli/approvals)). |
|
|
| ## Approval flow |
|
|
| When a prompt is required, the gateway broadcasts `exec.approval.requested` to operator clients. |
| The Control UI and macOS app resolve it via `exec.approval.resolve`, then the gateway forwards the |
| approved request to the node host. |
|
|
| For `host=node`, approval requests include a canonical `systemRunPlan` payload. The gateway uses |
| that plan as the authoritative command/cwd/session context when forwarding approved `system.run` |
| requests. |
|
|
| ## Interpreter/runtime commands |
|
|
| Approval-backed interpreter/runtime runs are intentionally conservative: |
|
|
| - Exact argv/cwd/env context is always bound. |
| - Direct shell script and direct runtime file forms are best-effort bound to one concrete local |
| file snapshot. |
| - Common package-manager wrapper forms that still resolve to one direct local file (for example |
| `pnpm exec`, `pnpm node`, `npm exec`, `npx`) are unwrapped before binding. |
| - If OpenClaw cannot identify exactly one concrete local file for an interpreter/runtime command |
| (for example package scripts, eval forms, runtime-specific loader chains, or ambiguous multi-file |
| forms), approval-backed execution is denied instead of claiming semantic coverage it does not |
| have. |
| - For those workflows, prefer sandboxing, a separate host boundary, or an explicit trusted |
| allowlist/full workflow where the operator accepts the broader runtime semantics. |
|
|
| When approvals are required, the exec tool returns immediately with an approval id. Use that id to |
| correlate later system events (`Exec finished` / `Exec denied`). If no decision arrives before the |
| timeout, the request is treated as an approval timeout and surfaced as a denial reason. |
|
|
| The confirmation dialog includes: |
|
|
| - command + args |
| - cwd |
| - agent id |
| - resolved executable path |
| - host + policy metadata |
|
|
| Actions: |
|
|
| - **Allow once** → run now |
| - **Always allow** → add to allowlist + run |
| - **Deny** → block |
|
|
| ## Approval forwarding to chat channels |
|
|
| You can forward exec approval prompts to any chat channel (including plugin channels) and approve |
| them with `/approve`. This uses the normal outbound delivery pipeline. |
|
|
| Config: |
|
|
| ```json5 |
| { |
| approvals: { |
| exec: { |
| enabled: true, |
| mode: "session", // "session" | "targets" | "both" |
| agentFilter: ["main"], |
| sessionFilter: ["discord"], // substring or regex |
| targets: [ |
| { channel: "slack", to: "U12345678" }, |
| { channel: "telegram", to: "123456789" }, |
| ], |
| }, |
| }, |
| } |
| ``` |
|
|
| Reply in chat: |
|
|
| ``` |
| /approve <id> allow-once |
| /approve <id> allow-always |
| /approve <id> deny |
| ``` |
|
|
| ### Built-in chat approval clients |
|
|
| Discord and Telegram can also act as explicit exec approval clients with channel-specific config. |
|
|
| - Discord: `channels.discord.execApprovals.*` |
| - Telegram: `channels.telegram.execApprovals.*` |
|
|
| These clients are opt-in. If a channel does not have exec approvals enabled, OpenClaw does not treat |
| that channel as an approval surface just because the conversation happened there. |
|
|
| Shared behavior: |
|
|
| - only configured approvers can approve or deny |
| - the requester does not need to be an approver |
| - when channel delivery is enabled, approval prompts include the command text |
| - if no operator UI or configured approval client can accept the request, the prompt falls back to `askFallback` |
|
|
| Telegram defaults to approver DMs (`target: "dm"`). You can switch to `channel` or `both` when you |
| want approval prompts to appear in the originating Telegram chat/topic as well. For Telegram forum |
| topics, OpenClaw preserves the topic for the approval prompt and the post-approval follow-up. |
|
|
| See: |
|
|
| - [Discord](/channels/discord#exec-approvals-in-discord) |
| - [Telegram](/channels/telegram#exec-approvals-in-telegram) |
|
|
| ### macOS IPC flow |
|
|
| ``` |
| Gateway -> Node Service (WS) |
| | IPC (UDS + token + HMAC + TTL) |
| v |
| Mac App (UI + approvals + system.run) |
| ``` |
|
|
| Security notes: |
|
|
| - Unix socket mode `0600`, token stored in `exec-approvals.json`. |
| - Same-UID peer check. |
| - Challenge/response (nonce + HMAC token + request hash) + short TTL. |
|
|
| ## System events |
|
|
| Exec lifecycle is surfaced as system messages: |
|
|
| - `Exec running` (only if the command exceeds the running notice threshold) |
| - `Exec finished` |
| - `Exec denied` |
|
|
| These are posted to the agent’s session after the node reports the event. |
| Gateway-host exec approvals emit the same lifecycle events when the command finishes (and optionally when running longer than the threshold). |
| Approval-gated execs reuse the approval id as the `runId` in these messages for easy correlation. |
|
|
| ## Implications |
|
|
| - **full** is powerful; prefer allowlists when possible. |
| - **ask** keeps you in the loop while still allowing fast approvals. |
| - Per-agent allowlists prevent one agent’s approvals from leaking into others. |
| - Approvals only apply to host exec requests from **authorized senders**. Unauthorized senders cannot issue `/exec`. |
| - `/exec security=full` is a session-level convenience for authorized operators and skips approvals by design. |
| To hard-block host exec, set approvals security to `deny` or deny the `exec` tool via tool policy. |
|
|
| Related: |
|
|
| - [Exec tool](/tools/exec) |
| - [Elevated mode](/tools/elevated) |
| - [Skills](/tools/skills) |
|
|