--- 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 also bind canonical execution context: canonical cwd, pinned executable path when applicable, and interpreter-style script operands. If a bound script changes after approval but before execution, the run is denied instead of executing drifted content. 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.`. 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: - `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 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..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.` 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. 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 allow-once /approve allow-always /approve 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)