Skip to content
· 9 min read

Hooks as contract: automating guardrails for agents

Hooks are where the team's politics become code. What needs to run before, after, and around the agent - and what should fail loudly.

Victor Dantas AI Committee · Bamse

Every team rule that lives only in people's heads will get broken sooner or later. "Always run prettier before committing." "Never let anything go straight to main." "Lint before declaring done." With humans this already leaked. With an agent making dozens of decisions a minute, it leaks on the first distracted session. Hooks are where those rules stop being folklore and become an executable contract.

A hook, in Claude Code, is a shell command the harness fires automatically at lifecycle events of the session. They live in settings.json, not in the model's judgment. That's the part that matters: the agent does not decide whether to run the hook. The harness runs it, every time, regardless of what the model "remembered" to do. It's precisely because they're deterministic and outside the agent's discretion that they work as guardrails.

Before: validate and block

The most powerful event is PreToolUse. It runs before a tool executes and can block the action. A hook that exits with code 2, or returns JSON with permissionDecision: "deny", stops the call from happening and even tells the agent why. This is where "never let it go to main" lives: a matcher on Bash that inspects the command and denies any git push aimed at a protected branch. It's not a suggestion in the prompt. It's a locked door.

"A guardrail that depends on the model remembering isn't a guardrail - it's a hope."

The practical difference is brutal. Asking "please don't push to main" in CLAUDE.md works until the session gets long enough for that instruction to drift out of focus. PreToolUse doesn't forget, doesn't get distracted, and doesn't negotiate. Either the command passes the filter, or it simply doesn't run.

After: format and verify

PostToolUse runs after the tool has already executed - so it doesn't block, it reacts. The classic case is auto-format: a matcher on Write|Edit that grabs the just-edited file and runs prettier on it. The agent doesn't even need to know it exists; the code comes out formatted the team's way, every time. It's exactly the pattern I use here on the portfolio, with formatting wired into a post-edit hook.

The same event works for loud verification. Edited a test file? Run the test and inject the result back into context. Touched a schema? Trigger the type-check. The point isn't to make it pretty - it's to close the loop without depending on the agent "remembering" to validate before saying it's done.

* Rule of thumb

A good guardrail fails loudly and deterministically. If the only thing holding a rule in place is the model remembering it, it isn't a rule - it's an implementation detail waiting to go wrong.

Around: set the stage

There are also events that wrap the whole session. SessionStart runs when the conversation begins and is great for loading context the agent will need anyway: the current branch, the open issues, the state of the local database. Stop runs when the agent thinks it's done - and can prevent it from stopping if a gate didn't pass, handing the ball back: "lint is still failing, keep going". Before, after, and around: the three fence in the agent's autonomy without dictating what it does in the middle.

In the end, hooks are how you encode the team's culture into a versioned file - and one of the central pieces of the harness around the agent. The rules that used to live in a Slack channel, a poorly documented onboarding, or the memory of whoever's been around longest become a contract the agent signs with no say in the matter. And a contract that doesn't depend on goodwill is the only kind that survives production.

Liked this piece?

I can take these and other notes to your team as a workshop or recurring mentorship through Bamse's AI Mentorship program.

Talk about mentorship
Next Writing

What we discovered at Bamse's AI Committee in early 2026