SidClawSidClaw is a governance layer for AI agents. The whole pitch fits in four words: fail closed by...
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.
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.
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.
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.
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.
===, which short-circuits on the first mismatched byte and leaks timing. Swapped for crypto.timingSafeEqual. Textbook, and we should have shipped it that way the first time.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.
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.