Plugin Permission Requests: User Approval for Tool Calls
Learn how plugin code can pause tool calls or operations until users approve. This guide is for developers integrating permission prompts with the Gateway approval flow.
Read this when
- You need a plugin hook or tool to ask before a side effect runs
- You need to configure where plugin approval prompts are delivered
- You are deciding between optional tools, exec approvals, and plugin approvals
Plugin permission requests allow plugin code to halt a tool call or a
plugin-owned operation until a user grants or denies it. This mechanism relies
on the Gateway plugin.approval.* flow and the same approval UI surfaces that
manage chat approval buttons and /approve commands.
These requests are meant for plugin or app permissions. They are not a substitute for host exec approvals, optional tool allowlists, or Codex's native permission review.
Choose the right gate
Choose the gate that aligns with the decision point you are addressing:
| Gate | Use it when | What it controls |
|---|---|---|
| Optional tools | A tool should not be visible to the model until the user opts in. | Tool exposure through tools.allow. |
| Plugin permission requests | A plugin hook or plugin-owned operation must ask before one action runs. | Runtime approval through plugin.approval.*. |
| Exec approvals | A host command or shell-like tool needs operator approval. | Host exec policy and durable exec allowlists. |
| Codex native permission requests | Codex asks before native shell, file, MCP, or app-server actions. | Codex app-server or native hook approval handling, routed through plugin approvals when OpenClaw owns the prompt. |
| MCP approval elicitations | A Codex MCP server requests approval for a tool call. | MCP approval responses bridged through OpenClaw plugin approvals. |
Optional tools act as a discovery-time gate, while plugin permission requests operate per call. When a sensitive tool demands explicit opt-in before the model sees it and approval before execution, apply both gates.
Request approval before a tool call
Most plugin-authored prompts should originate in a before_tool_call hook. This
hook executes after the model picks a tool but before OpenClaw runs it:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "deploy-policy",
name: "Deploy Policy",
register(api) {
api.on("before_tool_call", async (event) => {
if (event.toolName !== "deploy_service") {
return;
}
const environment =
typeof event.params.environment === "string" ? event.params.environment : "unknown";
return {
requireApproval: {
title: "Deploy service",
description: `Deploy service to ${environment}.`,
severity: environment === "production" ? "critical" : "warning",
allowedDecisions:
environment === "production"
? ["allow-once", "deny"]
: ["allow-once", "allow-always", "deny"],
timeoutMs: 120_000,
onResolution(decision) {
console.log(`deploy approval resolved: ${decision}`);
},
},
};
});
},
});
Write prompt text aimed at the person approving the action:
- Keep
titlebrief and action-oriented; the Gateway limits it to 80 characters. - Keep
descriptionprecise and constrained; the Gateway limits it to 512 characters. - Mention the action, target, and risk. Exclude secrets, tokens, or private payloads that must not surface in chat approval interfaces.
severityfalls back to"warning"when not provided. Reserve"critical"for actions where a wrong choice could lead to production damage or data loss.allowedDecisionsfalls back to["allow-once", "allow-always", "deny"]when not provided. Supply["allow-once", "deny"]when persistent trust is not safe for that action.timeoutMsdefaults to 120000 (2 minutes) and is capped at 600000 (10 minutes) no matter the requested value.
Decision behavior
OpenClaw generates a pending approval with a plugin: ID, sends it to
the available approval surfaces, and awaits a decision.
| Decision | Result |
|---|---|
allow-once | The current call proceeds. |
allow-always | The current call proceeds and the decision is forwarded to the plugin. |
deny | The call is blocked with a denied tool result. |
| Timeout | The call is blocked. |
| Cancellation | The call is blocked when the run is aborted. |
| No approval route | The call is blocked because no connected approval surface can resolve it. |
Execution is permitted only for the exact allow-once and allow-always
decisions the request allows. Unknown, malformed, mismatched, missing, and
timed-out decisions fail closed. The legacy timeoutBehavior field is still
accepted for plugin compatibility but is deprecated and ignored; avoid setting
it in new hooks.
allow-always is durable only when the requesting plugin or runtime handles
that persistence. For standard before_tool_call.requireApproval hooks, OpenClaw treats
allow-once and allow-always as approval decisions for the current call and
passes the resolved value to onResolution. If your plugin offers
allow-always, document and implement exactly what future calls it trusts.
When the hook also returns params, OpenClaw snapshots the base
parameters and those overrides at approval request time, then applies the
overrides only after approval succeeds. A lower-priority hook can still block,
but cannot modify the parameters covered by the pending approval.
allowedDecisions restricts the buttons and commands shown to the user. The
Gateway rejects a resolve attempt for any decision the request did not offer.
Route approval prompts
Approval prompts can resolve in local UI surfaces or in chat channels that
support approval handling. To forward plugin approval prompts to explicit chat
targets, configure approvals.plugin:
{
approvals: {
plugin: {
enabled: true,
mode: "targets",
agentFilter: ["main"],
targets: [{ channel: "slack", to: "U12345678" }],
},
},
}
approvals.plugin operates independently from approvals.exec. Enabling exec
approval forwarding does not route plugin approval prompts, and enabling plugin
approval forwarding does not alter host exec policy.
When a prompt includes manual approval text, resolve it with one of the offered decisions:
/approve <id> allow-once
/approve <id> allow-always
/approve <id> deny
For the complete forwarding model, same-chat approval behavior, native channel delivery, and channel-specific approver rules, see Advanced exec approvals.
Codex native permissions
Codex native permission prompts can also pass through plugin approvals, but their ownership differs from plugin-authored hooks.
- Codex app-server approval requests route through OpenClaw after Codex review.
- The native hook
permission_requestrelay can ask throughplugin.approval.requestwhen that relay is enabled. - MCP tool approval elicitations route through plugin approvals when Codex marks
_meta.codex_approval_kindas"mcp_tool_call".
For Codex-specific behavior and fallback rules, see Codex harness runtime.
Troubleshooting
The tool says plugin approvals are unavailable. No approval UI or configured
approval route accepted the request. Connect an approval-capable client, use a
channel that supports same-chat /approve, or configure approvals.plugin.
allow-always appears but the next call prompts again. The generic plugin
approval flow does not automatically persist trust for arbitrary hooks. Persist
plugin-owned trust in your plugin after onResolution("allow-always"), or
offer only allow-once and deny.
/approve rejects the decision. The request restricted
allowedDecisions. Use one of the decisions printed in the prompt.
A Discord, Matrix, Slack, or Telegram prompt routes differently from exec
approvals. Plugin approvals and exec approvals use separate config and may use
different authorization checks. Verify approvals.plugin and the channel's
plugin approval support instead of only checking approvals.exec.