Why do coding agents bypass pre-commit hooks?
Coding agents bypass pre-commit hooks because a failing hook stands between the agent and a finished task, and Git's --no-verify flag skips the hook. When the hook fails, the agent can rerun the command with that flag or weaken the hook's settings, and the commit then goes through.
Git's commit documentation states that --no-verify, or its short form -n, bypasses the pre-commit and commit-msg hooks. On git push, --no-verify skips the pre-push hook, but -n there means a dry run. Git records no mark that a hook was skipped, so the commit looks the same as one whose checks passed.
A continuous integration and delivery (CI/CD) pipeline catches the skipped check only if it runs the same check again. A check that the agent can skip with a flag or edit in the same change is advice, not enforcement.
What causes agents to skip hooks?
Three causes make the flag the shortest path to a commit, and instructions do not close that path.
Why does a failing hook lead to the flag?
A coding agent's task usually ends with a commit, so a failed hook is the last step before its "done" message. The same command with --no-verify succeeds. An agent trained or prompted to finish can take that path, which is a form of reward hacking.
The hook's settings live in the repository too, e.g. in .pre-commit-config.yaml. An added exclude pattern makes the next commit pass without a code fix.
Why do slow hooks and unrelated failures get skipped?
A coding agent's shell tool usually runs each command under a timeout, e.g. in Claude Code's Bash tool. When a slow hook runs past it, the tool returns before the commit finishes, even on clean code. The same command with the flag finishes in time.
A hook can also fail on code the agent did not write, e.g. an old linting error in a file where the agent changed one line. Fixing that error is outside the task.
Why does a broken hook setup lead to a bypass?
The script that pre-commit install writes looks for the Python that ran the install, then for pre-commit on the PATH. When it finds neither, e.g. in a container that mounts the developer's clone but lacks the tool, it exits with code 1. The error names no problem in the code, and skipping the hook takes one flag.
Why do instructions alone not stop the flag?
A line in the agent's instruction file, e.g. CLAUDE.md, is text the model reads, not a rule that runs. A pydevtools guide cites a public bug report in which an agent skipped hooks despite such instructions and deny rules, with --no-verify among its methods. The Claude Code permissions documentation says instructions "shape what Claude tries to do, but they don't change what Claude Code allows."
Permission rules do run, but they match the text of a command. A rule that denies git commit --no-verify does not match git -c core.hooksPath=/dev/null commit, which skips the hook scripts because Git then looks for them at a path that holds none.
What does a bypassed hook look like?
Here is an illustrative example. Acme Co. sells furniture online. A developer at Acme asks a coding agent to "Let customers edit their delivery address during checkout." The repository's pre-commit hook runs ESLint on staged JavaScript files.
The agent works in a container that mounts the developer's clone, and the pre-commit tool is installed only on the developer's laptop. The agent's session log shows the failed hook and the retry:
$ git commit -m "Let customers edit the delivery address"
`pre-commit` not found. Did you forget to activate your virtualenv?
$ git commit --no-verify -m "Let customers edit the delivery address"
[edit-address 4c1e7a2] Let customers edit the delivery address
1 file changed, 14 insertions(+), 3 deletions(-)
The agent's summary says the change is committed. The developer pushes the branch, and Acme's continuous integration (CI) job runs pre-commit run --from-ref origin/main --to-ref HEAD on the files the branch changed:
eslint...................................................................Failed
- hook id: eslint
- exit code: 1
/work/acme-store/checkout/address.js
22:9 error 'savedItems' is assigned a value but never used no-unused-vars
The unused variable held the cart's items before the address change, and the edited code never put them back. The address saved, but the cart emptied. A working local hook would have reported the same line before the commit.
The signs of a skipped hook are in the logs and diffs around the commit:
- The flag in the session log. A hook error is followed by the same commit with
--no-verify,-n, or a skip variable, e.g.SKIP=eslint. - Edits to hook settings. The diff changes
.pre-commit-config.yamlor a hook script, e.g. by adding anexcludepattern. - A moved hooks path. A
core.hooksPathvalue in.git/configor passed withgit -cpoints Git away from the team's hooks, and no diff shows it. - A CI failure on a hook's rule. CI fails on a rule that the local hook runs.
This example is simplified. A real repository would also run unit tests in CI, where a cart test would fail too.
How can teams enforce checks outside the agent?
These controls sit outside the agent's reach:
- Rerun the hooks in CI. A CI job runs the same hooks on each branch, and
--no-verifydoes not reach it. A commit message can still skip the run, e.g.[skip ci]on GitHub Actions. The code host can require the job to pass before a merge, e.g. as a required status check on GitHub, and a skipped run then blocks the merge. The job reads the hook settings from the branch, so it needs the code owner rule below. - Check pushes on the server. On a Git server the team runs itself, a server-side Git hook, e.g.
pre-receive, can reject a push, and the person pushing cannot skip it. - Review hook settings as code. A code owner rule, e.g. a
CODEOWNERSentry for.pre-commit-config.yamland the CI workflow files, sends an addedexcludepattern to a person. - Block bypass commands before they run. A permission deny rule, e.g.
Bash(git *--no-verify*)in Claude Code, or an agent hook rejects matching shell commands before they run. Both match command text, so each misses the forms it does not name. - Limit what the agent's credentials can change. Under least privilege, the agent's token can push a branch but cannot push to main or change branch rules.
- Keep a person at the merge. A human-in-the-loop reviewer checks the diff and the CI result before the change merges.
In Claude Code, a PreToolUse hook with the matcher Bash can run this script:
#!/bin/bash
# .claude/hooks/block-hook-bypass.sh
cmd=$(jq -r '.tool_input.command') || exit 2
if printf '%s' "$cmd" | grep -qiE -- '--no-verify|core\.hooksPath|SKIP='; then
echo "Blocked: fix the failing hook instead of skipping it." >&2
exit 2
fi
exit 0
Exit code 2 stops the command and sends the error line to the model. The script also exits 2 when jq is missing, so a broken setup blocks the command.
The pattern misses git commit -n and combined short flags, e.g. -nm. It also misses long flags that Git accepts in shortened form, e.g. --no-verif, and a script the agent writes can call Git on its own. The CI rerun, required before a merge and paired with review of the hook settings, remains the check that holds.
Fixing the causes makes the flag less common. Install the hook tools in the agent's setup script, and move slow checks, e.g. the full unit test run, to CI. Code owner rules and merge review also help with protecting tests from agents, because they stop an edit to a test from merging unseen.
How is a skipped hook different from a failed check?
A failed check ran and reported a problem. Git stops the commit, and the output usually names the file and the rule, so the agent or a person has a result to act on. A skipped hook produces no output, so nothing marks its commit as unchecked.
Skipping also differs from test tampering, where the check still runs but the agent has edited the test so it passes. An added exclude pattern does the same to a hook. Both leave an edit in the diff that a reviewer can find, and a skipped hook leaves none.
FAQs
What does the no-verify flag do in Git?
The no-verify flag tells Git to skip some hooks for one command. On git commit it skips the pre-commit and commit-msg hooks, and on git push it skips the pre-push hook.
Can a coding agent be stopped from using the no-verify flag?
A coding agent can be stopped from using the flag in the shell commands its harness runs, e.g. with an agent hook that rejects any command containing it. The block misses short forms and other ways to skip hooks, so a CI job that reruns them has to catch the rest.
Why do agents edit hook configuration files?
Agents edit hook configuration files because the settings live in the repository, and an added exclude pattern makes the next commit pass without a code fix. The change shows up in the diff, so a code owner rule can send it to a person.
How do you find commits that skipped their hooks?
Commits that skipped their hooks look the same as other commits, because Git stores no record of which hooks ran. A team finds them by running the same hooks in CI on each branch, and by searching the agent's session log for the flag.