SidClaw is a governance layer for AI agents. The whole pitch fits in four words: fail closed by default. If a tool call can't be evaluated against a policy (missing config, an unknown decision value, an error mid-evaluation), the safe move is to stop, not to shrug and let it run.
That's easy to write on a landing page. It's a property you have to actually hold at every boundary in the code, and this week we sat down and checked whether we did. We didn't, in several places. Here's the honest list, because a fail-open bug in a fail-closed product is both the most embarrassing kind and the most instructive.
The worst one: hooks failing open on a missing config
Our hooks integration loads a policy config and evaluates each tool call against it. If the config file was missing, the old code fell through to allow.
Simplified, but this is the shape of it:
// before
const config = loadPolicyConfig(path);
if (!config) {
return { decision: "allow" }; // fail-open
}
Read that again. The one situation where you know the least about what's permitted, no policy loaded at all, was the exact situation where we let everything through. A misconfigured deploy, a bad path, a missing env var, and the governance layer quietly becomes a passthrough.
// after
const config = loadPolicyConfig(path);
if (!config) {
return { decision: "deny", reason: "no policy config loaded" };
}
Missing config is now a deny with a reason attached, so it shows up in the trace instead of leaving you wondering why nothing ever got blocked.
Two MCP methods that skipped policy entirely
Our MCP proxy governs tool calls. But resources/read and prompts/get weren't going through policy evaluation at all. They got treated as "just reading" and passed straight through. A resource read can pull a file off disk. A prompt fetch can pull in instructions that change what the agent does next. Neither is obviously safe, and both now go through the same evaluate path as tools/call.
A wrapper that forwarded decisions it didn't understand
Our SDK wrappers take a decision back from the policy engine (allow, deny, hold-for-approval) and act on it. If a wrapper got back a value it didn't recognize, from version skew, a new decision type, a serialization bug, the old behavior was to forward the call. Unknown now means deny. If we don't understand the decision, the agent doesn't get to act on it.
The less obvious ones
A few of the ten fixes weren't fail-open in the "allow instead of deny" sense, but they were the same class of bug: the safe branch wasn't the default.
-
Non-constant-time secret comparison in the MCP HTTP proxy. We compared an inbound secret with
===, which short-circuits on the first mismatched byte and leaks timing. Swapped forcrypto.timingSafeEqual. Textbook, and we should have shipped it that way the first time. - A held trace could still be finalized. A trace on hold pending human approval could have its outcome finalized through a different code path, which quietly defeats the approval gate. The API now rejects finalization of a held trace.
- Non-deterministic tiebreak for equal-priority rules. When two policies had the same priority, which one won wasn't deterministic. For a governance decision, "it depends on map ordering" is not an acceptable answer. Ties now resolve to the most restrictive rule.
- The OpenClaw plugin skipped some tools by name. It carried a name-based exclusion list: govern everything except these. Exclusion lists are how things slip through. It now governs every tool and the skip list is gone.
The actual lesson
Fail-closed isn't a default you declare once at the top of the readme. It's an invariant you have to defend at every place a decision gets made: config load, unknown enum, error path, tiebreak, the method you assumed was read-only. Every one of those is a branch, every branch has a safe side and an unsafe side, and the unsafe side is almost always less code. So it's what you write when you're not thinking about it.
The fixes took an afternoon. The habit that catches them (assume every new branch is fail-open until you prove otherwise) is the harder thing to build, and it's what we're actually trying to install now.
What this doesn't mean
It doesn't mean the code is now provably fail-closed everywhere. An audit finds what you go looking for. We looked at config loading, decision handling, the MCP method surface, and the approval gate. We haven't done formal verification, we haven't fuzzed the policy evaluator, and there are branches we didn't think to check. If you're running this kind of tool, ours or anyone's, the useful question isn't "does it say fail-closed." It's "which specific code paths did they check, and can you see the trace when something gets denied."
The fixes are merged. The repo is open source (Apache 2.0) if you want the actual diffs instead of the simplified versions above.
- Repo: github.com/sidclawhq/platform
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.