Voice
Human Gate
Manual Verification Protection + AI Selector Compatible with OpenClaw . This is a complete TypeScript plugin project—a true implementation of the `before_tool_call` hook that can intercept tool calls, follow OpenClaw’s built-in approval workflow, and features an approval window mechanism.
Install
npm install
npm
README
# openclaw-human-gate
Human-in-the-loop approval middleware for [OpenClaw](https://github.com/openclaw/openclaw).
Intercepts tool execution with a `before_tool_call` hook and routes selected
calls through OpenClaw's **built-in** approval flow. When a call needs human
confirmation, the agent run is paused and the approval request is pushed to
every connected approval surface — the official TUI, the Control UI (Web), and
any chat channel that supports `/approve`. This plugin does **not** implement
its own terminal UI; it reuses OpenClaw's.
```
Agent
|
Tool call
↓
Human Gate Plugin
/ \
auto require-approval
↓
OpenClaw approval flow
(TUI / Web UI / /approve)
↓
Approve / Reject / Modify
```
## How it works
OpenClaw already ships a first-class approval mechanism: a `before_tool_call`
hook may return `{ requireApproval: { ... } }`, and the Gateway handles pausing
the run, rendering the prompt in the TUI / Control UI, enforcing the timeout,
and applying the decision. `deny`, `timeout`, and `cancelled` fail closed
(blocked).
This plugin is the **policy layer** on top of that mechanism: it decides which
calls need approval, with what severity and timeout, and remembers
`allow-always` decisions per session so the human is not re-prompted.
## Default posture (not "gate everything")
The plugin does **not** intercept every tool by default — that would be
unusable. Instead it classifies each call and only prompts for side-effecting
operations:
1. **User rules** (first match wins) — explicit override.
2. **Built-in destructive toolKinds** — `exec`, `apply_patch`, `code_mode_exec`
→ `require-approval`.
3. **Name-pattern classifier** (host `toolKind` first, then tool name):
- read-only (`read_*`, `get_*`, `list_*`, `search_*`, `glob`, `grep`,
`view_*`, `status_*`, `fetch`, …) → **auto** (pass through)
- destructive (`write_*`, `edit`, `delete_*`, `rm*`, `deploy_*`,
`publish_*`, `install_*`, `run_*`, `apply_*`, `send_*`, …) →
**require-approval**
4. **`defaultMode`** — fallback for anything unrecognised. Defaults to **`auto`**
(low friction). Set to `require-approval` for a strict shop where any
unknown tool must be approved.
Disable classification with `useClassifiers: false` to rely solely on explicit
rules + `defaultMode`.
## Approval window (less popup fatigue)
Gating every write means a multi-step task (refactor 10 files) prompts 10
times. The approval window fixes this: after you approve **one** destructive
call, further matching calls auto-pass for a turn or a time box.
```json5
approvalWindow: {
mode: "turn", // "off" | "turn" | "time" (default "turn")
match: "same-tool", // "same-tool" (default) | "destructive"
ttlMs: 300000, // for "time" mode only
bypassCritical: true // severity "critical" always prompts (e.g. prod deploys)
}
```
- `mode: "turn"` — once approved, same-class writes auto-pass for the rest of
the current agent run; a new user turn resets. (default)
- `mode: "time"` — once approved, same-class writes auto-pass for `ttlMs`.
- `mode: "off"` — prompt every destructive call (per-call behavior).
- `match: "same-tool"` — only the approved tool name shares the window (safer default; e.g. approving `apply_patch` does not auto-approve `exec`).
- `match: "destructive"` — one shared window for all gated writes (broadest and lowest-friction; opt in deliberately).
- `bypassCritical: true` — `severity: "critical"` calls (e.g. a `deploy-prod`
rule) always prompt even when a window is open.
The window is opened automatically when you approve a call (allow-once or
allow-always). `deny` / `timeout` / `cancelled` do not open it. It is stored
per session. This is separate from `allow-always`, which is a permanent
per-(rule, tool) grant for the whole session.
## Install
```bash
# from ClawHub (once published)
openclaw plugins install clawhub:openclaw-human-gate
# from a local tarball (for development)
npm pack --pack-destination /tmp
openclaw plugins install npm-pack:/tmp/openclaw-human-gate-0.1.0.tgz --force
# inspect
openclaw plugins inspect human-gate --runtime --json
```
Requires OpenClaw `>= 2026.7.2` (Node 22.22.3+ / 24.15+ / 25.9+).
## Build
```bash
npm install
npm run build # emits ./dist
npm run typecheck # tsc --noEmit
```
## Configure
The plugin reads its config from OpenClaw's plugin config
(`plugins.entries.human-gate.config`). Evaluation order: user rules → built-in
destructive toolKinds → name-pattern classifier → `defaultMode`. See
**Default posture** above.
```json5
{
plugins: {
entries: {
"human-gate": {
enabled: true,
config: {
defaultMode: "auto",
defaultSeverity: "warning",
defaultTimeoutMs: 300000,
rememberAllowAlways: true,
useClassifiers: true,
approvalWindow: {
mode: "turn",
match: "same-tool",
ttlMs: 300000,
bypassCritical: true
},
rules: [
{
id: "deploy-prod",
toolName: "deploy_service",
mode: "require-approval",
severity: "critical",
allowedDecisions: ["allow-once", "deny"],
timeoutMs: 300000,
reason: "Deploy to production"
},
{
id: "read-only-auto",
toolNamePattern: "^(read_|get_|list_).*",
mode: "auto"
},
{
id: "block-rm",
toolNamePattern: "^rm_.*",
mode: "block",
reason: "Destructive rm_* tools are blocked"
}
]
}
}
}
}
}
```
### Rule fields
| field | type | meaning |
| ----------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
| `id` | string (required) | Stable id; used in logs and allow-always keys. |
| `toolName` | string | Exact tool name match. Omit to match any. |
| `toolNamePattern` | string (regex source) | Matched against `toolName`; an invalid regex makes the rule non-matching and is never treated as match-all. |
| `toolKind` | string | Match host `toolKind` (e.g. `exec`, `code_mode_exec`, `apply_patch`). |
| `mode` | `auto` \| `require-approval` \| `block` (required) | Decision for a matched call. |
| `severity` | `info` \| `warning` \| `critical` | Shown in the approval UI. Defaults to `defaultSeverity`. |
| `allowedDecisions`| `["allow-once","allow-always","deny"]` | Decisions offered to the approver. |
| `timeoutMs` | integer (1000–600000) | Approval timeout. Defaults to `defaultTimeoutMs`. |
| `reason` | string | Human-readable reason in the approval request / block reason. |
### Built-in behavior (applied when no user rule matches)
Destructive toolKinds (always gated): `exec`, `apply_patch`, `code_mode_exec`.
Name-pattern classifier (`useClassifiers: true`, default):
- read-only names → `auto`: `read_*`, `get_*`, `list_*`, `search_*`, `glob`, `grep`, `view_*`, `show_*`, `status_*`, `ping`, `fetch`, `head`, `cat`, `ls`, `find`, `whoami`, `echo`, `inspect_*`, `describe_*`, `explain_*`, `query_*`, `count_*`
- destructive names → `require-approval`: `write_*`, `edit`, `delete_*`, `remove_*`, `rm*`, `rmdir`, `mkdir`, `move_*`, `rename_*`, `deploy_*`, `publish_*`, `install_*`, `uninstall_*`, `exec`, `run_*`, `apply_*`, `patch_*`, `create_*`, `update_*`, `kill_*`, `send_*`, `post_*`, `put_*`, `push_*`, `commit_*`, `flush_*`, `drop_*`, `truncate_*`, `grant_*`, `revoke_*`
Anything else → `defaultMode` (default `auto`).
### Approval routing
The approval prompt is delivered by the Gateway. To route it to a specific
channel (e.g. Slack DM), set the `approvals.plugin` block in OpenClaw config:
```json5
{
approvals: {
plugin: {
enabled: true,
mode: "targets",
agentFilter: ["main"],
targets: [{ channel: "slack", to: "U12345678" }]
}
}
}
```
The approval popup shows a bounded summary: tool name/kind, up to four derived
paths, selected safe scalar parameters (`command`, `file_path`, `url`,
`environment`, etc.), policy reason, and rule id. It never dumps the complete
params object, reducing accidental secret exposure and keeping the prompt
readable.
In a chat channel, resolve with `/approve <id> allow-once|allow-always|deny`.
## Ask tool (`human_gate_ask`)
The plugin also registers an optional `human_gate_ask` tool the agent can call
when it needs clarification, a decision, or more context. It is the Claude Code
"ask the human" pattern.
Enable it (it is `optional`):
```json5
{ tools: { allow: ["human_gate_ask"] } }
```
Parameters: `question` (required string), `choices?` (string[]), `allowFreeText?`
(boolean, defaults to true when no choices), `context?` (string).
The tool's `execute` returns a standard `{ content, details }` result whose text
is the question + numbered choices. The agent presents it in chat and waits for
the human's reply in the next turn. The structured `details` carries
... (truncated)
voice
Comments
Sign in to leave a comment