The OpenClaw control model, explained
OpenClaw is the most-starred project in the agent ecosystem, and it’s a different animal from the coding CLIs on our comparison: a personal-assistant gateway that fronts messaging channels and runs tools on a host that’s usually unattended. That shape makes its control model unusually interesting — it has controls the coding agents lack (per-sender policy, pairing, an audit command) and a default posture that assumes more trust than any of them.
This page covers OpenClaw’s own controls. For wiring ACP into OpenClaw, see the install guide.
The exec gate, and its default
Execution control is two settings in ~/.openclaw/openclaw.json:
{ "tools": { "exec": { "security": "full", "ask": "off" } } }
security is full (host exec), allowlist (patterns like "git *", "npm *"), or deny. ask is always or off. The default is full + off — every command runs on the host, nothing prompts. The docs are candid that this is intentional single-operator UX: “guardrails for operator intent, not hostile multi-tenant isolation.” Respect the candor, and read it literally: the default configuration of the ecosystem’s most-starred agent is the configuration every other harness calls its escape hatch. The hardened baseline the docs themselves recommend is security: "deny" (or allowlist) with ask: "always" — if your OpenClaw is reachable by anyone but you, start there.
Allowlist mode’s approval binding
Where OpenClaw has invested hardest — and where every harness should be taking notes — is making an approval mean what it said:
- Approvals bind the exact request context; the gateway stores a canonical run plan and rejects post-approval edits to the command or cwd.
- Approved interpreter scripts (
bun/deno runoperands) are bound to on-disk snapshots — rewrite the script after approval and the run is denied. strictInlineEvalforces approval for interpreter inline forms (python -c,node -e, …); any heredoc always requires approval; an interpreter command that can’t be resolved to exactly one local file is denied.- Approval prompts now detect invisible-Unicode obfuscation in what they display — an attack class most harnesses haven’t considered: the string the human reads and the string the shell runs can differ by characters nobody can see.
- Approval timeouts fail closed (part of a 2026 hardening wave that also moved transcript, sandbox, and MCP paths to fail-closed).
This is the deepest thinking about time-of-check/time-of-use in any harness’s approval flow. The approval choices are the familiar allow-once / allow-always / deny; allow-always accumulates standing grants that live in local config, with the usual caveat that nothing reviews what they’ve quietly become.
Tool policy by agent — and by sender
Because OpenClaw fronts messaging channels, it has a policy axis the coding CLIs don’t need: who is asking. Global and per-agent tools.allow/tools.deny, tool profiles (messaging, minimal), tools.toolsBySender, and an elevated exec gate with its own allow-from list. One rule is hard-coded: non-owner senders never get the cron or gateway tools — the harness understands that scheduling and self-reconfiguration are the dangerous verbs, a lesson we’ve argued from the policy side. Unknown DMs go through explicit pairing approval. This is real identity-aware policy — scoped to message senders; tool calls themselves still aren’t attributed to an identity the way a workspace ledger would.
The sandbox
Present, capable, off by default: agents.defaults.sandbox.mode: "off" | "all", per-agent or per-session scope, workspaceAccess: "none" | "ro" | "rw" (default none), docker bind mounts validated against a denylist (with a dangerouslyAllowExternalBindSources override that earns its name). Sandbox network defaults to none; elevated exec bypasses sandboxing; host=sandbox fails closed when no runtime exists. Subagents can be forced to sandbox with sessions_spawn sandbox: "require" (default inherit).
The hook surface
Two systems, easy to confuse, only one of which can control tools:
- Plugin hooks (in-process TypeScript, registered from a plugin entry):
before_tool_call/after_tool_callplus agent, message, and gateway lifecycle events.before_tool_callcan return{ block: true }— terminal, stops lower-priority handlers — or arequireApprovalresult whose resolutions are the typed unionallow-once | allow-always | deny | timeout | cancelled. Typed, composable, and it can drive the native approval flow: a genuinely good interception point. Two catches from the changelog: only the typed hook runner dispatches these events (the legacyregisterHookpath silently doesn’t), andbefore_tool_callexisted for a while before it was actually wired into the execution pipeline — verify your hook fires with a test call after upgrades, not by reading the docs. - Internal hooks (operator shell scripts): command and gateway lifecycle only — no tool events (the request for
tool:before/tool:afteris an open issue). If someone tells you they’ve “added a hook” to control OpenClaw tools, ask which system they mean.
Plugin installation itself is gated (plugins.allow, security.installPolicy) — the harness treats its own extension mechanism as a control surface, which is more than most do.
The audit command
openclaw security audit (with --deep and --fix) runs named checks against your live config — flagging, among others, exactly the permissive defaults above (tools.exec.security_full_configured, unauthenticated gateway binds). A harness that ships a tool to critique its own configuration is rare and commendable. Note what it audits: the configuration, not the history — session transcripts (JSONL per agent, always-on redaction) remain the run’s own account of what happened, and nothing records decisions off-machine.
Where the native model ends
OpenClaw’s control surface is broad and visibly battle-tested — approval binding and sender-axis policy are ahead of the coding CLIs — but the shape of the gaps is the ecosystem’s standard four, sharpened by the fact that OpenClaw’s whole job is to run unattended:
- The default posture and the deployment model point in opposite directions. An always-on, message-reachable agent is the empty-chair case par excellence, and the default config asks nothing and denies nothing.
- Policy is one host’s JSON. Everything above lives in
openclaw.jsonon the machine — no fleet view, no reconciliation, and the config is writable by whatever can write files there. - The record is self-authored. Transcripts plus a config auditor, but no independent per-decision ledger, and standing
allow-alwaysgrants accumulate outside any review surface.
The pairing that works: adopt the hardened baseline, keep the approval binding — it’s the best in the business — and put workspace policy, decision recording, and standing-grant review in a layer off the host. Our plugin rides before_tool_call: workspace rules return block or requireApproval through OpenClaw’s own typed flow, and every decision — including every allow-always a human grants — lands in a ledger you can actually read next quarter.