Shell Execution on Bots
Turn any channel bot into a shell-capable agent with per-platform approval routing — one YAML flag
Flip one YAML flag and any inbound channel bot can run shell commands, with approval routed to the platform of your choice.
Before this feature, adding execute_command to an inbound bot meant building the agent, attaching an approval backend, and lifting the default deny — all in Python. Now the operator flips one flag and the gateway wires everything at bot construction.
Quick Start
- Turn it on
Add
allow_shell: trueto any channel. On a loopback-bound gateway with a single-operator DM, auto-approve every command:yaml gateway: bind_host: "127.0.0.1" channels: slack: token: "${SLACK_BOT_TOKEN}" app_token: "${SLACK_APP_TOKEN}" allow_shell: true auto_approve_shell: true # kept: loopback bind + no group_policyStart the gateway:
bash praisonai gateway startNow
@your-bot run lsexecutes on the server. - Require approval for real workloads
Turn off auto-approve and route the prompt to the owner. Every shell call now sends a Slack Block Kit approval message to
UOWNER, and onlyUOWNERcan approve:yaml channels: slack: token: "${SLACK_BOT_TOKEN}" app_token: "${SLACK_APP_TOKEN}" allow_shell: true auto_approve_shell: false approval_channel: "UOWNER" approval_users: "UOWNER"
Exposure-aware auto-approval
auto_approve_shell: true runs every shell call immediately only when the gateway is loopback-bound and the channel is a single-operator DM. On any exposed or multi-user surface it is silently downgraded to an approval backend — the exact wiring auto_approve_shell: false produces.
This mirrors the gateway auth token’s “calibrated by exposure” posture (assert_external_bind_safe): blanket auto-approve is only trusted where a single operator reaches the bot over loopback.
Even with auto-approval, a bot-exposed execute_command does not see OPENAI_API_KEY, GATEWAY_AUTH_TOKEN, or other _TOKEN/_SECRET/*_PASSWORD names in its environment, so env or echo $… cannot print them — the shell tool strips them from the subprocess env. See Credential Environment Variables.
Behaviour matrix
| Gateway bind | Channel group_policy | auto_approve_shell_acknowledge_exposed | Effective backend |
|---|---|---|---|
loopback (127.0.0.1 / ::1) | unset (DM / non-group) | — | Blanket auto-approve (AutoApproveBackend) — unchanged behaviour |
externally-bound (0.0.0.0 or a real interface) | any | false (default) | Downgraded to approval backend (Slack / Telegram / Discord / HTTP / gateway approval — same routing as auto_approve_shell: false) |
| loopback | any group policy (mention_only, command_only, all, …) | false (default) | Downgraded to approval backend |
| externally-bound or group channel | any | true | Blanket auto-approve (explicit opt-in — WARN not emitted) |
| bind host unknown / unresolvable | — | — | Fail closed — treat as exposed, downgrade to approval |
The downgrade routes through the same approval-backend precedence documented under Choosing an approval_mode (Slack / Telegram / Discord / HTTP / gateway → fail-closed CallbackBackend). No new backend, no new precedence — only the trigger changes. Watch for this WARN line in gateway logs:
Channel 'slack' enables shell on an exposed/multi-user surface;
downgrading auto_approve_shell to require approval. Set
auto_approve_shell_acknowledge_exposed: true to keep blanket auto-approval.Configurations
Safe default (loopback DM)
Unchanged behaviour — loopback bind, no group policy, blanket auto-approve kept.
gateway:
bind_host: "127.0.0.1"
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: true # kept: loopback bind + no group_policyExposed — downgraded automatically
Externally-bound gateway on a group channel — auto_approve_shell: true silently becomes “approval required”.
gateway:
bind_host: "0.0.0.0"
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: true # silently becomes "approval required"
# WARN in gateway log:
# Channel 'slack' enables shell on an exposed/multi-user surface;
# downgrading auto_approve_shell to require approval. Set
# auto_approve_shell_acknowledge_exposed: true to keep blanket auto-approval.Exposed — acknowledged
Explicit acknowledgement keeps blanket auto-approve on an exposed surface. No WARN is emitted.
gateway:
bind_host: "0.0.0.0"
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: true
auto_approve_shell_acknowledge_exposed: true # I accept the RCE surfaceConfiguration Options
Nine per-channel fields control shell execution and approval routing.
| Field | Type | Default | Description | |
|---|---|---|---|---|
allow_shell | bool | False | Opt in shell execution for this channel. Adds execute_command to the agent’s tools, injects a permission-grant sentence into instructions, and lifts the deny-list entry. | |
auto_approve_shell | bool | True | Runs every shell call immediately (AutoApproveBackend) only on a loopback-bound gateway + non-group channel. On externally-bound gateways or any channel with a group_policy, the gateway downgrades this to the approval backend (see Exposure-aware auto-approval and auto_approve_shell_acknowledge_exposed). If False, always wires a human-in-the-loop backend via the routing ladder below. | |
auto_approve_shell_acknowledge_exposed | bool | False | Explicit acknowledgement that blanket auto-approval is safe on this externally-exposed or multi-user channel. Set to true (with auto_approve_shell: true) to keep blanket auto-approve on 0.0.0.0 binds or group channels. Off by default — the gateway fails closed to approval. | |
approval_channel | str | None | Where the approval prompt is delivered. Slack: user/channel ID. Telegram: chat ID. Discord: channel ID (falls back to home_channel). | |
approval_users | `str \ | list[str]` | None | Approver allowlist. Comma-separated string or list. Falls back to SLACK_APPROVERS / TELEGRAM_APPROVERS / DISCORD_APPROVERS env var. |
approval_mode | str | None (= "channel") | Backend selector: "channel" (default per-platform), "gateway", "http", "webhook". Validated at load time — typos throw ValueError. | |
approval_webhook_url | str | None | HTTPS URL for approval_mode=webhook. Falls back to APPROVAL_WEBHOOK_URL env var. If missing, warns and falls through to GatewayApprovalBackend. | |
approval_http_host | str | "127.0.0.1" | HTTP dashboard host for approval_mode=http. Set to "0.0.0.0" to expose. | |
approval_http_port | int | 8899 | HTTP dashboard port for approval_mode=http. |
auto_approve_shell accepts string truthy values "1", "true", "yes", "on" (case-insensitive).
Choosing an approval_mode
Pick the backend that matches where you want to answer approval prompts.
The routing precedence, exactly as wired at bot construction:
approval_mode == "gateway" → GatewayApprovalBackend()
approval_mode == "http" → HTTPApproval(host, port)
approval_mode == "webhook" OR
approval_webhook_url is set → WebhookApproval(webhook_url)
↳ webhook mode with NO url:
warn + fall through to gateway
channel_type == "slack" + resolved id → SlackApproval(...)
channel_type == "telegram" + resolved id → PresentationApprovalBackend(target=chat_id)
channel_type == "discord" + resolved id → DiscordApproval(...)
GatewayApprovalBackend importable → GatewayApprovalBackend() (fallback)
otherwise → CallbackBackend (deny shell tools)
+ warning logOn timeout, the Slack / Telegram / Discord card edits to “⌛ Timed out — not run” instead of “❌ Denied”. The auto-wired GatewayApprovalBackend() above has no notify_url, so it sends no webhook; only a backend you build yourself with notify_url also sends a follow-up {"status": "timed_out", ...} POST — see GatewayApprovalBackend webhook payload.
Fail-closed behaviour
When auto_approve_shell: false is set but no approval backend can be wired, shell commands are denied instead of silently auto-approved.
apply_bot_smart_defaults() may install an AutoApproveBackend when auto_approve_tools is on. If the shell wiring then finds no SlackApproval / PresentationApprovalBackend / DiscordApproval / GatewayApprovalBackend / HTTPApproval / WebhookApproval, leaving that backend in place would auto-approve shell despite the explicit opt-out. The gateway now swaps in a deny-by-default CallbackBackend.
If the gateway cannot determine its bind interface (host unresolvable), the exposure check also fails closed and blanket auto-approve is downgraded to require approval.
| Situation | Outcome |
|---|---|
| A backend resolves from the routing ladder | Shell approval routed to that backend. |
No backend resolves, existing AutoApproveBackend or None | Shell tools denied by a CallbackBackend; startup logs a warning. |
| Non-shell tools | Still auto-approved — the deny fence is scoped to execute_command, shell_command, and acp_execute_command. |
The warning line to watch for at startup:
allow_shell with auto_approve_shell=false needs approval_channel,
approval_mode (gateway|http|webhook), or a custom approval backend on the agent
— shell commands will be denied until one is configuredWith routing rules
Routing rules on a shell-enabled channel keep execute_command — routed agents are shell-enabled too, not just the default channel clone.
Previously a routing rule swapped in a raw agent that never saw enable_shell_tools(), so execute_command disappeared for any turn that hit a routing rule — even with allow_shell: true. The gateway now re-applies the channel’s shell setup to routed agents, and runs the full smart-defaults pipeline first.
| Behaviour | Detail |
|---|---|
| Smart defaults inherited | _apply_channel_shell calls apply_bot_smart_defaults(clone, config) before enable_shell_tools(). Routed clones now get the same memory, tool wiring, and guardrails as top-level bots — not only the shell tool. |
| Routed agents opt in too | Every routed agent on a shell-enabled channel gains execute_command, closing the silent gap. Any routing rule on that channel inherits the shell capability. |
Cloned per (channel, agent) | The routed agent is cloned once per channel-agent pair and cached, so a shared agent used by other channels never gains execute_command on channels that did not opt in. |
| Approval backend reused when compatible | A routed clone reuses the channel’s wired approval backend when the target chat matches and its approver set is the same or narrower — so routed shell approvals render on the same chat. Otherwise the clone keeps its own backend. |
| Reload drops the clones | Hot-reloading a channel to remove allow_shell: true drops the cached shell-enabled clones — the next inbound message uses the non-shell agent. |
What allow_shell changes on the agent
Setting the flag runs enable_shell_tools() on the per-channel cloned agent, after smart defaults are applied.
The five side effects, in order:
| Step | Effect |
|---|---|
| 1. Tool injection | Adds execute_command to agent.tools if not already present. |
| 2. Permission grant | Appends You can run shell commands on the bot server using the execute_command tool. to agent.instructions (idempotent). |
| 3. Deny-list lift | Removes execute_command, shell_command, and acp_execute_command from agent._perm_deny. |
| 4. Approval backend | Sets agent._approval_backend — AutoApproveBackend() when auto-approve is on, the routed platform backend when one resolves, otherwise a deny-by-default CallbackBackend that rejects shell tools (see Fail-closed behaviour). |
| 5. Approval registry sync | Mirrors the chosen backend into the global approval registry keyed by agent.name, so @require_approval tools (execute_command is decorated) honour the same policy. |
Per-platform routing
Each platform resolves the approval target from a fallback chain, then builds its native backend.
| Channel | Resolves approval_channel from | Uses backend |
|---|---|---|
| Slack | approval_channel → owner_user_id → SLACK_APPROVAL_CHANNEL | SlackApproval(token, channel, allowed_approvers) |
| Telegram | approval_channel → owner_user_id → TELEGRAM_CHAT_ID | PresentationApprovalBackend(allowed_actors, target=chat_id) — buttons render on the channel’s own bot |
| Discord | approval_channel → home_channel → DISCORD_APPROVAL_CHANNEL | DiscordApproval(token, channel_id, allowed_approvers) |
| WhatsApp / email / other | — | GatewayApprovalBackend() (fallback) |
approval_users accepts a comma-separated string or a list. An empty allowlist passes allowed_approvers=None to the platform backend, whose native default lets anyone in the channel approve.
Environment Variables
Every fallback the feature reads, in one place.
| Env var | Purpose |
|---|---|
SLACK_APPROVAL_CHANNEL | Slack user/channel ID for approval prompts (fallback for approval_channel) |
TELEGRAM_CHAT_ID | Telegram chat ID for approval prompts (fallback for approval_channel) |
DISCORD_APPROVAL_CHANNEL | Discord channel ID for approval prompts (fallback for approval_channel) |
SLACK_APPROVERS | Comma-separated Slack user IDs (fallback for approval_users) |
TELEGRAM_APPROVERS | Comma-separated Telegram user IDs (fallback for approval_users) |
DISCORD_APPROVERS | Comma-separated Discord user IDs (fallback for approval_users) |
APPROVAL_WEBHOOK_URL | HTTPS URL (fallback for approval_webhook_url) |
Recipes
Copy-paste-ready configurations for common setups.
Slack auto-approve (dev)
Fastest to demo — every shell call runs immediately on a loopback-bound gateway with a DM channel.
gateway:
bind_host: "127.0.0.1"
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: trueOn 0.0.0.0 or a channel with a group_policy, add auto_approve_shell_acknowledge_exposed: true — otherwise this downgrades to approval. See Exposure-aware auto-approval.
Slack owner-DM approval
Every command needs owner approval, and only the owner may approve.
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: false
approval_channel: "UOWNER"
approval_users: "UOWNER"WhatsApp gateway queue
WhatsApp has no native approval wiring, so route approvals into the gateway control plane.
channels:
whatsapp:
allow_shell: true
auto_approve_shell: false
approval_mode: gatewayMulti-channel policy
Different channels, different policies — all in one gateway.
gateway:
bind_host: "127.0.0.1" # loopback: Slack blanket auto-approve stays
channels:
slack:
token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
allow_shell: true
auto_approve_shell: true # kept only on loopback + non-group DM
telegram:
token: "${TELEGRAM_BOT_TOKEN}"
allow_shell: true
auto_approve_shell: false
approval_channel: "123456789" # owner chat ID
whatsapp:
allow_shell: true
auto_approve_shell: false
approval_mode: gateway # queue in control plane
discord:
token: "${DISCORD_BOT_TOKEN}"
allow_shell: true
auto_approve_shell: false
approval_mode: webhook
approval_webhook_url: "https://hooks.example.com/approve"On a 0.0.0.0 bind — or if the Slack channel carries a group_policy — the Slack line downgrades to approval unless you add auto_approve_shell_acknowledge_exposed: true. See Exposure-aware auto-approval.
HTTP dashboard approvals
Serve a built-in approval page and answer from the browser.
channels:
telegram:
token: "${TELEGRAM_BOT_TOKEN}"
allow_shell: true
auto_approve_shell: false
approval_mode: http
approval_http_host: "0.0.0.0" # default "127.0.0.1"
approval_http_port: 9000 # default 8899User Flow
A Slack workspace owner wants their support team to type @bot run pytest -q and see results in-thread, but only after they personally approve each command. They edit gateway.yaml on the server, add allow_shell: true, auto_approve_shell: false, and approval_channel: "UOWNER" to their slack: block, then restart the gateway. From that moment, every shell request from any Slack user triggers a Block Kit approval prompt in the owner’s DMs — they tap Approve and the command runs; they tap Deny and the agent replies “That command needs approval and was denied.” No Python was written.
Troubleshooting
Bot still asks for approval with auto_approve_shell: true
Upgrade to a PraisonAI build with the approval-registry sync fix (≥ 2026-07-24). On older builds, auto_approve_shell: true attached AutoApproveBackend to the agent instance only, but execute_command is decorated with @require_approval and consults the global approval registry (often with agent_name=None) — so it kept prompting. The fix mirrors the backend into the registry keyed by agent.name, and honours mark_approved() even for critical-risk tools like execute_command.
Best Practices
Never auto-approve on a public channel
Set auto_approve_shell: true only on a loopback-bound gateway with a single-operator DM. On externally-bound gateways or group channels the gateway downgrades it to approval automatically; overriding with auto_approve_shell_acknowledge_exposed: true re-opens a remote-code-execution surface — only do so on a channel you fully trust. See Exposure-aware auto-approval.
Set approval_users explicitly
Prefer an explicit approval_users allowlist over relying on SLACK_APPROVERS / TELEGRAM_APPROVERS / DISCORD_APPROVERS env vars — it keeps the policy visible in the config and avoids env-var leakage.
Use approval_mode: gateway for channels without native wiring
WhatsApp, email, and other channels have no in-channel approval UI. Route them to approval_mode: gateway so approvals queue in the control plane instead of falling silently.
Validate the YAML before starting
Run praisonai gateway lint — the fail-closed approval_mode validator rejects typos at load time, catching chanel/webook before they strand approvals.
Watch the startup log for '— shell commands will be denied'
When auto_approve_shell: false resolves no approval backend, the gateway logs a warning and denies shell tools. If your bot suddenly refuses commands with “needs approval and was denied”, that log line lists the missing knobs — add approval_channel, set approval_mode, or wire a custom backend.
Routing rules inherit shell on shell-enabled channels
Adding allow_shell: true to a channel with routing rules grants execute_command to every routed agent on that channel, not just the default one. Keep routing rules narrow on shell-enabled channels, and remove allow_shell on reload to drop the cached shell clones.
Related
Built-in, Python-registered, file-based, and entry-point commands — with the double-opt-in shell rule.
Approval ProtocolApproval backend reference for Slack, Telegram, Discord, webhook, and HTTP.
Bot GatewayGateway config, channel security, and hot-reload.
Bot Default ToolsHow tools get injected on bot agents.
Durable ApprovalsSurvive gateway restarts across in-flight approvals.