warden-fast
July 2, 2026 · View on GitHub
A tiny compiled client that lets warden screen every Claude Code tool call with no felt latency.
Why
A Node hook pays Node's startup + ESM module-load cost on every single tool
call. Measured on this machine (node native/bench.mjs):
node cc-hook.mjs : median 78.0 min 69.4 max 84.7 ms
warden-fast.exe : median 18.3 min 15.6 max 21.3 ms
speedup (median) : 4.3x (saves ~60ms per tool call)
The daemon already loads the classifier + policy once and answers over a socket
— but a Node client still pays Node startup to ask it. warden-fast is that
client, compiled: it cold-starts in ~2ms and is a pure byte pipe.
How it works
Claude Code ──stdin(JSON)──▶ warden-fast ──TCP 127.0.0.1──▶ warden daemon
▲ │
└──────── stdout bytes ────────┘
- The daemon (
warden-serve) writes its loopback port to~/.warden/daemon.json(0600). warden-fastreads that port, forwards the hook's stdin verbatim, reads back one line, and prints it — that line is exactly the bytes the hook should emit (ahookSpecificOutputdeny/ask JSON, or empty for allow/defer).
It parses none of the payload, holds no policy, runs no classifier. All logic stays in the daemon (JS) — the single source of truth. The binary is dumb on purpose: nothing to keep in sync, nothing to drift.
Fail-safe, not fail-open. If the daemon can't be reached (not running,
timeout, or short read), warden-fast falls back to the in-process Node hook
(src/cc-hook.mjs) — slower, but it still screens. Only if that fallback is
also unavailable does it exit 0 with no output so Claude Code proceeds — warden
never bricks your tooling, but it never silently stops screening either. The
fast ~2ms path is unchanged; the fallback is a cold path that runs only when the
daemon is down. Override the fallback hook with WARDEN_FALLBACK_HOOK (and the
node binary with WARDEN_NODE).
Security model
The fast-hook listener is bound to 127.0.0.1 only — never exposed off-box.
Any process running as the same user can connect, but that is already inside
warden's trust boundary: such a process could edit ~/.warden/config.json,
unregister the hook, or kill the daemon regardless. So loopback-only is the real
control; no shared token is used (it would add fragility, not security, at this
boundary). This matches the existing named-pipe transport.
Build
Requires Go ≥ 1.21. Zero external dependencies (stdlib only) → a single static binary, no libc, no runtime.
cd native
# this platform
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o warden-fast .
# cross-compile for the fleet
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o warden-fast-linux-amd64 .
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o warden-fast.exe .
Verify against a live daemon (spawns the binary exactly as Claude Code does):
node native/smoke.mjs # correctness: deny / defer / daemon-down fallback
node native/bench.mjs 30 # latency vs the node hook
Use it as the hook
- Run the daemon (ideally as a service so it's always up):
warden-serve # listens on the socket + loopback fast-hook - Point your Claude Code PreToolUse hook command at the binary instead of
warden-hook:{ "type": "command", "command": "C:\\path\\to\\warden\\native\\warden-fast.exe", "timeout": 5 }
That's it — same deny/ask/allow behavior, ~60ms cheaper per call.