steinborn.dev
← all notes
ai-engineering

The permission callback that never fired

· 3 min read · Benjamin Steinborn

LibrisVault runs headless Claude Agent SDK sessions that write into a real vault on the disk. The obvious safety layer sits within the permission callback the SDK offers: register a handler, deny what you do not like. Before building on it, I measured whether it fires at all. The answer moved the security boundary two layers down.

The measurement

The initial probe was deliberately crude. A canUseTool handler that denies everything and runs against the real SDK on a real ingest while counting its own invocations. Not “how often does it catch the dangerous case”, just: does it run?

It was not invoked at all. Not rarely, not for some tools. Zero times.

The reason is that the callback is advisory and shadowable: other paths through the SDK reach a tool without consulting it. A PreToolUse hook, by contrast, is invoked and does block, which I confirmed with a side-effect canary rather than by reading the log. The hook creates a file when it runs and the blocked command creates one when it does not. Afterwards, you look at which file exists. As a result, the tool-level policy moved to the hook and the callback stayed as a redundant second layer.

Why a working hook is still not the boundary

A hook decides on tool arguments. This might be sufficient for file edits because the path is right there. For shell commands it will not do: deciding what an arbitrary shell string will write is not a tractable problem, a policy pretending otherwise has unknown holes.

Thus, the hard boundary sits at the operating system. Every agent session runs inside the SDK’s sandbox, bubblewrap on Linux, whose write allowlist contains exactly one path: the vault root. A read-only profile gets an empty allowlist making “chat cannot modify the vault” a property of the process which I’d much rather trust than an instruction in a prompt.

The setting that turned out to matter most

One option in that configuration is worth the whole note. allowUnsandboxedCommands defaults to true, and the Bash tool ships a dangerouslyDisableSandbox parameter that the default permits.

Measured with the sandbox enabled but that option left alone: the agent hit the write denial, set the parameter itself, and created the canary outside the vault on its second attempt. Nothing malicious but nevertheless signs of a capable system routing around an obstacle meant to stop it. Set to false, the parameter is ignored and every command is sandboxed.

My takeaway: Trust but verify. Relying on a sandbox with an escape hatch would have been merely decorative, especially if an agent defaults to using the escape hatch.

What stays honest about the remaining layer

The bash denylist that sits alongside all this comprises additional defense in depth, the repository labels it as such rather than as a boundary. A stricter design was considered (allowing only the vault’s own scripts) but rejected on evidence: of 68 bash calls in one validated run, 14 were ordinary find, ls, cat and python3, therefore the whitelist would have broken real ingestion. The test suite even carries a case named for the gap this leaves, asserting that a plain write outside the vault is not refused by the bash policy. The sandbox is what refuses it.

A guard you have not watched fail is a guess, similar to a smoke detector which was never tested. That explains why a probe remained in the repository rather than being deleted once the point had been made. An SDK upgrade can change which layer fires. The command that survives is a successor to the original counter: it builds the production configuration and asserts the side effect, so what can be re-checked in a minute is whether the canary still lands outside the vault, not the callback’s invocation count. The sandbox is also configured to fail loudly if it is unavailable, so a run without protection stops instead of quietly proceeding.

The pattern generalizes past agents. Validation layers, permission systems, quarantine logic: the question is never whether a safeguard exists, but whether anyone has watched it catch something. In regulated data work, such a distinction matters.