OpenClaw’s Claude-CLI Tool-Policy Bug Shows Why Tool Availability Needs Provenance
OpenClaw’s latest Claude-CLI policy bug is small enough to look like plumbing and important enough to explain a whole class of agent-platform failures. The short version: a tool that is hidden from one bridge because it already exists somewhere else should not become a tool that the user is forbidden to delegate. Those are different facts. OpenClaw briefly treated them like the same fact, and the result was an ACP workflow that failed with a phantom security denial.
The report, filed as GitHub issue #89242 with the concrete symptom split into #89241, affects sessions using a claude-cli backend. In that configuration, OpenClaw’s loopback MCP bridge excludes native tools such as read, write, edit, apply_patch, exec, and process. That exclusion makes sense. Claude Code already exposes those capabilities natively, so the MCP bridge should not present duplicate tools with overlapping semantics and confuse the harness.
The bug is what happened next. According to the issue’s source trace, resolveGatewayScopedTools() receives params.excludeToolNames, turns them into a deny set, and then copies that flattened denylist into the inherited tool-deny state. Downstream, ACP spawn checks inherited denies with findAcpUnsupportedInheritedToolDeny(). It sees apply_patch and refuses to create the child session with an error along the lines of runtime="acp" is unavailable because the requester denies apply_patch. Nobody denied apply_patch. The runtime filtered it out of one presentation layer and accidentally promoted that filtering decision into authority state.
The control plane needs provenance, not a flatter denylist
This is exactly the kind of bug agent systems produce once they stop being chat wrappers and start becoming control planes. A tool can be absent for many reasons. It can be absent because the user is not allowed to use it. It can be absent because the current harness exposes an equivalent native tool. It can be absent because a provider cannot serialize the schema, because a channel cannot safely expose it, because prompt budget is tight, because a feature flag is off, or because a bridge is trying to avoid duplicate names.
Only one of those reasons means “a delegated child must not receive this tool.” OpenClaw’s failure mode is that those meanings collapsed into a single flat inherited denylist. Once that happens, every downstream component has to assume the safest interpretation. ACP spawn did the locally safe thing: if the parent really denies apply_patch, then the child should not get a broader tool surface. That conservative inheritance is correct. The input it received was not.
The relevant prior work matters here. PR #80979, created in May, intentionally added inherited tool restrictions for delegated sessions. That is the right security principle. A user, group, channel, or configured session policy that denies exec should not be bypassable by spawning a child agent. But the principle needs a typed policy model underneath it. “Excluded from MCP loopback because native Claude CLI already has it” is not the same as “denied by operator policy.”
MCP makes boolean tool availability obsolete
The Model Context Protocol pushes this issue from edge case to design requirement. MCP servers, CLI harnesses, local tools, provider-native tool calling, and platform tools can all describe overlapping capabilities. The same logical operation may exist through multiple routes, and the safe presentation for one route may be to hide it because another route owns it. That does not make it a security denial. It makes it a routing decision.
The practical model should look more like { tool, allowed, reason, scope, inheritable } than ["apply_patch"]. A configured policy denial should be inheritable. A sender or group policy denial should probably be inheritable. A provider schema incompatibility might be inheritable only to children using the same provider. A loopback dedup exclusion should not be inheritable as a denial at all. It should carry provenance that says: hidden here, not forbidden everywhere.
This also matters for diagnostics. The failure message in #89241 told the user that the requester denied apply_patch. That is worse than a crash because it points operators in the wrong direction. A user debugging this will inspect policy files, channel permissions, ACP config, and approval settings when the actual problem is presentation-state contamination from the Claude CLI bridge. Good control planes do not merely fail closed; they explain which boundary failed. Otherwise every safety feature becomes another possible liar in the stack.
For OpenClaw operators, the immediate workaround is straightforward but unsatisfying: if ACP spawn fails from a Claude CLI-backed session with a phantom requester denies apply_patch error, try an API-backed model lane while the bug is being fixed. Treat that as evidence of policy contamination, not a real permission setting. Do not loosen actual tool policy to “fix” it unless you have separately confirmed that a configured denial exists.
For platform builders, the lesson is bigger. Tool availability is now a multi-dimensional state, not a boolean. Agent runtimes need to know whether a tool is hidden for UX, hidden for compatibility, denied for security, unavailable because of transport, or withheld because of delegation scope. The old model works until one subsystem uses exclude to mean “don’t duplicate this” and another subsystem reads it as “never allow this.”
The uncomfortable part is that both sides of this bug were trying to be safe. The MCP bridge avoided duplicate native tools. ACP spawn refused to widen a delegated child’s authority. The failure came from the glue code between them. That is where mature agent platforms will win or lose: not in the model’s answer, but in the boring authority metadata that tells every runtime component what a missing tool actually means.
Sources: GitHub issue #89242, GitHub issue #89241, OpenClaw PR #80979, Model Context Protocol specification