Git hooks for AI engineering

By

Use Git hooks as deterministic guardrails for AI coding agents, with fast checks before commits, stronger checks before pushes, and CI as the final gate.

~~~

AI coding agents can change a lot of code very quickly.

That speed is useful. It also makes small mistakes easier to miss. An agent might forget a test, stage an unrelated file, or commit code that breaks a project rule.

Git hooks give us a deterministic checkpoint.

A hook runs when something happens in Git. It does not matter whether the change came from Cursor, Codex, Claude Code, a desktop Git client, or a person using the terminal.

This makes hooks especially useful in projects built with AI agents.

Why Git hooks fit AI engineering

We already give agents instructions. We might have an AGENTS.md, project rules, a prompt, or a detailed plan.

Those instructions guide the agent. They do not guarantee the final result.

A Git hook checks what the agent is about to commit. It can run the same command every time and reject the commit when that command fails.

I use this split:

Hooks turn a few important expectations into executable checks, while review still handles everything else.

Git hooks and agent hooks are different

The word hook can mean two different things here.

An agent hook runs around an agent event. For example, it might run before a shell command or after a tool call.

A Git hook runs around a Git event:

pre-commit
commit-msg
post-commit
pre-push

Agent hooks know about the agent, while Git hooks know about the repository.

If several agents and tools work on the same project, Git is the shared boundary. Every tool eventually creates a commit or pushes one.

I prefer Git hooks for repository checks because every tool reaches that boundary.

Where Git hooks live

Git keeps local hooks inside its hooks directory.

Ask Git for the exact path:

git rev-parse --git-path hooks

Do not assume the path is always .git/hooks.

You might see files ending in .sample inside that directory. Git only runs a hook when it has the exact hook name without .sample.

The file must also be executable:

chmod +x .git/hooks/pre-commit

If a hook never runs, check its filename and permissions first.

Start with a fast pre-commit check

A pre-commit hook runs before Git creates a commit.

For an agent-built JavaScript project, I might start with:

#!/bin/sh

git diff --cached --check || exit 1
npm run check:commit

Save this as .git/hooks/pre-commit, then make it executable.

The first command checks the staged patch for whitespace errors and unresolved conflict markers. The second calls a normal project command.

The project might define that command like this:

{
  "scripts": {
    "check:commit": "npm run lint && npm run test:fast"
  }
}

The exact checks depend on the project. Keep them fast enough to run on every commit.

When a command exits with status 0, Git continues. A non-zero status stops the commit.

This creates a useful agent loop:

  1. the agent changes the code
  2. the agent tries to commit
  3. the hook finds a problem
  4. the agent fixes it
  5. the commit succeeds

The failure message becomes feedback for the agent.

Make failures easy for an agent to fix

Agents work better when a check reports a concrete problem.

This message is not useful:

Validation failed

This one is:

src/auth/session.ts imports src/ui/modal.ts
Server code cannot import browser components.

Whenever possible, print the file, rule, and expected fix.

This matters for people too. A clear hook saves everyone from opening the script to understand what failed.

Put project rules in normal scripts

I keep Git hook files short.

The hook should call a project command:

#!/bin/sh

npm run check:commit

The actual checks belong in package.json, a shell script, or a small program under scripts/.

This gives us one command that works everywhere:

npm run check:commit

An agent can run it before committing. The Git hook can run it again. CI can use the same command.

We avoid hiding important project logic inside .git.

Use hooks to protect architectural boundaries

Linting and tests are the obvious checks. Hooks can also protect rules specific to your project.

For example, a project might require:

Turn rules like these into scripts:

#!/bin/sh

npm run check:boundaries &&
npm run check:generated &&
npm run test:fast

This is where hooks become more valuable than another paragraph in an agent prompt.

The prompt explains the architecture. The script proves that a change still respects part of it.

Do not try to encode every preference. Pick rules that are objective, quick, and expensive to forget.

Check the staged snapshot

Git commits the staged snapshot, also called the index.

The file in your editor might contain more changes than the staged version. This happens with partial staging. It also happens when several tasks share one working directory.

See the exact patch with:

git diff --cached

That output is what the commit will contain.

My advice is to keep pre-commit hooks read-only. Let them report problems without rewriting files.

Automatic formatting can modify the complete file. That may mix staged work with changes that were supposed to remain unstaged.

If you use lint-staged or another tool that rewrites staged files, test partial staging carefully.

Catch secrets before an agent commits them

Agents sometimes inspect configuration while debugging. A secret can then end up in a patch by mistake.

A pre-commit hook is a good place to run a secret scanner:

#!/bin/sh

npm run secrets:check

Use a scanner designed for secrets. A small regular expression will miss many real credentials and flag harmless strings.

The hook reduces risk, but it is not the security boundary. Run secret scanning in CI and on the remote repository too.

If a real secret enters Git history, removing the line is not enough. Revoke and replace the credential.

Require useful commit messages

Agents can produce vague subjects like update files or fix stuff.

The commit-msg hook receives the commit message file as its first argument. We can reject known bad subjects:

#!/bin/sh

message_file=$1
subject=$(sed -n '1p' "$message_file")

case "$subject" in
  "update files"|"fix stuff"|"changes")
    echo "Write a commit subject that explains the change"
    exit 1
    ;;
esac

Save this as .git/hooks/commit-msg.

I would not create a complicated grammar for commit messages. A hook cannot decide whether a message accurately describes the patch.

It can catch empty messages and subjects from a short denylist. Review still handles meaning.

Each Git hook receives different information. Check the Git hooks documentation before adding a hook you have not used before.

Run wider checks before a push

A pre-push hook runs before Git sends commits to the remote.

This is a good place for checks that take longer than the commit loop:

#!/bin/sh

npm test &&
npm run build

An agent may create several small commits before pushing. Running the full suite once at push time keeps each commit fast.

I still avoid very long browser suites here. Slow hooks teach people and agents to bypass them.

Run expensive integration and browser tests in CI.

Record completed agent work after a commit

The post-commit hook runs after Git creates the commit.

At this point, returning an error cannot stop or remove that commit. This makes post-commit useful for logs and notifications.

Here is a small local commit log:

#!/bin/sh

subject=$(git log -1 --pretty=%s)
printf '%s\n' "$subject" >> .git/commit-log.txt

The file stays inside .git, so it does not appear as a project change.

I use a more complete version to log every Git commit to one plain text file. Git records the completed event no matter which agent made the commit.

This gives me a compact record of finished work without saving prompts, transcripts, or tool output.

Share hooks with every agent environment

Files inside .git/hooks are local. Git does not include them in a clone.

For shared hooks, keep them in a tracked directory:

.githooks/
  pre-commit
  commit-msg
  pre-push

Then configure the clone:

git config core.hooksPath .githooks

Check the setting with:

git config --get core.hooksPath

Every local or remote agent environment needs this setup. A tracked hook does nothing until Git knows where to find it.

You can add the command to the project setup script. For a JavaScript project, Husky can also install and share hooks.

I use core.hooksPath for a few short shell scripts. I would use Husky when a JavaScript team already depends on its workflow.

Read shared hooks before enabling them. A cloned hook is executable code from the repository.

Tell agents not to bypass hooks

Git lets a commit skip pre-commit and commit-msg:

git commit --no-verify -m 'skip local checks'

A push can skip pre-push too:

git push --no-verify

Your agent instructions should say not to use --no-verify.

That instruction is still not enforcement. An agent or person can bypass the hook, delete it, or work in a clone where hooks were never configured.

Required rules must also run in CI so they cannot be bypassed this way.

Use bypasses only when a hook itself is broken and you understand the risk. Fix the hook immediately afterward.

Hooks and CI have different jobs

Local hooks give the agent fast feedback. CI verifies the result in a clean environment.

I often run the same command in both:

npm run check:commit

The hook catches a mistake before it leaves the machine. CI catches it again if the hook was missing or skipped.

Branch protection can require CI to pass. It cannot require a local hook that the remote never sees.

Hooks help the workflow. CI enforces the shared standard.

Know what hooks cannot catch

A Git hook only runs at a Git event, so it cannot help while an agent keeps uncommitted changes. It also cannot tell whether the implementation solves the right problem or review a misleading test that passes.

Hooks also struggle with subjective rules. They cannot reliably decide whether code is clear, an abstraction is justified, or a user interface feels right.

Use hooks for deterministic evidence:

Use human or agent review for judgment.

How I use Git hooks with AI agents

I keep the commit hook quick. It checks the staged patch, lint, types, and fast tests.

The push hook runs the full test suite and production build. CI repeats required checks on a clean machine.

I also use a post-commit hook for my work log. It records successful commits from every tool I use.

I place a few deterministic checks at the point where agent work becomes repository history. Adding more hooks is not the goal.

If staging and commits are still new to you, start with my free Git course. Hooks make more sense once the basic Git workflow feels familiar.

Tagged: Git · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about git: