Gate Your Agent's Tools
James stared at the tool profiles page in the dashboard. Lesson 3 had shown him the binary gate: coding gives the agent shell access, messaging takes it away. In Lesson 12, two agents shared work through orchestration. The booking agent needed the calling tool to confirm appointments. But binary access felt incomplete.
He thought about when his old company rolled out a new purchase order system. For the first week, every employee with procurement access could authorize purchases directly. An intern placed a $40,000 equipment order by accident, clicking through the approval screen without realizing the amount. After that, every PO above $500 required a manager's signature. Nobody lost access to the ordering system. The company added a sign-off step. The door stayed open; they installed a doorbell.
Tool profiles were the door lock: open or closed. What James needed was the doorbell.
Emma pulled up a table:
| Tier | Mechanism | Enforcement Level | What It Prevents |
|---|---|---|---|
| 1 | Tool profiles (coding/messaging/minimal) | In-process, same Node.js runtime | Agent accessing unauthorized tools |
| 2 | Plugin hooks with requireApproval | In-process, plugin intercepts tool call before execution | Sensitive operations without operator approval |
| 3 | NemoClaw sandboxing | Out-of-process, kernel-level (Landlock, seccomp, netns) | API key exfiltration, network escape |
"Tool profiles are Tier 1. Binary: the tool is allowed or it is denied. But what about operations where you want the tool to exist, you just want a human to approve each use?"
James thought about it. "Like a booking agent that needs the calling tool, but should not call a customer without someone checking first."
"Exactly. Tier 2. Let's build that gate."
James opened a new file in his editor. "TypeScript plugin. I saw the plugin SDK docs. Three files: package.json, manifest, and the entry point with definePluginEntry." He started typing an import statement.
"Wait." Emma closed the laptop lid. "What is the skill here?"
"Writing a TypeScript plugin."
"You have been using this agent for twelve lessons. It writes code. It reads documentation. It follows constraints. You have a running AI Employee that can build software." She paused. "What is the actual skill?"
James stared at the closed laptop. At his old company, after the purchase order incident, he did not personally code the approval workflow into the procurement system. He wrote the policy: POs above $500 require manager sign-off, timeout after 48 hours means denied, applies to all departments except emergency procurement. The IT team implemented it. His job was getting the constraints right.
"Telling it what to build," he said. "Precisely enough that it does not get it wrong."
"And what happens if you get a constraint wrong in an OpenClaw plugin?"
"It breaks?"
"Worse. It compiles, loads, and does nothing. Silent failure. No error message." Emma opened the laptop again. "There are six constraints that cause silent failures in plugins. If you hand-write the code, you will hit each one. If you specify the constraints and hand them to your agent with a reference, it builds the plugin correctly on the first attempt."
James looked at the blank editor. Then at his WhatsApp thread with the agent. Twelve lessons of telling it what to do. "So I write the constraints, not the code."
"You write the constraints. The agent writes the code. You test the result."
You are doing exactly what James is doing: you need a gate between "tool allowed" and "tool runs." Tool profiles (Tier 1 from Lesson 3) are binary: allow or deny. Now you build Tier 2: a custom gate that intercepts tool calls and asks a human operator to approve or deny before the tool runs. Tier 3 (NemoClaw sandboxing) comes in Lesson 15.
You will not write the plugin code. You will send a prompt that tells your AI Employee to build it. The prompt includes a link to this page so your agent reads the technical constraints and reference code it needs. OpenClaw plugins fail silently when built incorrectly: the code compiles, the plugin loads, and nothing happens. The link prevents that.
Time budget: 35 minutes. 5 to craft the prompt, 10 for the agent to build and register, 10 for E2E testing on WhatsApp, 10 for exploration.
Send the Prompt
Copy the URL of this page from your browser. Send this prompt to Claude Code or your AI Employee on WhatsApp:
Build me an OpenClaw plugin called "my-approval-gate" that requires
my approval on WhatsApp before any shell command runs.
Read the technical reference at the bottom of this page before building:
[paste this page's URL]
After creating the plugin files in ~/.openclaw/plugins/my-approval-gate/,
register the plugin:
- Set plugins.load.paths to include ~/.openclaw/plugins
- Enable plugins.entries.my-approval-gate
- Add approvals.plugin config with enabled: true and mode: "session"
- Restart the gateway
- Run openclaw plugins list --verbose to confirm it loaded
Your agent reads the page, finds the constraints and reference code in the Technical Reference section at the bottom, builds the plugin, registers it, and verifies the load. You did not write TypeScript. You wrote a specification with a link to the right constraints.
OpenClaw plugins fail silently when built incorrectly. The code compiles, the plugin loads, and nothing happens. No error message. The link gives your agent the six platform constraints and working reference code it needs to build the plugin correctly on the first attempt.
Verify the Plugin Loaded
After your agent finishes, check the dashboard or run:
openclaw plugins list --verbose
Your plugin should appear as loaded and enabled. If it appears as loaded but disabled, your agent missed the plugins.entries config. If it does not appear at all, the plugins.load.paths config does not include the right directory. Send a follow-up message to your agent describing what you see.
Test the Approval Flow
With the plugin loaded, trigger it. Send a message on WhatsApp:
Run ls in bash
Here is what happens:
- The agent decides to call the
exectool - Your plugin's
before_tool_callhook fires - The hook returns
requireApproval - The gateway sends the approval prompt to you on WhatsApp
- You see:
Shell Command Approval Required
Description: Command: ls -F
Tool: exec | Plugin: my-approval-gate | Agent: main
ID: plugin:83b5035e-faa4-495b-8ed3-8da725a8a327
Expires in: 120s
Reply with: /approve <id> allow-once|allow-always|deny
Three decisions:
| Decision | Effect |
|---|---|
allow-once | This call runs. Future calls still require approval. |
allow-always | This and all future calls from this plugin run without asking. |
deny | This call is blocked. The agent receives a denial. |
Respond:
/approve plugin:83b5035e-faa4-495b-8ed3-8da725a8a327 allow-once
The tool runs. The agent returns the output. The entire flow happened through WhatsApp: agent requested a tool, you approved on your phone, tool executed.
If you do not respond within 120 seconds, timeoutBehavior: "deny" blocks the call automatically. Fail closed.
If your agent is connected to Slack, the same requireApproval object routes approval prompts to designated Slack approvers. Your plugin code works across channels without modification.
When It Does Not Work
If the approval prompt never appears, check in this order:
openclaw plugins list --verboseshows your plugin loaded and enabled- Gateway log shows your plugin's
console.logline:tail -f ~/.openclaw/logs/gateway.log - The log shows
tool call: exec(notbash) - If the log shows
tool call: execbut no approval prompt appears, the approval routing config is missing. Ask your agent: "Check whether openclaw.json has approvals.plugin with enabled: true and mode: session. Add it if missing." - If the log shows nothing from your plugin, the hook registration failed. Ask your agent: "Check whether the plugin uses api.on() or api.registerHook(). It must use api.on()."
Send your agent the specific symptom you see. It has the constraints URL. It can diagnose and fix.
Try With AI
Exercise 1: Test All Three Decisions
Trigger the approval prompt three times. Respond with allow-once, then deny, then wait for the timeout (2 minutes).
For allow-once: verify the command output appears in chat.
For deny: verify the agent receives a denial message.
For timeout: verify the call is blocked without your input.
What you are learning: The three approval decisions and the fail-closed timeout behavior. deny and timeout produce the same result: the tool does not run.
Exercise 2: Gate a Different Tool
Send your agent a follow-up prompt:
Modify the approval gate to also require approval for file_write operations.
Use severity "critical" for file_write (it is more dangerous than exec).
Keep the exec gate at severity "warning".
After the agent modifies the plugin, test by asking: "Write 'hello' to /tmp/test.txt" and verify the approval prompt appears with the critical severity icon.
What you are learning: Extending a plugin specification through conversational refinement. You did not read the TypeScript to modify it. You described the change, your agent applied it, you verified the result.
Exercise 3: Discover the MCP Bypass
If you have any MCP servers configured (like mcp-server-time from Lesson 7), ask your agent:
What time is it in Tokyo?
Does the approval prompt appear? (No. MCP tools bypass before_tool_call hooks.)
Now ask your agent:
Why did the approval gate not fire for the time tool?
Read the six constraints at [paste your Six Constraints URL]
and explain Constraint 5.
What you are learning: The MCP bypass is the design constraint that shapes Chapter 57. When you build your own MCP server, you cannot rely on gateway hooks to gate operations. The server must protect itself.
When Emma came back, James had the approval prompt open on his phone and the gateway log scrolling on his terminal.
"It works." He showed her the WhatsApp thread. "Agent calls exec, hook fires, I get the approval prompt, I approve, tool runs."
"What did you write?"
"Five lines of English and two URLs." He showed her the prompt. "The constraints section and the reference code. The agent built the plugin, registered it, restarted the gateway."
Emma looked at the gateway log. "What about MCP tools?"
James paused. He sent a message: "What time is it in Tokyo?" The time appeared instantly. No approval prompt.
"The hook does not see MCP tools," he said. "They get added after the hook wrapping. If I build something in Chapter 57 that calls customers or books appointments, this gate will not protect those operations."
"So what do you do?"
"Build the gate into the MCP server itself." He looked at Emma's three-tier table. "Tool profiles are Tier 1. This plugin is Tier 2. Neither tier protects MCP operations. The MCP server has to protect itself."
She glanced at his phone. "I shipped a plugin once with every constraint right except the tool name. Used the display name instead of the internal name. Took me an hour to figure out why nothing fired."
James looked at the prompt on his screen. "At my old company, writing that purchase order policy took three meetings and a legal review. This took five lines and two links. But the thinking was the same: figure out the constraints, write them down clearly enough that someone else can execute them. That someone just happens to be an AI."
"Lesson 14. All of this runs on your laptop. Close the lid and the doorbell goes silent."
Flashcards Study Aid
Technical Reference
Your agent reads this section via the URL you pasted into the prompt. You do not need to read or understand the code below. If the plugin works, skip ahead to Flashcards. If it does not work, the When It Does Not Work section above has the debugging steps.
Six Plugin Constraints
OpenClaw plugins fail silently when these constraints are violated. No error message. No warning. The plugin loads, compiles, and does nothing.
Constraint 1: api.on() NOT api.registerHook(). OpenClaw has two hook systems. Both compile. api.registerHook() is the legacy untyped system that silently skips registration without special config. api.on() is the typed system that actually works. Use api.on().
Constraint 2: Discovery is not activation. plugins.load.paths tells the gateway where to find the plugin. plugins.entries.<id>.enabled = true activates it. Without both, the plugin appears loaded but never runs.
Constraint 3: Hook name requirement (legacy only). The legacy registerHook system requires { name: "my-hook" } in options. api.on() does not have this requirement.
Constraint 4: Tool name normalization. The display name is bash. The internal name is exec. A hook checking for "bash" never fires. Include console.log("[plugin-name] tool call:", event.toolName) to verify.
Constraint 5: MCP tools bypass before_tool_call hooks. MCP tools are appended after hook wrapping. The approval gate does not intercept MCP tool calls. MCP servers must protect themselves.
Constraint 6: Approval routing config required. The plugin returns requireApproval, but the gateway needs routing instructions to deliver the prompt. Add approvals.plugin to openclaw.json with enabled: true and mode: "session". Without this, the hook fires, the tool call blocks, but no approval prompt reaches the operator. The agent receives a "blocked" signal and responds with confused text about needing approval instead of the formatted prompt appearing in chat.
{
"approvals": {
"plugin": {
"enabled": true,
"mode": "session"
}
}
}
Three modes: "session" (approval prompt appears in the same chat), "targets" (routes to specific channels/users), "both" (enables both paths). Use "session" for WhatsApp same-chat approval.
Reference Implementation
Three files in ~/.openclaw/plugins/my-approval-gate/:
{
"name": "my-approval-gate",
"version": "1.0.0",
"type": "module",
"main": "index.ts"
}
{
"id": "my-approval-gate",
"name": "My Approval Gate",
"description": "Requires operator approval before exec calls",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "my-approval-gate",
name: "My Approval Gate",
description: "Requires operator approval before exec calls",
register(api) {
api.on(
"before_tool_call",
async (event) => {
console.log("[my-approval-gate] tool call:", event.toolName);
if (event.toolName !== "exec") return {};
return {
requireApproval: {
title: "Shell Command Approval Required",
description: `Command: ${event.params?.command}`,
severity: "warning",
timeoutMs: 120_000,
timeoutBehavior: "deny",
},
};
},
{ priority: 100 },
);
},
});
requireApproval fields: title (shown to operator), description (the command), severity (info/warning/critical), timeoutMs (120000 = 2 minutes), timeoutBehavior ("deny" = fail closed, "allow" = fail open).