# Gate Your Agent's Tools (/docs/meet-your-personal-ai-employee/14-gate-your-agents-tools)

---
type: Document
title: "Gate Your Agent's Tools"
description: "Specify a TypeScript plugin that intercepts tool calls and requires operator approval on WhatsApp, then have your AI Employee build it from a constraint-driven prompt referencing this lesson."
status: stable
order: 9614
chapter: 56
lesson: 14
duration_minutes: 35
generated:
  by: ag-archive/0.1.0
  at: 2026-08-26T00:00:00Z
ksor:
  audience: [public]
  approval:
    by: ag-archive/0.1.0
    at: 2026-08-26T00:00:00Z
trust_tier: unverified
build_id: sha256:c8912c3008ae345a7fdc33ace656531ad74f37c065177057c390c7d759f1e970
source_commit: dad4758bb59919c7b9130444778ade062a2dce3e
ksor_version: 0.0.47
---

## What You Will Learn

In this lesson, 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. Lesson 3 had shown him the binary gate: `coding` gives the agent shell access, `messaging` takes it away. In Lesson 13, 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 16.

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](#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.

> [!TIP]
> **[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:

```bash
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:

| 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.

> [!NOTE]
> **[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: `openclaw logs --follow` (file at `/tmp/openclaw/openclaw-YYYY-MM-DD.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.

---

## What You Should Remember

### Three-Tier Safety Model

**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.

### Constraint-Driven Specification

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.

### Six Constraints That Cause Silent Failures

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 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 15. All of this runs on your laptop. Close the lid and the doorbell goes silent."

## Flashcards Study Aid

<Flashcards />

---

## Technical Reference {#technical-reference}

> [!NOTE]
> **[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](#when-it-does-not-work) section above has the debugging steps.


### Six Plugin Constraints {#six-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.

```json title="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 {#reference-implementation}

Three files in `~/.openclaw/plugins/my-approval-gate/`:

```json title="package.json"
{
  "name": "my-approval-gate",
  "version": "1.0.0",
  "type": "module",
  "main": "index.ts"
}
```

```json title="openclaw.plugin.json"
{
  "id": "my-approval-gate",
  "name": "My Approval Gate",
  "description": "Requires operator approval before exec calls",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}
```

```typescript title="index.ts"

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).
