Make AI Coding Assistants Follow Engineering Rules with Hooks
Project docs can describe coding standards, but they only advise an AI coding assistant. This article explains why those reminders fail, how event-driven hooks add deterministic control, and how to design triggers, matchers, and actions for formatting, checks, sensitive-file protection, and notifications.
Why Documented Rules Are Not Reliable Enough
Teams often put coding standards, directory conventions, and cautions into project documentation, expecting an AI coding assistant to read them before starting and follow them at every step. That practice is useful, but it is only advisory. The model can load the rules into its context without guaranteeing that it will apply them on every action. As tasks grow more complex and context grows longer, rules are more likely to be skipped.
Typical failures include:
- The project requires a typed language, but generated code omits type declarations.
- The project expects a static check after every change, but after several rounds of edits the check is skipped.
- The project forbids reading sensitive configuration files, yet while debugging the model opens them to gather context.
These failures do not mean the model is unwilling to follow rules. Compliance depends on how the model allocates attention across its context, and that allocation is unstable. Without an external guarantee, whether a rule is followed is largely a matter of chance.
An office reminder to save energy can raise awareness, but it cannot ensure that the air conditioner is turned off. A definite result needs a mechanism such as an automatic power cutoff.
The Basic Idea of Deterministic Control
Deterministic control means that when a certain event occurs, the runtime automatically performs a predefined action. Whether the action runs depends on the event and the rule, not on whether the model remembers the rule.
The two approaches differ as follows:
| Dimension | Documented constraint | System-level constraint |
|---|---|---|
| Who enforces it | The model decides | The runtime executes it automatically |
| Reliability | Depends on attention to context | Corresponds one-to-one with the event |
| Best fit | Style, preferences, and background | Formatting, checks, permissions, notifications, and other actions that must happen |
| When it fails | The rule is ignored and the miss is hard to notice | Logs and exit status can locate the failure |
The two are not mutually exclusive. Documentation explains why a practice exists and how to do it well. System-level constraints ensure that required actions actually occur. In practice, move rules that must run to the system layer, and leave content that needs understanding and judgment in the documentation.
The Three Parts of a Hook
A complete hook rule has three parts.
1. Event: when it fires
The event is the trigger point. Common categories include:
- Before a tool runs: fires before the model calls a tool, which suits permission checks and argument validation.
- After a tool runs: fires after the tool finishes, which suits formatting, checks, and logging.
- When a task ends: fires after the model finishes a reply, which suits notifications or a result summary.
Event names differ across tools. Design around the stage where you need to intervene, rather than memorizing a specific name.
2. Match condition: under what circumstances it fires
The matcher filters events. Events happen often, but not every one needs an action. Match conditions usually include:
- Operation type, such as file edits and file writes only.
- File path or extension, such as specific file types under the source directory.
- Command content, such as blocking commands that contain a class of high-risk arguments.
More precise matchers mean fewer false triggers and lower cost. A matcher that is too broad runs an expensive check on every edit, and teams soon turn the hook off.
3. Action: what happens after it fires
The action is usually a command or a script. Typical actions include:
- Running a formatter to keep a consistent style.
- Running a static check to catch potential errors.
- Refusing access to a sensitive file and printing the reason.
- Showing a desktop notification when a task is complete.
Think of an access-control door: the event is “someone wants to enter,” the matcher is “cardholders only,” and the action is “open the door if verification succeeds, otherwise raise an alarm.” The rule is complete only when all three are combined.
Typical Uses
In real projects, these cases are the best fit for hooks.
Format after an edit. Whenever the model changes a source file, a formatting command runs immediately so the style stays aligned with the project. These actions are short and idempotent, which makes them the best place to start.
Run checks after a change. Static checks can find unused variables, type mismatches, and similar issues. A hook can run the check after a modification and feed the result back to the model so it can fix the problem in the same task.
Protect sensitive files. Before a tool runs, inspect the target path and block environment files, key files, and deployment configuration. This is a security requirement and cannot rely on a written reminder alone.
Notify when a task finishes. After a long-running task, a hook sends a notification so the developer does not have to watch the screen.
Validate before commit. Before the version-control system accepts a commit, run tests or check scripts and block the commit if they fail. These rules can sit alongside an existing continuous-integration pipeline as a second line of defense.
Example Configuration and Script
The following examples illustrate the design only. The configuration syntax is schematic and is not the real syntax of any specific tool. When you implement a hook, follow that tool’s official documentation.
# Illustration: format source files after an edithooks:- name: format-after-editevent: post_tool_usematcher:tools: [edit, write]paths: ["src/**/*.js", "src/**/*.ts"]action:type: commandrun: "./scripts/format.sh"timeout_seconds: 30- name: protect-env-filesevent: pre_tool_usematcher:tools: [read, edit, write]paths: ["**/.env*", "**/secrets/**"]action:type: blockmessage: "Access to sensitive configuration files is blocked. Use placeholders or explanatory documentation instead."
A formatting script can stay minimal: read the file path, call the formatter the project has agreed on, and return a clear exit status.
#!/usr/bin/env bash# Illustration: formatting script that takes a file path as an argumentset -euo pipefailFILE_PATH="${1:?Missing file path argument}"# Process only existing regular files, not directories or deleted filesif [[ ! -f "$FILE_PATH" ]]; thenecho "Skip: file does not exist -> $FILE_PATH" >&2exit 0fi# Call the formatter defined by the project; the command name comes from project configurationproject-formatter --write "$FILE_PATH"
A few details matter. Quote the path so spaces or special characters do not break the command. Check that the file exists before acting on it. Write errors to standard error and exit with a non-zero status so the calling system can recognize failure.
Design Principles
Least privilege. A hook should do only what it must. A formatting hook should not have network access, and a check hook should not modify source code.
Idempotence. Repeating the same action should produce the same result. Formatting does this naturally. Notification actions need deduplication.
Bound the runtime. Hooks block later work. Checks that take more than a few tens of seconds belong before commit or in continuous integration, not after every edit.
Define failure semantics. Decide in advance whether a failed hook blocks the operation or only logs and continues. Security rules usually block. Formatting rules can warn instead.
Keep logs. Record each trigger, match result, and execution result so false blocks and performance problems can be investigated.
Common Mistakes
Mistake 1: Treating hooks as a replacement for code review. Hooks can enforce deterministic rules such as formatting, checks, and permissions, but they cannot judge whether business logic is correct. They supplement review; they do not replace it.
Mistake 2: Adding as many rules as possible. Every hook adds execution cost and debugging difficulty. Configure hooks only for rules that truly need a deterministic guarantee, and leave the rest in documentation.
Mistake 3: Ignoring re-entrant triggers. Formatting changes a file, and that change can fire the formatting hook again. A poor design creates an infinite loop. Limit the match scope, fire only when the tool call comes from the model, or have the script detect whether the file contents actually changed.
How to Roll It Out
Move in this order to reduce risk:
- Inventory the rules. List every team standard and mark which ones must be guaranteed and which ones are only advice.
- Pilot one low-risk rule first, such as formatting after an edit.
- Write the script and verify it locally by hand, confirming that the exit status and output match expectations.
- Configure the matcher and watch how often it fires over a period of time, so you can avoid false and excessive triggers.
- Extend gradually to checks, permission protection, and notifications, and mirror the critical rules in continuous integration.
- Review the hook list regularly and remove rules that are obsolete or no longer needed.
Once rules move from documentation to the system layer, quality expectations shift from “we hope people remember” to “the process makes it happen.” Execution becomes observable and measurable, and failures are easier to diagnose.
