Advanced Exec Approvals: Safe Bins, Interpreter Binding, Approval Forwarding
This page covers advanced exec approval concepts for technical users, including safe bins for stdin-only executables, interpreter and runtime binding, and forwarding approvals to chat channels with native delivery support.
Read this when
- Configuring safe bins or custom safe-bin profiles
- Forwarding approvals to Slack/Discord/Telegram or other chat channels
- Implementing a native approval client for a channel
Advanced exec approval concepts include the safeBins fast path, interpreter and runtime binding, and forwarding approvals to chat channels with native delivery support. For the foundational policy and approval workflow, refer to Exec approvals.
Safe bins (stdin-only)
The tools.exec.safeBins list contains stdin-only executables such as cut that function in allowlist mode without requiring explicit allowlist entries. These safe binaries reject file arguments and path-like tokens, restricting them to processing only the input stream. Consider this a limited fast path for stream filters rather than a broad trust mechanism.
Warning
Refrain from adding interpreter or runtime executables like
python3,node,ruby,bash,sh, orzshtosafeBins. When a binary can evaluate code, run subcommands, or read files by design, use explicit allowlist entries and keep approval prompts active. Custom safe binaries must declare an explicit profile intools.exec.safeBinProfiles.<bin>.
Default safe binaries:
cut, uniq, head, tail, tr, wc
grep and sort are excluded from the default set. If you choose to include them, maintain explicit allowlist entries for their non stdin workflows. For grep operating in safe bin mode, supply the pattern using -e or --regexp; positional pattern format is disallowed to prevent file operands from being passed as ambiguous positional arguments.
Argv validation and denied flags
Validation relies solely on the argv structure, with no host filesystem existence checks, preventing file existence oracle behavior based on allow or deny differences. File related options are blocked for default safe binaries; long options fail closed, rejecting unknown flags and ambiguous abbreviations. Recognized read only boolean flags of the default binaries, such as wc -l, tr -d, and uniq -c, are permitted, while unrecognized short flags remain fail closed and are escalated for manual approval.
Flags denied per 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,-otail:--follow,--retry,-F,-fwc:--files0-from
When a tool is classified as a safe bin, its argv tokens are forced to be literal text during execution. This means no globbing or $VARS expansion occurs for stdin-only segments, so patterns like * or $HOME/... cannot be used to sneak in file reads. The tools awk, sed, and jq are permanently excluded from safe bin status because their behavior cannot be restricted to stdin-only operation: jq has the ability to read environment data and load jq code from modules or startup files. For these tools, use an explicit allowlist entry or an approval prompt instead of safeBins.
Trusted binary directories
Safe bins must resolve from directories that are trusted, which include system defaults and any optional tools.exec.safeBinTrustedDirs entries. Entries under PATH are never automatically trusted. The set of default trusted directories is kept deliberately small: /bin, /usr/bin. If your safe bin executable lives in package-manager or user paths, for instance /opt/homebrew/bin, /usr/local/bin, /opt/local/bin, /snap/bin, you need to add those paths explicitly to tools.exec.safeBinTrustedDirs.
Shell chaining, wrappers, and multiplexers
Shell chaining, using &&, ||, or ;, is permitted as long as every top-level segment passes the allowlist check, which includes safe bins or skill auto-allow. Redirections are still not supported in allowlist mode. Command substitution via $() or backticks is blocked during allowlist parsing, even inside double quotes; use single quotes if you need literal $() text.
On macOS companion app approvals, any raw shell text that includes shell control or expansion syntax (&&, ||, ;, |, `, $, <, >, (, )) counts as a miss against the allowlist, unless the shell binary itself has been allowlisted.
For shell wrappers (bash|sh|zsh ... -c/-lc), request scoped environment overrides are limited to a small explicit set of allowed values (TERM, LANG, LC_*, COLORTERM, NO_COLOR, FORCE_COLOR).
When making allow-always decisions in allowlist mode, transparent dispatch wrappers (such as env, flock, nice, nohup, stdbuf, timeout) store the inner executable path rather than the wrapper path. Shell multiplexers (busybox, toybox) are unwrapped for shell applets (sh, ash, and others) in the same manner. If a wrapper or multiplexer cannot be safely unwrapped, no allowlist entry is automatically persisted.
When allowlisting interpreters like python3 or node, choose tools.exec.strictInlineEval=true so that inline eval still needs an explicit approval. In strict mode, allow-always can still persist benign interpreter or script invocations, but inline eval carriers are not persisted automatically.
Safe bins versus allowlist
| Topic | tools.exec.safeBins | Allowlist (exec-approvals.json) |
|---|---|---|
| Goal | Automatically allow narrow stdin filters | Explicitly trust particular executables |
| Match type | Executable name plus safe bin argv policy | Resolved executable path glob, or bare command name glob for PATH invoked commands |
| Argument scope | Constrained 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:
- The value for
safeBinsis sourced from configuration, eithertools.exec.safeBinsor a per-agent settingagents.entries.*.tools.exec.safeBins. safeBinTrustedDirsis pulled from configuration, viatools.exec.safeBinTrustedDirsor the agent-specificagents.entries.*.tools.exec.safeBinTrustedDirs.- Configuration provides
safeBinProfilesthroughtools.exec.safeBinProfilesor the per-agentagents.entries.*.tools.exec.safeBinProfiles. Agent-level profile keys take precedence over global ones. - Allowlist entries reside in the host-local approvals file under
agents.<id>.allowlist, or can be managed through the Control UI oropenclaw approvals allowlist .... - When interpreter or runtime binaries appear in
safeBinswithout explicit profiles,openclaw security auditissues a warning usingtools.exec.safe_bins_interpreter_unprofiled. - Missing custom
safeBinProfiles.<bin>entries can be scaffolded as{}usingopenclaw doctor --fix, but you should review and tighten them afterward. Interpreter and runtime binaries are never auto-scaffolded.
Custom profile example:
{
tools: {
exec: {
safeBins: ["myfilter"],
safeBinProfiles: {
myfilter: {
minPositional: 0,
maxPositional: 0,
allowedValueFlags: ["-n", "--limit"],
deniedFlags: ["-f", "--file", "-c", "--command"],
},
},
},
},
}
Interpreter/runtime commands
Approval-based interpreter and runtime runs deliberately follow conservative rules:
- The exact argv, cwd, and env context is always bound.
- Direct shell scripts and direct runtime file forms are best-effort bound to a single concrete local file snapshot.
- Common package manager wrapper forms that still resolve to one direct local file, such as
pnpm exec,pnpm node,npm exec, andnpx, are unwrapped before binding. - If OpenClaw cannot identify exactly one concrete local file for an interpreter or runtime command (for example, package scripts, eval forms, runtime-specific loader chains, or ambiguous multi-file forms), approval-backed execution is denied rather than claiming semantic coverage it does not possess.
- For those workflows, prefer sandboxing, a separate host boundary, or an explicit trusted allowlist or 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 approved-run system events, specifically Exec finished and, when configured, Exec running. 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.
Followup delivery behavior
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.
- If a valid external delivery target exists, meaning a deliverable channel plus target
to, followup delivery uses that channel. - In webchat-only or internal-session flows with no external target, followup delivery stays session-only, identified as
deliver: false. - If a caller explicitly requests strict external delivery with no resolvable external channel, the request fails with
INVALID_REQUEST. - If
bestEffortDeliveris enabled and no external channel can be resolved, delivery is downgraded to session-only instead of failing.
Minimal scopes for third-party clients
Gateway approval resolution is protected by the dedicated operator.approvals scope. This applies to both the owner-specific exec.approval.resolve method and the kind-agnostic approval.resolve method; operator.write does not subsume it. Dashboards and integrations should request only the scopes required by the methods they use. Treat approval-resolution access as remote-execution-grade authority and grant operator.approvals deliberately, even when the client only presents a small approval UI.
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:
{
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 or error does not silently retry as a plugin approval.
Plugin approval forwarding
Plugin approval forwarding uses the same delivery pipeline as exec approvals but has its own independent configuration 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.
Same-chat approvals on any channel
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.
Native approval delivery
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 or 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.
When a native approval client is configured but no native runtime is active on the originating channel, OpenClaw continues showing the local deterministic /approve prompt. If the native runtime is active and tries to deliver the card but no target accepts it, OpenClaw sends a fallback notice in the same chat containing the exact /approve <id> <decision> command, so the request can still be completed.
General model:
- The host exec policy still determines whether exec approval is required
approvals.execmanages forwarding approval prompts to other chat destinationschannels.<channel>.execApprovalsenables or disables channel-specific native clients for Discord, Slack, Telegram, QQ bot, and similar platforms- Slack plugin approvals can use Slack's native approval client when the request originates from Slack and Slack plugin approvers are resolved;
approvals.plugincan also route plugin approvals to Slack sessions or targets even when Slack exec approvals are turned off - Google Chat native approval cards handle exec and plugin approvals that come from Google Chat spaces or threads when stable
users/<id>approvers are resolved fromdm.allowFromordefaultTo; they do not use reaction events for decisions - WhatsApp and Signal reaction approval delivery is controlled by
approvals.execandapprovals.plugin; they have nochannels.<channel>.execApprovalsblocks
Native approval clients automatically enable DM-first delivery when all of these conditions are met:
- The channel supports native approval delivery
- Approvers can be resolved from explicit
execApprovals.approversor owner identity such ascommands.ownerAllowFrom channels.<channel>.execApprovals.enabledis not set or is"auto"
Use enabled: false to explicitly disable a native approval client. Use enabled: true to force it on when approvers are resolved. Public origin-chat delivery remains 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?
- Discord:
channels.discord.execApprovals.* - Slack:
channels.slack.execApprovals.* - Telegram:
channels.telegram.execApprovals.* - QQ bot:
channels.qqbot.execApprovals.* - Google Chat: configure stable approvers with
channels.googlechat.dm.allowFromorchannels.googlechat.defaultTo; noexecApprovalsblock is needed - WhatsApp: use
approvals.execandapprovals.pluginto send approval prompts to WhatsApp - Signal: use
approvals.execandapprovals.pluginto send approval prompts to Signal
Routing specific to native clients:
- Telegram defaults to approver DMs (
target: "dm"). Switch tochannelorbothto also display approval prompts in the originating Telegram chat or topic. For Telegram forum topics, OpenClaw keeps the topic for both the approval prompt and the post-approval follow-up. - Discord and Telegram approvers can be explicit (
execApprovals.approvers) or inferred fromcommands.ownerAllowFrom; only resolved approvers can approve or deny. - Slack approvers can be explicit (
execApprovals.approvers) or inferred fromcommands.ownerAllowFrom. Slack plugin approval DMs use Slack plugin approvers fromallowFromand account default routing, not Slack exec approvers. Slack native buttons preserve the approval id kind, soplugin:ids can resolve plugin approvals without a second Slack-local fallback layer. - Google Chat native cards keep the manual
/approvefallback in message text, but card button callbacks carry only opaque action tokens; the approval id and decision are recovered from server-side pending state. - WhatsApp emoji approvals handle both exec and plugin prompts when the matching top-level forwarding family routes to WhatsApp. Native-origin prompts bind directly; shared target-mode delivery binds the same typed approval metadata to the accepted WhatsApp message receipt.
- Signal reaction approvals handle both exec and plugin prompts only when the matching top-level forwarding family is enabled and routes to Signal. Direct same-chat Signal exec approvals can suppress the local
/approvefallback without explicit approvers; Signal reaction resolution still requires explicit Signal approvers fromchannels.signal.allowFromordefaultTo. - Matrix native DM or channel routing and reaction shortcuts handle both exec and plugin approvals; plugin authorization still comes from
channels.matrix.dm.allowFrom. Matrix native prompts includecom.openclaw.approvalcustom event content on the first prompt event so OpenClaw-aware Matrix clients can read structured approval state while stock clients keep the plain-text/approvefallback. - Native Discord and Telegram approval buttons carry an explicit exec or plugin owner kind in transport-private callback data and resolve only that owner. Older
/approvecontrols that lack a kind remain a bounded compatibility path: they try only owner kinds the actor may approve, continue only after an approval-not-found result, and never infer ownership from the approval ID. - The requester does not need to be an approver.
- If no operator UI or configured approval client can accept the request, the prompt falls back to
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:
Official mobile operator apps
The official iOS and Android apps can also review Gateway-owned pending exec approvals when an operator.admin connection is used, or when their paired operator.approvals device was explicitly targeted by the request. They read the same sanitized durable record used by the Control UI, submit a kind-aware decision, and display the Gateway's canonical first-answer result. The Apple Watch mirrors these approval prompts through the paired iPhone, with allow-once and deny actions. Direct Watch Gateway mode does not review approvals.
A lost resolve acknowledgement does not make the submitted choice authoritative: the app disables the controls and reads the record again. If another surface won, the app shows that recorded decision. Pending prompts remain bound to the Gateway that issued them, so switching the active Gateway cannot redirect an old approval ID.
macOS IPC flow
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + approvals + system.run)
Security considerations:
- Unix socket mode
0600, token saved inexec-approvals.json. - Same UID peer verification.
- Challenge/response mechanism (nonce, HMAC token, request hash) combined with a brief TTL.
FAQ
When would accountId and threadId be used on an approval target?
Apply accountId when multiple identities are configured on the channel and the approval prompt must exit through a designated account. Apply threadId when the destination supports topics or threads and the prompt should remain within that thread rather than the main chat.
A practical Telegram scenario involves an operations supergroup with forum topics and two Telegram bot accounts. The to value identifies the supergroup, accountId picks the bot account, and threadId chooses 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 this configuration, forwarded exec approvals are sent by the ops-bot Telegram account into topic 77 of chat -1001234567890. A target lacking accountId falls back to the channel's default account, and a target without threadId sends to the top-level destination.
When approvals are sent to a session, can anyone in that session approve them?
No. Session delivery only determines where the prompt appears. It does not automatically authorize every member of that chat to approve.
For generic same-chat /approve, the sender must already hold command authorization in that channel session. If the channel defines explicit approval approvers, those approvers can authorize the /approve action even if they lack command authorization elsewhere in that session.
Some channels enforce stricter rules. Discord, Telegram, Matrix, Slack native approval DMs, and similar native approval clients rely on their resolved approver lists for approval authorization. For instance, a Telegram forum topic approval prompt may 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 reject it.
Related
- Exec approvals, core policy and approval workflow
- Exec tool
- Elevated mode
- Skills, skill-based auto-allow behavior