Skip to content
Agentic Control Plane

Claude Code Hooks vs ACP: When a 50-Line Hook Is Enough

David Crowe David Crowe · · Updated · 6 min read
claude-code codex hooks tool-policy build-vs-buy
Share X HN LinkedIn

The best objection to what we build is: “I can do this with a Claude Code hook in an afternoon.” True. Here’s the afternoon version, working:

// ~/.claude/settings.json
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [
  { "type": "command", "command": "node ~/hooks/deny.mjs" } ] } ] } }
// ~/hooks/deny.mjs — deny two catastrophes, allow the rest
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));
const cmd = input.tool_input?.command ?? "";
if (/rm\s+-rf\s+[~/]/.test(cmd) || /push\s+--force.*main/.test(cmd)) {
  console.log(JSON.stringify({ hookSpecificOutput: {
    hookEventName: "PreToolUse", permissionDecision: "deny",
    permissionDecisionReason: "blocked by local hook" } }));
}

This is genuinely good. It’s deterministic, it runs outside the model, it survives --dangerously-skip-permissions, and it took twenty minutes. If you run one harness on one machine, you could stop reading here — honestly.

Where it rots

We know because our production version is the same idea with two years of scar tissue. The rot arrives in a fixed order:

Respelling. The regex above misses rm -r -f ~/, rm --recursive --force ~, bash -c "rm -rf ~", and the catastrophe hidden after &&. Every string you add teaches you why deny lists get bypassed; the real fix is tokenizing every segment of the command line and recursing into bash -c — at which point your afternoon script is a parser with test cases.

The second harness. The day Codex shows up, your hook needs Codex’s dialect — different config file, plus a one-time /hooks trust review without which Codex lists your hook and silently never runs it. Cursor is a third dialect. The rules drift apart precisely because each copy is cheap to edit and nothing checks parity. Hand-maintained, per-integration enforcement rots — that’s not an opinion about your discipline, it’s what N×M maintenance does.

Ask, not just deny. Deny-or-allow is easy. The useful middle — “git push asks first, curl to unknown hosts asks first” — needs the ask wire format per harness (and Codex’s parser acts on deny only, so ask has to degrade correctly there).

The log. The first time something goes wrong you’ll want one file that says what ran and why it was allowed — which means every decision, including the allows, from every harness, in one format. Bolting that on third is how ours became one dispatcher instead of three scripts.

Updates. Hook APIs move. Harnesses add tools mid-flight. Your script gets the maintenance budget of any other internal tool: whatever’s left.

The line

Build it when: one harness, one machine, rules you can count on one hand, and you’ll actually maintain the parser. Genuinely fine.

Stop building it when any of these arrive: a second harness, a teammate who needs the same rules, an ask-tier, or the day you need the audit answer fast.

Our local runtime is the maintained version of the exact script above — one hook dispatcher across Claude Code, Codex, and Cursor, a 268-line pure decision engine you can read in one sitting, a floor that’s respelling-proof, allow / ask / deny in one policy file, every decision logged. One command, no account, MIT, on-device:

curl -sf https://agenticcontrolplane.com/install.sh | bash -s -- --local

Keep your hook in the repo anyway. Reading what will say no to your agent is a habit worth having — ours is here, and the 60-second recording is what it looks like running.

Should I build agent control myself or use a tool?

Build it if you run one harness on one machine and enjoy maintaining it — a PreToolUse hook denying a handful of patterns is a fine afternoon project. Adopt something once you need respelling-proof parsing, more than one harness, an audit trail, or rules a teammate also has to have.

What can a PreToolUse hook actually do?

Everything enforcement needs: it receives the tool name and full input before execution and can allow, ask, or deny with a reason. It runs outside the model, so a prompt-injected agent can’t talk its way past it, and it still fires under –dangerously-skip-permissions.

Do Codex and Cursor support the same hooks?

Each has its own dialect: Claude Code’s settings.json, Cursor’s hooks.json with lowercase event names, and Codex’s hooks.json plus a one-time /hooks trust review without which hooks are silently skipped. A DIY setup means maintaining all three; drift between them is the failure mode.

Is the ACP local runtime open source?

Yes — MIT. The installer, the 268-line decision engine, and the hook are readable top to bottom, and local mode works with no account and no network calls in the decision path.

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog