The review Jason asked for in Discord on 2026-09-16, against openclaw/openclaw main as of
2026-09-15, and the answer it produced.
The obvious contribution, a guardrail provider interface in core, has been proposed and refused twice:
- Issue #46441, "Pluggable Guardrail Provider Interface for tool authorization", closed 2026-04-26 by clawsweeper.
- PR #64868, which implemented it, closed 2026-04-25 by steipete.
Both give the same reason. VISION.md keeps core lean, optional runtime capability ships as a
plugin, and guardrail providers belong on ClawHub. clawsweeper's closing comment names the
only condition under which a core issue is welcome again:
open a narrow SDK/API issue only if a concrete provider cannot be implemented with the existing hook context, block, rewrite, and approval semantics
So the order is: build the provider, find the wall, then file. A feature proposal filed before the provider exists gets the same boilerplate the last two got.
src/plugins/hook-before-tool-call-result.ts on main:
export type PluginHookBeforeToolCallResult = {
params?: Record<string, unknown>;
block?: boolean;
blockReason?: string;
requireApproval?: {
title: string; description: string; severity?: "info" | "warning" | "critical";
timeoutMs?: number; allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">;
onResolution?: (decision: PluginApprovalResolution) => Promise<void> | void;
};
};Three returns, three decisions:
| OpenClaw | CTRLRun |
|---|---|
return {} |
ALLOW, and the executor is entered |
block: true |
DENY, with the rule that refused it |
requireApproval |
APPROVE, through the framework's own interrupt |
| host executes the tool | the executor passed to Control.execute |
after_tool_call |
the executor's return, NotExecuted, or anything else |
That last row is why this is an adapter in CTRLRun's own sense (SPEC-v0.5 §2) rather than a
wrapper. The adapter surface exists for one reason, to route an APPROVE through a
framework's own human-in-the-loop primitive instead of raising past it, and OpenClaw has one.
Two host properties make the fit better than expected, both from
docs/plugins/hooks/reference.md:
before_tool_callfails closed: "On thrown error or timeout: Fail closed: block the run, tool call, or install", 15 seconds by default. A guardrail whose decision point is down blocks tool calls, without the plugin implementing anything.- Unresolved approvals always deny.
timeoutBehavior: "allow"is deprecated inhook-before-tool-call-result.tswith a note that it is retained for compatibility only.
The 15-second fail-closed budget means the interrupt cannot block inside the hook. A wait()
there would deny every approval a human took more than fifteen seconds to answer. So the hook
returns requireApproval immediately, the host owns the waiting, and the CTRLRun worker
thread blocks instead on a verdict the plugin posts back when onResolution fires.
That works, and it leaves exactly one residual window: onResolution cannot veto. Its
return type is Promise<void> | void. Once the operator answers allow-once the host
proceeds, so if CTRLRun's second pass then refuses, because a precondition moved or the
approval no longer binds to those arguments, the plugin cannot stop the call.
This is the narrow gap clawsweeper's closing comment left the door open for. It is not a
request for a guardrail subsystem; it is one optional field on an existing result type.
ISSUE-DRAFT.md is the filing, held until the provider is running against a live host so the
issue can point at it.
PR #142700, "prevent tool parameter changes after approval", open since 2026-09-09, is the
same concern one step earlier: it stops an approved call from executing rewritten parameters.
It is also what lets this adapter declare carries_approved_arguments: true honestly, because
the host's frozen snapshot is then a real second record rather than an echo of the request.
OpenClaw's MCP client takes a URL:
{ mcp: { servers: { payments: { url: "http://127.0.0.1:8900/mcp", transport: "streamable-http" } } } }ctrlrun gateway --upstream <the real server> intercepts tools/call and relays every other
method unchanged, so any MCP server OpenClaw talks to can be put behind a policy today, with
no code on either side. ctrlrun mcp-operator added as a second MCP server lets the held
approval be answered from the same conversation. That covers MCP tools; it does not cover
OpenClaw's own exec, write, browser or messaging tools, which is what the plugin is for.
Both found by reading the tracker before writing the README, and both are load-bearing:
- #102961, open,
needs-product-decision, diamond-rated.after_tool_callflattens the structuredToolResultto a bare string for exec and bash-family tools, soevent.erroris set only for harness-level failures and a nonzero exit reports nothing. A maintainer-framed comment already lays out three options. This is the single best thread in the repo for CTRLRun to speak in, because the whole issue is the distinction between failed and cannot tell, which is the kernel's subject.no-new-fix-pr, so a comment, not a PR. - #119253, open,
needs-security-review. Bundle-MCP tools bypasswrapToolWithBeforeToolCallHook, so MCP tool calls produce no audit records and the plugin hooks do not run for them. The plugin therefore does not gate MCP tools today, andctrlrun gatewayis the answer for those.
Four providers are already visible in those threads: CRE (claude-rule-enforcer), APort Agent
Guardrails, SHACKLE (pyshackle), and openclaw-dataops-guardian. Every one of them is a
decision engine. None of them models execution, so none can tell a failed call from one whose
outcome was never established, and OpenClaw's own observation hook cannot help them: it runs
concurrently, its return value is ignored, and the reference tells plugin authors not to rely
on it for a policy requirement.
That is the whole differentiation, and it is structural rather than a feature list.