In this chapter, you will build a plugin that requires your approval before the agent runs shell commands, and you will build it by telling the agent what to build.
By the end, you should be able to explain the three-tier safety model (tool profiles gate access, plugin hooks gate execution, NemoClaw sandboxes the runtime), write a constraint-driven specification that produces a working plugin, and identify the six plugin constraints that cause silent failures when violated. You will also experience the pattern: specify constraints, let the agent build, verify the result.
James stared at the tool profiles page in the dashboard. Module 9.1, Chapter 3 had shown him the binary gate: coding gives the agent shell access, messaging takes it away. In Module 9.1, Chapter 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:
"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 chapters. 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 chapters 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 Module 9.1, Chapter 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 Module 9.1, Chapter 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.
Copy the URL of this page from your browser. Send this prompt to Claude Code or your AI Employee on WhatsApp:
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.
After your agent finishes, check the dashboard or run:
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.
With the plugin loaded, trigger it. Send a message on WhatsApp:
Here is what happens:
Three decisions:
Respond:
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.
If the approval prompt never appears, check in this order:
Send your agent the specific symptom you see. It has the constraints URL. It can diagnose and fix.
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.
Send your agent a follow-up prompt:
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.
If you have any MCP servers configured (like mcp-server-time from Module 9.1, Chapter 7), ask your agent:
Does the approval prompt appear? (No. MCP tools bypass before_tool_call hooks.)
Now ask your agent:
What you are learning: The MCP bypass is the design constraint that shapes Module 9.2. When you build your own MCP server, you cannot rely on gateway hooks to gate operations. The server must protect itself.
Tier 1: Tool profiles control binary access (coding, messaging, minimal, full). Tier 2: Plugin hooks with requireApproval add human sign-off before sensitive operations. Tier 3: NemoClaw sandboxing enforces kernel-level isolation. Each tier protects against different threats.
You do not write the plugin code. You write the constraints and a reference URL. The agent reads both, builds the plugin, registers it, and verifies the load. The skill is specifying constraints precisely enough that the agent builds correctly on the first attempt.
Plugins that violate platform constraints compile, load, and silently do nothing. The six: use api.on() not registerHook(), two-step registration (discovery + activation), correct hook name, tool name normalization (exec not bash), MCP tools bypass hooks, approval routing config required.
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 Module 9.2 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."
"Module 9.1, Chapter 14. All of this runs on your laptop. Close the lid and the doorbell goes silent."
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.
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)
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.
Three files in ~/.openclaw/plugins/my-approval-gate/:
package.json
openclaw.plugin.json
index.ts
requireApproval fields: title (shown to operator), description (the command), severity (info/warning/critical), timeoutMs (120000 = 2 minutes), timeoutBehavior ("deny" = fail closed, "allow" = fail open).