Status: available for projects [email protected]
← Back to articles

Guardrails for AI Agents: Use Hooks, Not Just Rules

This weekend I was building a feature for a client with Claude Code, and I learned something that changed how I think about guardrails for AI development.

Until now, I put all my rules into markdown files. CLAUDE.md, rules files, guidelines for each agent. "Do not touch the migrations folder." "Never edit the .env file." That works most of the time. But the agent reads these rules as text, and text can be missed, forgotten in a long session, or simply misread.

Hooks run code, not suggestions

Claude Code supports hooks. A hook is a small script that runs automatically at specific moments: when a session starts, when you send a message, or before and after every action the agent takes. The script receives the details of that action as JSON and can let it through or block it.

That last part is the important one. A hook that runs before every file edit can compare the target path with a list of protected paths and stop the edit with a clear reason. The agent does not have to remember the rule. It simply cannot break it.

A simple example

I asked the agent to build the hook itself. Here is a simplified version. The configuration goes into .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-paths.js"],
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

And the script itself:

const fs = require('fs');

const input = JSON.parse(fs.readFileSync(0, 'utf8'));
const file = input.tool_input?.file_path ?? '';

const protectedPaths = ['.env', 'database/migrations/', 'config/auth.php'];

if (protectedPaths.some((path) => file.includes(path))) {
  console.error(`Blocked: ${file} is protected. Ask the developer to change it.`);
  process.exit(2);
}

process.exit(0);

Two details matter here. Exit code 2 is what blocks the action, and the message is sent back to the agent so it knows why. Any other error code only shows a warning and lets the action continue. The onFailure: "block" option makes sure that if the script itself crashes or goes missing, the edit is blocked instead of silently allowed.

It feels like architecture tests

If you work with Laravel, this will feel familiar. With Pest you can write architecture tests that check your code never uses dd or dump, that controllers stay thin, or that certain classes are never used in certain layers.

Markdown rules are like a style guide in a wiki. Hooks are like the test suite. They run every time, they behave the same way every time, and they fail loudly.

It lives in the repository

The settings file and the hook scripts are committed together with the code. Everyone on the team gets the same guardrails, and so does every agent session, without anyone having to set anything up on their own machine.

Where the limits are

Hooks are not a full sandbox. A hook that watches file edits will not see an agent changing the same file through a shell command, so you need to cover those paths too, for example with a second hook on shell commands or with deny rules in the permission settings. I see hooks as one layer next to permissions and normal code review, not a replacement for them.

Markdown rules still have their place. They explain why something matters. Hooks make sure it actually happens.

Conclusion

If you work with AI agents on a shared codebase, start small. Write one hook that protects your most sensitive paths, commit it, and see how it feels. For me, it turned a list of hopes into something I can actually rely on.

Email me CV ↓