Writing PreToolUse hooks that keep an agent in scope
Telling an agent “only edit files in this module” works most of the time. In a regulated codebase, most of the time is not good enough. Claude Code hooks let us turn that instruction into a rule the agent cannot break. This note walks through how we approach them.
What a PreToolUse hook is
Hooks are commands that Claude Code runs at specific points in a session. A PreToolUse hook runs after the agent has decided to use a tool, such as editing a file or running a shell command, and before the tool actually runs. The hook receives a JSON description of the tool call on standard input and decides whether the call may go ahead.
If the hook exits with code 2, the tool call is blocked and whatever the hook wrote to standard error is passed back to the agent as the reason. The agent reads that reason and adjusts its approach. That feedback loop is what makes hooks practical: the agent does not just fail, it learns the boundary.
Configuration
Hooks are configured in Claude Code’s settings file. A matcher selects which tools the hook applies to:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./policy/scope-check.sh" }]
}
]
}
}
A scope check
The simplest useful hook rejects edits outside an allowed directory. A sketch in shell:
#!/usr/bin/env bash
set -euo pipefail
path=$(jq -r '.tool_input.file_path // empty')
allowed="$(realpath "$MODULE_DIR")/"
target=$(realpath -m "$path")
if [[ "$target" != "$allowed"* ]]; then
echo "Edit blocked: $path is outside $MODULE_DIR. Stay within the module." >&2
exit 2
fi
Two details matter. Paths are resolved before comparison, so ../ tricks and symlinks do not escape the check. And the error message tells the agent what to do instead, not only what it did wrong.
Policies we plan to enforce
- Scope. Edits only inside the assigned module and its test directory.
- Dependencies. Changes to build files are checked against an approved list of libraries and versions.
- Secrets. Content that matches credential patterns is rejected, and reads of known secret locations are blocked.
- Commands. Shell commands are restricted by the tool allow-list first; hooks add argument-level checks where the allow-list is too coarse.
- Protected tests. Golden-master fixtures are read-only. An agent that cannot make a test pass must not be able to change the expected output instead.
Defence in depth
Hooks are one layer. Tool allow-lists limit what the agent can attempt at all. Worktrees and file permissions limit what it can reach. Branch protection limits what can be merged. And every blocked attempt is logged, which gives reviewers useful signal: an agent that hit the scope boundary twenty times on one task probably had a badly defined task.
Testing the hooks themselves
Policy code deserves its own tests. We keep a set of recorded tool-call payloads, both allowed and forbidden, and run every hook against them in CI. A change to a policy that silently allows something it used to block should fail a build, not be discovered in an audit.