docs/tools/exec-approvals-advanced.md
Advanced exec-approval topics: the safeBins fast-path, interpreter/runtime
binding, and approval-forwarding to chat channels (including native delivery).
For the core policy and approval flow, see Exec approvals.
tools.exec.safeBins names stdin-only binaries (for example cut) that
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.
Default safe bins:
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.
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; long options validate fail-closed (unknown flags and ambiguous abbreviations are rejected).
Denied flags by safe-bin profile:
grep: --dereference-recursive, --directories, --exclude-from, --file, --recursive, -R, -d, -f, -rjq: --argfile, --from-file, --library-path, --rawfile, --slurpfile, -L, -fsort: --compress-program, --files0-from, --output, --random-source, --temporary-directory, -T, -owc: --files0-fromSafe 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. awk,
sed, and jq are always denied as safe bins because their semantics cannot be
validated to stdin-only: jq can read environment data and load jq code from
modules or startup files. Use an explicit allowlist entry or approval prompt for
those tools instead of safeBins.
Safe bins must resolve from trusted binary directories (system defaults plus
optional tools.exec.safeBinTrustedDirs). PATH entries are never auto-trusted.
Default trusted 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 (&&, ||, ;) 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, transparent dispatch wrappers
(for example env, flock, nice, nohup, stdbuf, timeout) persist the
inner executable path instead of the wrapper path. Shell multiplexers
(busybox, toybox) are unwrapped for shell applets (sh, ash, etc.) the
same way. If a wrapper or multiplexer cannot be safely unwrapped, no allowlist
entry is persisted automatically.
If you allowlist interpreters like python3 or node, prefer
tools.exec.strictInlineEval=true so inline eval still requires an explicit
approval. In strict mode, allow-always can still persist benign
interpreter/script invocations, but inline-eval carriers are not persisted
automatically.
| 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, or bare command-name glob for PATH-invoked commands |
| Argument scope | Restricted by safe-bin profile and literal-token rules | Path match by default; optional argPattern can restrict parsed argv |
| Typical examples | head, tail, tr, wc | jq, 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.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:
{
tools: {
exec: {
safeBins: ["myfilter"],
safeBinProfiles: {
myfilter: {
minPositional: 0,
maxPositional: 0,
allowedValueFlags: ["-n", "--limit"],
deniedFlags: ["-f", "--file", "-c", "--command"],
},
},
},
},
}
Approval-backed interpreter/runtime runs are intentionally conservative:
pnpm exec, pnpm node, npm exec, npx) are unwrapped before binding.When approvals are required, the exec tool returns immediately with an approval id. Use that id to
correlate later approved-run system events (Exec finished, and Exec running when configured).
If no decision arrives before the timeout, the request is treated as an approval timeout and
surfaced as a terminal host-command denial. For main-agent async approvals with an originating
session, OpenClaw also resumes that session with an internal followup so the agent observes that
the command did not run instead of later repairing a missing result. Pending exec approvals expire
after 30 minutes by default.
After an approved async exec finishes, OpenClaw sends a followup agent turn to the same session.
Denied async approvals use the same main-session followup path for the denial status, but they do
not register elevated runtime handoffs and they do not run the command. Denials without a resumable
main session are either suppressed or reported through a safe direct route when one exists.
to), followup delivery uses that channel.deliver: false).INVALID_REQUEST.bestEffortDeliver is enabled and no external channel can be resolved, delivery is downgraded to session-only instead of failing.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:
{
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
The /approve command handles both exec approvals and plugin approvals. If the ID does not match a pending exec approval, it automatically checks plugin approvals instead. This fallback is bounded to "approval not found" failures; a real exec approval denial/error does not silently retry as a plugin approval.
Plugin approval forwarding uses the same delivery pipeline as exec approvals but has its own
independent config under approvals.plugin. Enabling or disabling one does not affect the other.
For plugin-authoring behavior, request fields, and decision semantics, see
Plugin permission requests.
{
approvals: {
plugin: {
enabled: true,
mode: "targets",
agentFilter: ["main"],
targets: [
{ channel: "slack", to: "U12345678" },
{ channel: "telegram", to: "123456789" },
],
},
},
}
The config shape is identical to approvals.exec: enabled, mode, agentFilter,
sessionFilter, and targets work the same way.
Channels that support shared interactive replies render the same approval buttons for both exec and
plugin approvals. Channels without shared interactive UI fall back to plain text with /approve
instructions. Plugin approval requests may restrict the available decisions: approval surfaces use
the request's declared decision set, and the Gateway rejects attempts to submit a decision that was
not offered.
When an exec or plugin approval request originates from a deliverable chat surface, that same chat
can approve it with /approve by default. This applies to Slack, Matrix, Microsoft Teams, and
similar deliverable chats, in addition to the existing Web UI and terminal UI flows, using the
normal channel auth model for that conversation. If the originating chat can already send commands
and receive replies, approval requests no longer need a separate native delivery adapter just to
stay pending.
Discord, Telegram, and QQ bot also support same-chat /approve, but those channels still use their
resolved approver list for authorization even when native approval delivery is disabled.
Some channels can also act as native approval clients: Discord, Slack, Telegram, Matrix, and QQ bot.
Native clients add approver DMs, origin-chat fanout, and channel-specific interactive approval UX on
top of the shared same-chat /approve flow.
When native approval cards/buttons are available, that native UI is the primary agent-facing path.
The agent should not also echo a duplicate plain chat /approve command unless the tool result says
chat approvals are unavailable or manual approval is the only remaining path.
If a native approval client is configured but no native runtime is active for the originating
channel, OpenClaw keeps the local deterministic /approve prompt visible. If the native runtime is
active and attempts delivery but no target receives the card, OpenClaw sends a same-chat fallback
notice with the exact /approve <id> <decision> command so the request can still be resolved.
Generic model:
approvals.exec controls forwarding approval prompts to other chat destinationschannels.<channel>.execApprovals controls whether Discord, Slack, Telegram, QQ bot, and similar
channel-specific native clients are enabledapprovals.plugin can also route plugin approvals to Slack
sessions or targets even when Slack exec approvals are disabledusers/<id> approvers resolve from dm.allowFrom or
defaultTo; they do not use reaction events for decisionsapprovals.exec and
approvals.plugin; they do not have channels.<channel>.execApprovals blocksNative approval clients auto-enable DM-first delivery when all of these are true:
execApprovals.approvers or owner
identity such as commands.ownerAllowFromchannels.<channel>.execApprovals.enabled is unset or "auto"Set enabled: false to disable a native approval client explicitly. Set enabled: true to force
it on when approvers resolve. Public origin-chat delivery stays explicit through
channels.<channel>.execApprovals.target. When native target enables origin-chat delivery,
approval prompts include the command text.
FAQ: Why are there two exec approval configs for chat approvals?
channels.discord.execApprovals.*channels.slack.execApprovals.*channels.telegram.execApprovals.*channels.qqbot.execApprovals.*channels.googlechat.dm.allowFrom or
channels.googlechat.defaultTo; no execApprovals block is requiredapprovals.exec and approvals.plugin to route approval prompts to WhatsAppapprovals.exec and approvals.plugin to route approval prompts to SignalNative-client-specific routing:
target: "dm"). Switch to channel or both to also show
approval prompts in the originating Telegram chat/topic. For Telegram forum topics, OpenClaw
preserves the topic for the approval prompt and the post-approval follow-up.execApprovals.approvers) or inferred from
commands.ownerAllowFrom; only resolved approvers can approve or deny.execApprovals.approvers) or inferred from
commands.ownerAllowFrom. Slack plugin approval DMs use Slack plugin approvers from allowFrom
and account default routing, not Slack exec approvers. Slack native buttons preserve approval id
kind, so plugin: ids can resolve plugin approvals without a second Slack-local fallback layer./approve fallback in message text, but card button
callbacks carry only opaque action tokens; the approval id and decision are recovered from
server-side pending state./approve fallback without explicit approvers; Signal reaction resolution
still requires explicit Signal approvers from channels.signal.allowFrom or defaultTo.channels.matrix.dm.allowFrom. Matrix native prompts
include com.openclaw.approval custom event content on the first prompt event so OpenClaw-aware
Matrix clients can read structured approval state while stock clients keep the plain-text
/approve fallback.plugin: ids go straight to plugin
approvals, everything else goes to exec approvals. Native Telegram approval buttons follow the
same bounded exec-to-plugin fallback as /approve.askFallback.Sensitive owner-only group commands such as /diagnostics and /export-trajectory use private
owner routing for approval prompts and final results. OpenClaw first tries a private route on the
same surface where the owner ran the command. If that surface has no private owner route, it falls
back to the first available owner route from commands.ownerAllowFrom, so a Discord group command
can still send the approval and result to the owner's Telegram DM when Telegram is the configured
primary private interface. The group chat only gets a short acknowledgement.
See:
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + approvals + system.run)
Security notes:
0600, token stored in exec-approvals.json.accountId and threadId be used on an approval target?Use accountId when the channel has multiple configured identities and the approval prompt must
leave through one specific account. Use threadId when the destination supports topics or
threads and the prompt should stay inside that thread instead of the top-level chat.
A concrete Telegram case is an operations supergroup with forum topics and two Telegram bot
accounts. The to value names the supergroup, accountId selects the bot account, and threadId
selects the forum topic:
{
approvals: {
exec: {
enabled: true,
mode: "targets",
targets: [
{
channel: "telegram",
to: "-1001234567890",
accountId: "ops-bot",
threadId: "77",
},
],
},
},
channels: {
telegram: {
accounts: {
default: {
name: "Primary bot",
botToken: "env:TELEGRAM_PRIMARY_BOT_TOKEN",
},
"ops-bot": {
name: "Operations bot",
botToken: "env:TELEGRAM_OPS_BOT_TOKEN",
},
},
},
},
}
With that setup, forwarded exec approvals are posted by the ops-bot Telegram account into topic
77 of chat -1001234567890. A target without accountId uses the channel's default account, and
a target without threadId posts to the top-level destination.
No. Session delivery only controls where the prompt appears. It does not by itself authorize every participant in that chat to approve.
For generic same-chat /approve, the sender must already be authorized for commands in that
channel session. If the channel exposes explicit approval approvers, those approvers can authorize
the /approve action even when they are not otherwise command-authorized in that session.
Some channels are stricter. Discord, Telegram, Matrix, Slack native approval DMs, and similar
native approval clients use their resolved approver lists for approval authorization. For example,
a Telegram forum-topic approval prompt can be visible to everyone in the topic, but only numeric
Telegram user IDs resolved from channels.telegram.execApprovals.approvers or
commands.ownerAllowFrom can approve or deny it.