AI Harness Engineering · OpenClaw 10 of 11 · Anatomy of an Agent Gateway

Tools, Exec-Approvals & Sandboxing — The Hard Fence

Lesson 3 said it plainly: prompt-injection guardrails are soft; the real fence is mechanical. This lesson builds that fence — the tool policy, the exec-approval ladder, the allowlist, and the sandbox: the layers that decide whether the agent's request to run a command actually executes.

Prerequisites: Lessons 1–9 (especially Lesson 3's hard-vs-soft enforcement). You know what a shell command and a glob are. A little Python for the labs.
9
Chapters
2
Simulations
3
Code Labs

Chapter 0: The Agent's Hands

The whole lesson in one sentence. An agent that can only talk is harmless; an agent with tools can act in the world — and the same system.run that lets it grep your codebase lets it delete your home directory. So OpenClaw stacks a mechanical fence — tool policy, an exec-approval ladder, an allowlist, and a sandbox — that decides, on every single request, whether the command actually executes.

Up to now the agent has been mostly a mouth: it reads messages, thinks, and writes replies. This lesson is about its hands. A tool is any ability the agent can invoke to change or sense the world outside the conversation: run a shell command (exec), drive a browser, search the web, read and write files. Tools are what turn a chatbot into an agent. They are also exactly where it can hurt you.

Hold the two halves of that together, because the whole lesson lives in the tension. The power of a tool and its risk are the same thing seen from two sides. A shell that can run rg to search your project can also run rm -rf ~ to erase it. A browser that can read a docs page can also read your logged-in bank session. You cannot have the useful half without owning the dangerous half — so you put a fence around the dangerous half.

Soft fence vs. hard fence. Lesson 3 drew the line we build on here. You can ask the model nicely — "please don't run destructive commands" — but that is a soft guardrail: a prompt-injected web page or a jailbreak can talk the model out of it. The model's good intentions are not a security boundary. The hard fence is mechanical: code that sits between the agent's request and the host, and refuses to forward a command the policy doesn't allow — no matter how convincingly the model asks. This lesson is that hard fence.

OpenClaw builds the fence in three enforcement layers, and naming them now gives you the map for the whole lesson. First, a tool policy decides whether a tool is even allowed (an allow/deny list). Second, for the riskiest tool — exec — an exec-approval ladder decides whether a command runs automatically, prompts a human, or is blocked outright. Third, a sandbox isolates whatever does run, so even an allowed command can only touch a fenced-off corner of the machine. Policy, then approval, then sandbox. Each chapter fills one in.

Keep one frame in mind throughout: every layer here is designed to fail safe. When something is ambiguous — no UI is reachable, the command doesn't match, the config and the host disagree — the answer defaults to "deny," not "allow." A fence that opens when it's confused is not a fence.

Lesson 3 distinguished a soft guardrail from a hard fence. Why is "tell the model not to run destructive commands" not a real security boundary?

Chapter 1: The Exec-Policy Ladder

Of all the tools, exec — running a shell command on the host — is the sharpest. So OpenClaw gives it a dedicated dial: a security mode, set at tools.exec.security, that picks one of five behaviors. Think of the five as a ladder from strictest to loosest. As you climb, the agent gets more freedom and you get less protection. Each rung is a deliberate point on that trade.

The five rungs, strictest to loosest.
  • deny — block all host execution. The agent cannot run a command at all, full stop. The fence is welded shut.
  • allowlist — run only pre-approved commands, with no prompt. A command that matches the allowlist runs silently; anything else is denied silently. No human in the loop.
  • ask — an allowlist, but on a miss it prompts a human instead of denying. Matches run; non-matches pause and wait for a person to approve or reject.
  • auto — deterministic matches run; misses go through an automatic reviewer and then, if still unsure, a human. The "smart middle" rung.
  • full — unrestricted. Every command runs, bypassing the gates entirely. This is the dangerous top rung.

Notice the shape: at the bottom (deny) nothing runs; at the top (full) everything runs; the three rungs in between are where the interesting work happens, because they treat a matched command and an unmatched command differently. The allowlist is the hinge the whole ladder turns on — we devote the next chapter to it.

The gateway enforces the stricter of two votes. The mode isn't decided in one place. The config proposes a mode (say, auto), but the host — the node that would actually run the command — carries its own default mode, and the gateway enforces whichever of the two is stricter. If your config says full but the host's default is allowlist, the effective mode is allowlist. You can always tighten the fence from either side; you can never loosen it past what the host permits. Two locks, and the stronger one wins.

Below, the ladder made visible. A command sits on the left; the five modes are rows on the right. Flip the command between allowlisted and not, and watch each rung light up its outcome — green for run, amber for prompt-a-human, red for deny. The point to feel: deny and full ignore the allowlist entirely (they're flat), while allowlist, ask, and auto all hinge on whether the command matched.

The exec-policy ladder — five modes, one command

Each row is a security mode. The pill shows what that mode does with the current command: run, prompt, or deny. Toggle whether the command is on the allowlist and watch the middle three rungs flip while deny and full stay put.

The command rg pattern is currently allowlisted.

Let's build the ladder in code — the single function that, given a mode and whether the command matched, returns one of run, prompt, or deny. It is small, but the shipped version has the exact bug that ruins fences: it rubber-stamps everything.

Your config sets tools.exec.security to full, but the host node's default mode is allowlist. What mode does the gateway actually enforce?

Chapter 2: The Allowlist — What "Approved" Actually Means

The ladder hinges on one question: is this command on the allowlist? In Chapter 1 we faked the answer with "is the first word in a list." Real life is far more slippery, because a command is not a single word and an attacker will exploit every gap. So OpenClaw's allowlist is precise about what "matches" means. Five rules, each closing a specific hole.

The five matching rules.
  • Glob patterns. An entry can be a literal name (rg), an absolute path (/opt/homebrew/bin/rg), or a glob (~/Projects/**/bin/x). The command's binary must glob-match an entry.
  • A bare name matches only PATH-invoked binaries. The entry rg matches when you type rg (resolved via your PATH), but not when you run ./rg or /tmp/rg — a relative or absolute path is a different thing and must be matched by a path entry. This stops an attacker dropping a malicious rg in a local folder.
  • An optional argPattern restricts arguments. An entry can carry a regex the joined arguments must match. {pattern: 'python3', argPattern: '^safe\\.py$'} allows python3 safe.py but denies python3 other.py. The binary alone is not enough.
  • A shell chain requires every segment to pass. A command like a && b | c is three commands glued together. Each segment must independently satisfy the allowlist; one bad segment denies the whole chain. rg foo && rm -rf / is denied because rm isn't approved — even though rg foo is.
  • strictInlineEval blocks inline code. Even when an interpreter is allowlisted, this flag denies its inline-eval forms — python -c, node -e, osascript -e — because inline code is an open door: python3 -c "..." can run anything. You allowlisted the interpreter to run scripts you trust, not arbitrary inline programs.

Sit with why each rule exists, because each is a patched attack. Globs without the bare-name rule would let ./rg impersonate the real rg. An interpreter without argPattern would let python3 anything.py through once python3 was approved for one safe script. A chain without the all-segments rule would let an attacker smuggle rm -rf / behind a benign rg. And strictInlineEval exists because python3 -c turns "I approved python" into "I approved arbitrary code." The allowlist is a list of scars.

Let's implement the matcher itself — the function that takes a full command string and decides, segment by segment, whether the allowlist permits it. Getting the chain and inline-eval rules exactly right is the difference between a fence and a sieve.

The interpreter python3 is on the allowlist. By the allowlist rules, what happens to python3 -c "import os; os.system('rm -rf ~')" when strictInlineEval is on?

Chapter 3: The Approval Flow — Pinning the Decision

In ask and auto modes, a command that misses the allowlist doesn't die — it pauses and asks a human. That pause is a small protocol, and it has one subtle property that makes it actually safe rather than theater. Let's walk it.

When a miss happens, the gateway broadcasts an exec.approval.requested event to every connected operator client — your desktop app, the CLI, the web UI. The event carries what's being asked: the exact command, the working directory, who's asking. A human sees it and clicks. The UI sends back an exec.approval.resolve with their verdict. If approved, the gateway forwards the run to the node host that will execute it. If denied, that's the end — denied approvals are terminal; the command does not run, and there is no retry of the same request.

The canonical binding — no bait-and-switch. Here is the rule that turns this from a UI nicety into a security mechanism. When a human approves, they are not approving "the agent's next command in general." The approval is pinned to one concrete tuple: this exact command, this working directory, this agent id, this session key. If any of those four changes between the approval and the forwarded run — the command string is altered by a byte, the cwd swaps, a different agent or session tries to ride the approval — the forwarded run is rejected. You approved that command in that place; nothing else can wear the approval like a coat.

Why does this matter so much? Without canonical binding, a clever attack is trivial: get the human to approve a harmless-looking ls, then swap in rm -rf ~ before the run is forwarded — same approval, different command. By pinning the approval to the exact command, cwd, agent, and session, OpenClaw makes the approval non-transferable. The thing the human saw and the thing that runs are guaranteed identical, or nothing runs. Consent is for one specific act, not a blank check.

The flow, end to end. miss in ask/auto → gateway broadcasts exec.approval.requested (command, cwd, agent, session) → operator UI shows it → human resolves with exec.approval.resolveif approved AND the canonical tuple still matches, forward to the host → run. If denied, terminal — no run, no retry. If the tuple changed, reject the forward even though approval was granted.
Allowlist miss
ask / auto mode — command not pre-approved
exec.approval.requested
broadcast to operators: command, cwd, agent, session
human resolves
exec.approval.resolve — approve or deny
↓ approve + tuple matches
forward to host
the pinned command runs — nothing else can

So the approval flow is not "are you sure?" It is a cryptographically-careful handshake about one specific command in one specific place. The pin is the whole point. We'll see the same instinct — consent attached to an exact request, not a category — echo through the precedence rules in Chapter 6.

A human approves an exec.approval.requested for ls /tmp. Before the run is forwarded, the command is changed to rm -rf ~. What happens?

Chapter 4: Ask Policy & the "YOLO" Lock

The ask mode has its own little dial — when to ask — set at tools.exec.ask. It takes three values, and the spread covers the natural range of paranoia.

The three ask settings.
  • off — never prompt. (Combined with a permissive mode, this is the road to "YOLO" — see below.)
  • on-miss — prompt only when the command misses the allowlist. Matches run silently; the unknown gets a human. This is the sensible default for ask.
  • always — prompt on every command, even allowlisted ones. Maximum caution; a human signs off on each run.

Now the hard question: what happens when a prompt is required but no human is reachable? Your phone is asleep, the desktop app is closed, the UI is down. The system can't just hang forever, and it certainly can't quietly run the command. That decision is tools.exec.askFallback, and its default is deny. If there's no one to ask, the safe answer is no. Fail closed, every time.

"YOLO" requires three switches, on purpose. "YOLO mode" — the agent runs anything with no approvals — is genuinely useful sometimes (a trusted, throwaway sandbox). But it is also exactly the configuration that can ruin your day, so OpenClaw makes it impossible to fall into by flipping one switch. To get true no-approvals execution you must set all three: tools.exec.security = full AND tools.exec.ask = off AND the host's askFallback = full. Miss any one and the fence reasserts itself — a stricter security mode gates the command, or ask stops to prompt, or askFallback denies when no one answers. Three independent locks, all of which must be open. You cannot YOLO by accident.

This is a recurring design principle worth naming: dangerous states should require multiple, independent confirmations. A single boolean that disables all safety is a single point of catastrophe — one bad config push, one fat-fingered toggle, and you're wide open. Spreading the requirement across three knobs in three layers means an accident in one place is caught by the other two. The fence has defense in depth even against you.

Think of askFallback as the answer to "the doorbell rang and nobody's home." A safe house doesn't open the door to whoever rings when the owners are out; it stays locked. askFallback: deny is that locked door. Setting it to full — "let anyone in when we're out" — is one of the three switches YOLO needs, and it's the one that should make you most nervous.

You set tools.exec.security = full and tools.exec.ask = off, but leave the host's askFallback at its default. Have you enabled "YOLO" (no-approvals) execution?

Chapter 5: Sandboxing — Shrinking the Blast Radius

The ladder and the allowlist decide whether a command runs. The sandbox decides where it runs — it isolates tool execution so that even an allowed command can only touch a fenced-off slice of the machine. The word to hold onto is blast radius: if something does go wrong, how much can it reach? The sandbox shrinks that radius.

Three knobs define the sandbox.
  • modewhether to sandbox: off (run tools on the host directly), non-main (sandbox only non-main sessions — the default), or all (sandbox everything).
  • scopehow containers are allocated: agent (one container per agent), session (one per conversation), or shared (one shared by all). Tighter scope = stronger isolation between agents/sessions.
  • workspaceAccesswhat host files the sandbox can see: none (no host files at all), ro (a read-only mount), or rw (read-write). This is the dial between "the agent can read my project" and "the agent can change my project."

The default — non-main — is a thoughtful compromise. Your main session is your trusted foreground chat, where you're watching every step, so it runs on the host (full speed, full access). But non-main sessions — background jobs, spawned subagents, scheduled tasks running while you're not looking — are exactly the ones you can't supervise, so they get boxed up. Sandbox the unattended; trust the watched.

The sandbox boxes the tools, not the gateway. A crucial scoping fact: only tool execution is sandboxed. The gateway process itself — the daemon owning your provider connections, your sessions, your config — always runs on the host, never inside a container. The sandbox is a fence around the agent's hands (the commands it runs), not around the agent's brain (the gateway). Don't confuse "the agent's command runs in a box" with "the whole system runs in a box" — it's only the former.

Let's build the precedence-aware decision that ties tool policy and sandbox together — the function that, for a given tool request, decides whether it's allowed, whether it's sandboxed, and the write-gate that workspaceAccess: ro enforces. This is also where Chapter 6's headline rule first appears in code.

Sandbox mode is non-main (the default). A background subagent session and your foreground main session both run a command. Which one is sandboxed?

Chapter 6: Precedence — "Why Was This Blocked?"

You now have three mechanisms — tool policy, exec approval, sandbox — and the natural next question, the one you'll actually ask at 2 a.m. when something doesn't run, is: which one stopped it? The answer is a strict order of evaluation, and getting that order right is what makes the fence reasonable to debug.

The precedence rule. Tool allow/deny policy is evaluated before sandbox rules. A tool that the policy denies stays denied no matter what the sandbox says — you cannot "sandbox your way" into running a denied tool, and you cannot un-deny it by changing the sandbox mode. The mental model is a fixed pipeline: tool policy → approval → sandbox. A request must clear each gate in turn; the first gate that says no is the answer.

This ordering isn't arbitrary — it's the only one that's safe. If the sandbox were checked first, you could imagine a denied tool slipping through because "it's sandboxed, so it's fine." But a denied tool is denied because you never want it to run at all, sandboxed or not. Policy is a statement of intent ("this tool is off-limits"); the sandbox is a statement of containment ("if it runs, box it"). Intent comes first. You decide whether before you decide where.

The one escape hatch: tools.elevated. There is exactly one way for an authorized request to escape the sandbox: a tool listed in tools.elevated, invoked by an authorized sender, runs on the host even when the sandbox would otherwise box it. This is a deliberate, narrow hole for trusted system-administration tools that genuinely need host access. But note where it sits in the pipeline: elevated escapes the sandbox gate — it does not escape the tool policy gate. A tool that is both elevated and in tool_deny is still denied. Escaping containment is not the same as escaping the decision to forbid.

So the full picture is a funnel with one labeled side door. A request enters at tool policy: if denied, it dies here, full stop — even elevated can't save it. If allowed, it reaches approval: in ask/auto, a miss waits for a canonically-pinned human yes. If approved, it reaches the sandbox: boxed unless the mode says otherwise or it's elevated. Three gates, one order, one side door — and now "why was this blocked?" has a deterministic answer you can trace.

A tool is listed in BOTH tools.elevated (escapes the sandbox) and the tool deny policy. What happens when the agent invokes it?

Chapter 7: Browser & Web Search — Capability That Reaches Outward

We've spent the lesson on exec because it's the sharpest tool, but the fence is for all capability that reaches into the world — and two more tools deserve a careful look, because their danger is quieter than a shell command's.

First, browser automation. When the agent drives a browser, it isn't using a fresh, anonymous tab — it can act inside a logged-in profile. That profile may hold your authenticated bank session, your email, your cloud console. So a browser tool isn't "read a public web page"; it's potentially "act as you, everywhere you're signed in." The right mental model: treat the browser profile as a secret, as sensitive as a password file. An agent that can drive your logged-in browser can do anything you can do in it — and a prompt-injected page might try to make it.

The browser profile is logged-in state. The thing that makes browser automation powerful — reusing your sessions so the agent doesn't have to log in — is exactly what makes it dangerous. Cookies and saved logins are bearer tokens: whoever holds them is you to those sites. So the same fence logic applies: who can invoke the browser tool, on which sessions, with what supervision. Capability that can act as you needs at least the care you'd give a command that can delete your files.

Second, web search. It looks innocuous — just fetching results — but it spans many providers, each an external service you're handing a query to, and each a channel through which untrusted text flows back into the agent. That returned text is a classic prompt-injection vector (Lesson 3's territory): a search result can contain instructions aimed at your agent. So web search is sensitive twice over: outbound (your queries leave your machine to third parties) and inbound (untrusted content returns and may try to steer the agent).

The unifying principle. Any tool whose effect reaches outward — runs a command, drives a logged-in browser, queries an external provider, touches the network — gets the same fence: a policy that decides whether it's allowed, approval where a human should weigh in, and isolation to bound the damage. exec is just the loudest example. The discipline is uniform: power that touches the world is gated, period.

This is why the three-layer fence is framed around tools in general, not just exec. The ladder and allowlist are exec-specific because exec needs the finest control, but tool policy (allow/deny), the sandbox, and the precedence order apply to every tool. Browser and search are the reminder that the agent's reach is wider than a shell — and the fence has to be just as wide.

Why is browser automation treated as sensitive, and the browser profile as a secret?

Chapter 8: Showcase & Connections

Here is the whole fence in one diagram. A command request enters on the left and flows through three gates — tool policy → exec approval → sandbox — and stops at the first one that says no. Flip the knobs and watch where it stops and why: change the exec mode, toggle whether the command hits the allowlist, deny the tool outright, set the sandbox mode, and switch the workspace to read-only. The precedence rule is highlighted as you go: a tool-policy deny beats everything, and elevated escapes only the sandbox.

The hard fence — trace a command through all three gates

The request flows left to right through tool policy, exec approval, and sandbox. A gate glows pass, prompt, or stop. Toggle the knobs and watch the verdict and the reason update.

exec modeask
Set the knobs and read the verdict.
The fence in one breath. Tools are the agent's hands; power and risk are one thing (Ch 0). The exec ladder has five rungs — deny, allowlist, ask, auto, full — and the gateway enforces the stricter of config and host (Ch 1). The allowlist means globs, bare-name-equals-PATH, argPattern, all-segments-of-a-chain, and strictInlineEval (Ch 2). The approval flow is canonically pinned to one command/cwd/agent/session (Ch 3). YOLO needs all three of security-full, ask-off, askFallback-full (Ch 4). The sandbox has mode/scope/workspaceAccess and boxes only tool execution, not the gateway (Ch 5). Precedence: tool policy before sandbox; denied stays denied; elevated escapes the sandbox but not policy (Ch 6). And every outward-reaching tool — browser, search — gets the same fence (Ch 7).

Where this fits in the series

Related lessons on this site

The Feynman test. Close the lesson and explain to a friend: why a soft guardrail isn't a security boundary; the five rungs of the exec ladder and which two ignore the allowlist; what "approved" precisely means (globs, bare-name, argPattern, all-chain-segments, strictInlineEval); why the approval is pinned to an exact command; why YOLO needs three switches; what the sandbox boxes and what it never boxes; and the precedence rule — tool policy before sandbox, denied stays denied, elevated escapes only the sandbox. If you can, you own the hard fence, and you can reason about why any command was blocked.
In one sentence, what IS the "hard fence" this lesson built?