Skip to main content

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:

TierMechanismEnforcement LevelWhat It Prevents
1Tool profiles (coding/messaging/minimal)In-process, same Node.js runtimeAgent accessing unauthorized tools
2Plugin hooks with requireApprovalIn-process, plugin intercepts tool call before executionSensitive operations without operator approval
3NemoClaw sandboxingOut-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.

Why the Link Matters

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:

  1. The agent decides to call the exec tool
  2. Your plugin's before_tool_call hook fires
  3. The hook returns requireApproval
  4. The gateway sends the approval prompt to you on WhatsApp
  5. 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:

DecisionEffect
allow-onceThis call runs. Future calls still require approval.
allow-alwaysThis and all future calls from this plugin run without asking.
denyThis 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.

Slack Approval Routing

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:

  1. openclaw plugins list --verbose shows your plugin loaded and enabled
  2. Gateway log shows your plugin's console.log line: tail -f ~/.openclaw/logs/gateway.log
  3. The log shows tool call: exec (not bash)
  4. If the log shows tool call: exec but 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."
  5. 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

This section is for your agent, not for you

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.

openclaw.json (add to top level)
{
"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/:

package.json
{
"name": "my-approval-gate",
"version": "1.0.0",
"type": "module",
"main": "index.ts"
}
openclaw.plugin.json
{
"id": "my-approval-gate",
"name": "My Approval Gate",
"description": "Requires operator approval before exec calls",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
index.ts
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).