Pre-commit hooks for AI-agent reposWhat 2010s setups miss. What to add.
Your pre-commit hooks were built for humans typing code. An AI agent doing 900-line commits at 2am is a different problem. The existing stack has real gaps.
-
01
Trigger
git commit
agent or human stages a commit
any branch -
02
Step
hook scan
gitleaks + state-file check run on staging area
< 2s typical -
03
Result
block or pass
commit halted if agent state present
.claude/ · .cursor/ · .aider/
TL;DR· the answer, in twenty seconds
What: Lint and test pre-commit hooks don't catch secrets or agent state files. AI agents generate both at volume, and they commit without thinking twice about what's staged.
Fix: Add gitleaks to .pre-commit-config.yaml, a custom bash hook that blocks .claude/, .cursor/, .aider/, and .codex/ from staging, and mirror both checks in CI where --no-verify can't reach them.
Lesson: Pre-commit hooks reduce friction but can be skipped. Any check that matters for security has to live in CI, not only on the local filesystem.
In 2015, adding ESLint and a test runner to your pre-commit hooks was considered solid hygiene. It was. The human developer who wrote the code knew roughly what was staged. A hook that caught lint errors and failing tests before a push was usually enough to prevent the obvious embarrassments.
An AI coding agent changes the threat model. The agent stages files it created, modified, or was told to include. It does not distinguish between application code and its own state files. It will commit .claude/settings.local.json if you don't stop it. It will dump a 2,000-line file from a single refactor if that's what the task required. Knostic found Claude Code's state file in roughly 1-in-13 npm packages they scanned in February 2026, and the GitGuardian 2026 State of Secrets Sprawl report found that AI-assisted commits leak secrets at about 2x the rate of baseline human commits.
The pre-commit hooks most teams have don't address any of this.
What to know in 60 seconds
- Lint and test hooks are not useless, but they solve a different problem than what AI agents introduce.
- The two new categories you need: secret detection and agent state file blocking.
--no-verifybypasses all local hooks. CI is the actual enforcement layer.- Gitleaks has a pre-commit integration that works against staged content, not just committed history.
- Agent state directories (
.claude/,.cursor/,.aider/,.codex/) belong in.gitignoreand in a pre-commit block. Both.
What 2010s hooks don't catch
A typical 2024 .pre-commit-config.yaml runs a linter, maybe a formatter, sometimes a short test suite. Nothing in that stack looks for secrets in staged content. Nothing blocks agent-specific directories. Nobody designed it to handle a tool that writes into your project folder as a side effect of doing its job.
The failure modes in agent-era repos fall into three buckets.
Agent state files. Claude Code writes .claude/settings.local.json. Cursor writes into .cursor/. Aider leaves .aider.history and .aider.chat.history.md. Codex has its own working directory. None of these are in the default .gitignore that git init or most project templates generate. If the agent stages them and your hooks don't block them, they go into the commit. From there they can reach a registry, a Docker image, or a deployed artifact.
Secrets in generated code. An agent asked to scaffold a new integration will sometimes hardcode a credential it found in context: a database URL from a conversation, an API key from your shell environment. It's not malicious. The agent pattern-matches on examples. If your .env was readable when the agent ran, the value might appear in a test fixture or a generated config file. Lint won't catch it.
Large accidental diff. Agents can regenerate entire files when asked to make a small change. A task like "add error handling to the auth module" can produce a 1,400-line diff if the agent decided to rewrite the whole file. That's hard to review and easy to miss. It's also a common vector for introducing subtle changes that weren't requested.
The 2026 hook stack
Start with the standard pre-commit framework. If you're not using it, install it once:
pip install pre-commit
# or
brew install pre-commit
Then configure it. Here's a .pre-commit-config.yaml that covers the gaps:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
args: ['--maxkb=500']
- id: check-merge-conflict
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
- repo: local
hooks:
- id: block-agent-state
name: Block agent state files
entry: .git/hooks/block-agent-state.sh
language: script
always_run: true
pass_filenames: false
The check-added-large-files hook at 500 KB catches agent-generated file dumps before they hit the branch. Adjust the threshold for your project type, but 500 KB is a reasonable starting point for most codebases.
The gitleaks hook
Gitleaks scans staged content for over 150 secret patterns: AWS keys, GitHub tokens, Stripe keys, Anthropic API keys, database connection strings, and more. It runs against the diff that's about to be committed, not the full repo history. That matters because agents can introduce a secret in a file they touched, not in a file they created from scratch.
The gitleaks hook above uses the pre-commit integration from the official gitleaks repo. It requires no additional configuration for most projects. If you need to allowlist a test fixture that contains a fake key, add a .gitleaks.toml:
[allowlist]
description = "test fixtures"
paths = [
"tests/fixtures/fake_credentials.json"
]
Don't allowlist real credential patterns. Use a secrets manager instead.
Blocking agent state files
The gitleaks hook catches secrets in file content. It does not block an agent state file that contains no secrets, or one that uses an encoding gitleaks doesn't recognize. You want a separate, explicit block on the directories themselves.
Create .git/hooks/block-agent-state.sh:
#!/usr/bin/env bash
BLOCKED_PATHS=(
".claude/"
".cursor/"
".aider/"
".codex/"
".claude/settings.local.json"
"settings.local.json"
)
staged=$(git diff --cached --name-only)
for path in "${BLOCKED_PATHS[@]}"; do
if echo "$staged" | grep -q "^${path}"; then
echo "pre-commit: blocked staged agent state file: $path"
echo "Add $path to .gitignore and unstage it with: git restore --staged $path"
exit 1
fi
done
exit 0
Make it executable:
chmod +x .git/hooks/block-agent-state.sh
This script is separate from the pre-commit framework because .git/hooks/ scripts run even without pre-commit install. If you want it managed by the framework instead, the local hook entry in the config above handles that.
Detecting AI-authored commits in prepare-commit-msg
Claude Code, Codex CLI, and some agent wrappers pass -m to git commit directly. That's detectable. A prepare-commit-msg hook can warn you when a commit message was auto-generated without a human in the loop.
Create .git/hooks/prepare-commit-msg:
#!/usr/bin/env bash
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"
# COMMIT_SOURCE is "message" when -m was used
if [ "$COMMIT_SOURCE" = "message" ]; then
msg=$(cat "$COMMIT_MSG_FILE")
# Check for patterns common in agent-generated commit messages
if echo "$msg" | grep -qiE "^(feat|fix|refactor|chore|add|update|implement|generate).*(\(.*\))?:"; then
echo ""
echo "pre-commit: this commit message looks auto-generated (conventional commit pattern + -m flag)"
echo "Review staged content before continuing."
echo ""
fi
fi
Make it executable:
chmod +x .git/hooks/prepare-commit-msg
This doesn't block anything. It adds a line to the terminal output when the commit message pattern matches what agents typically produce. The developer decides whether to continue. For teams that want a hard block, change the exit code to 1 and add a --force escape hatch.
License headers for generated code
If your policy requires license headers on all source files, an agent that creates new files won't add them. The pre-commit framework has hooks for this:
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
- id: insert-license
files: \.py$
args:
- --license-filepath
- .license-header.txt
- --comment-style
- '#'
This is optional and policy-dependent. Add it if your org requires it; skip it if you don't have a license header policy.
CI mirror is required
git commit --no-verify bypasses every hook you just configured. Any developer on the team can use it. Any agent wrapper that passes --no-verify to speed up commits will use it. Pre-commit hooks are a development convenience, not a security control.
The same checks have to run in CI, where --no-verify has no effect.
Hooks scan for secrets that entered the environment and ended up in a file. A broker like hasp addresses the earlier step: if the agent never received the raw value, there is nothing for gitleaks to find and nothing for the state-file block to stop. The hooks below are still worth running regardless, because they catch cases where secrets arrive through paths other than the agent session.
For GitHub Actions, add a job that runs gitleaks on the push:
name: Secret scan
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: $
For the agent state file check, add a step that runs before any build or deploy job:
- name: Block agent state files
run: |
if find . \( -path "./.claude" -o -path "./.cursor" -o -path "./.aider" -o -path "./.codex" \) -print -quit | grep -q .; then
echo "Agent state directory found in repo tree"
exit 1
fi
Run this on every PR and every push to main. The pre-commit hooks reduce noise during development. The CI jobs are the actual gate.
The part teams usually get wrong
Most teams assume pre-commit is sufficient because "we're not that kind of team" or "our agents are carefully supervised." Neither holds at scale.
Agent supervision degrades. The first week a team uses Claude Code or Cursor, developers review every commit carefully. By week six, they're merging agent PRs after a skim. By week twelve, the agent has merge permissions. The review attention that caught problems early is gone.
The --no-verify reflex exists. Any developer who has fought a flaky pre-commit hook has muscle memory for --no-verify. That same reflex fires when the agent is blocked. The hook you added for agent state files gets bypassed the first time it interrupts a fast-moving session. Without CI enforcement, the bypass sticks.
.gitignore and pre-commit are not the same control. Adding .claude/ to .gitignore stops git from tracking new files in that directory. It does not protect files that were already tracked. If an agent adds a new file to .claude/ that doesn't match an existing ignore pattern, git status will show it as untracked but git add . will still stage it. The pre-commit hook is the safety net for cases where .gitignore patterns are incomplete.
A realistic threat model treats pre-commit as a first layer that catches most mistakes at low cost, and CI as the enforcing layer that catches what pre-commit misses or what was bypassed.
A checklist you can paste into a PR
## AI-agent pre-commit checklist
- [ ] .claude/, .cursor/, .aider/, .codex/ in .gitignore
- [ ] gitleaks hook in .pre-commit-config.yaml (v8.18.4+)
- [ ] check-added-large-files hook (--maxkb=500 or project-appropriate)
- [ ] block-agent-state.sh present and executable in .git/hooks/
- [ ] pre-commit install run in repo (hooks registered)
- [ ] gitleaks job in CI (runs on push + PR)
- [ ] Agent state file check in CI (blocks if directories present)
- [ ] .gitleaks.toml allowlist covers only fake/test credentials
- [ ] prepare-commit-msg hook warns on auto-generated messages
- [ ] License header hook added if org policy requires it
- [ ] Confirmed --no-verify is not used in CI scripts or agent wrappers
Run this against every repo where an agent has write access, not just new ones.
What this means for your stack
The steps above are a self-contained setup you can ship in an afternoon. They reduce the probability that an agent commits a state file or a leaked secret to your main branch. They don't eliminate the problem. An agent running in a CI environment, an agent with write access to multiple repos, or an agent that stages content before hooks run are all cases where local hooks don't help.
The deeper fix is runtime-level: secrets stay out of the agent's environment entirely, so there's nothing to scan for in the first place. hasp is one working implementation of this model. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, hasp project bind to register a project profile, and the agent receives a reference instead of a live credential. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
Whether or not you add a broker, the hook stack above is worth shipping. Gitleaks plus a state-file block plus CI mirroring is a material improvement over lint-only hooks for any repo where an agent is committing.
Sources· cited above, in one place
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.