
1. Why agent commits need a local gate
A coding agent reads your .env to debug a failing test, writes the token into a fixture, and commits. Server-side push protection acts when the commit is pushed. By then the secret already sits in a local commit, possibly several, and an agent that commits in small steps can build on top of it before anyone looks. A local pre-commit hook stops the commit itself, which is the cheapest place to stop it: nothing to rewrite, nothing to rotate yet.
This guide sets up four things: a pinned Gitleaks hook on staged changes, a written policy for false positives, a CI check that catches commits that skipped the hook, and a drill that proves both layers block a planted fake key. Sanitizing what you send to a model is a separate problem, covered in Sanitizing Code and Data Before Sending to AI; this guide is about what the agent writes into Git.
One maintenance note before you adopt it. The current Gitleaks README opens with a warning that the project is feature complete, that new features are not being merged, that future releases will be security patches only, and that the maintainer is shifting focus to a successor project, Betterleaks. The commands below work today; put a review date on the tool choice.
2. Install via pre-commit with a pinned rev
The pre-commit framework is a Python tool (pip install pre-commit). Add this .pre-commit-config.yaml at the repository root:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
Then run pre-commit install, which writes .git/hooks/pre-commit. Two version details matter. The Gitleaks README example still pins rev: v8.24.2, so copying it verbatim pins an older release. And v8.30.1, released on 21 March 2026, was the newest release on the releases page when this guide was checked on 10 October 2026. A tag is readable but movable; pre-commit autoupdate --freeze moves to the latest tag and stores its commit hash with a # frozen: v… comment, which is the stricter pin. Treat updates to that line as reviewed changes, not something an agent bumps on its own.
The gitleaks hook id uses language: golang. Since pre-commit 3.0.0 the framework bootstraps Go if it is missing and builds the hook in an isolated GOPATH, so the first run is slow. If you would rather use a binary you installed and verified yourself, the hook file also defines gitleaks-system, and gitleaks-docker runs the official image.
3. Scan staged changes, and full history once
All three hook ids run the same command:
gitleaks git --pre-commit --redact --staged --verbose
In the v8.30.1 source, --staged makes Gitleaks read git diff -U0 --no-ext-diff --staged, which is the index. That is what gets committed, so it is the right input, but it has a consequence worth knowing: in our test, the same fake key in a modified but unstaged file produced scanned ~0 bytes and exit 0. --redact keeps the secret out of the hook output, which matters when an agent reads that output back into its context. Older snippets use gitleaks protect --staged; detect and protect have been deprecated since v8.19.0 and are hidden from --help.
The hook only sees new changes. Scan the existing history once, without --staged, which by default walks git log -p across all refs:
gitleaks git --redact --report-path gitleaks-report.json
Rotate anything real it finds: deleting the line in a later commit leaves the secret in every clone of the earlier one. If the remaining findings are reviewed test data, keep the report as a baseline. Run with --baseline-path gitleaks-report.json and later reports contain only new findings; in our run the second scan wrote an empty []. Exit code 1 means "leaks or error", so a broken config also fails the commit, which is the safer failure.
4. False positives: .gitleaksignore, gitleaks:allow and allowlists
Some suppression is inevitable, and an agent that hits a blocked commit will look for the fastest way past it. Decide in advance which mechanism is allowed where. Gitleaks offers three:
| Mechanism | Scope | Fails when | Suggested policy |
|---|---|---|---|
gitleaks:allow comment on the line | One line | Anyone can add it, including the agent | Test fixtures only, reviewed; consider --ignore-gitleaks-allow in CI |
.gitleaksignore fingerprint | One finding | The line moves, or the fingerprint form does not match the scan | Known historical findings, one entry per reviewed item |
[[allowlists]] in .gitleaks.toml | Paths, regexes, stopwords, commits | A broad pattern hides real secrets | Narrow paths with targetRules, changed only through review |
The fingerprint row has a trap we confirmed by running it. A finding in a commit has the fingerprint commit:file:rule:line; a staged finding has no commit, so its fingerprint is file:rule:line. Gitleaks checks the short form first, so leak-test.env:github-pat:1 suppressed the staged finding (exit 0), while the same finding with a commit prefix did not (exit 1). After one line was inserted above the secret, the short entry stopped matching and the hook blocked again. The README also labels .gitleaksignore experimental. Both points argue for config allowlists over fingerprints for anything that changes.
A config that keeps the built-in rules and allows one fixture directory for one rule:
[extend]
useDefault = true
[[allowlists]]
description = "Reviewed synthetic tokens in auth tests"
targetRules = ["github-pat"]
paths = ['''^tests/fixtures/auth/''']
Keep it as .gitleaks.toml at the repository root, where both the hook and CI find it without a --config flag. The path is an example; the point is that the exception names a rule and a directory instead of switching off detection for the repository. If you use CODEOWNERS or branch rules, put .gitleaks.toml, .gitleaksignore and .pre-commit-config.yaml under the same reviewers as your CI workflows.
5. CI check for bypassed hooks
A pre-commit hook is advisory. git commit --no-verify skips the pre-commit hook entirely, SKIP=gitleaks skips this one, and a fresh agent sandbox may never have run pre-commit install. In our test, --no-verify committed the fake key without complaint. CI is where you enforce it; the AI-assisted CI/CD guide covers the wider pipeline.
Do not just run pre-commit run --all-files in CI for this hook. Its command scans the staged diff, and a fresh CI checkout has nothing staged. We reproduced that: after the bypassed commit, the staged scan reported no leaks found and exit 0, while a scan of the commit range exited 1. Scan the commits in the pull request instead:
name: gitleaks
on:
pull_request:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install gitleaks 8.30.1 with a pinned checksum
run: |
curl -sSfL -o gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz
echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb gitleaks.tar.gz" | sha256sum -c -
tar -xzf gitleaks.tar.gz gitleaks
- name: Scan the commits in this pull request
run: ./gitleaks git --redact --verbose --log-opts="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
The checksum is the linux_x64 line from the release's gitleaks_8.30.1_checksums.txt, so a replaced download fails the job instead of running. fetch-depth: 0 makes both ends of the range available. The range scan reports the commit hash for each finding, which tells the author exactly which commit to drop before merging. Make the job a required status check, or it only reports.
6. Prove it with a fake key; what push protection adds
Run the drill in a scratch branch or a throwaway clone. Generate the key at run time so no token-shaped string ever lives in your docs or scripts:
printf 'GITHUB_TOKEN=ghp_%s\n' "$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 36)" > leak-test.env
git add leak-test.env
git commit -m "test: planted fake key"
The default github-pat rule matches ghp_ plus 36 letters and digits. Expected result: the commit is refused, the output shows RuleID: github-pat, Secret: REDACTED and leaks found: 1, and git log has no new commit. Then commit again with --no-verify, push the branch, open a pull request, and confirm the CI job fails on that commit. Delete the branch afterwards. Pass criterion: blocked locally, red in CI, and the key never appears unredacted in any log.
What we ran on 10 October 2026: Gitleaks 8.30.1 (Windows x64 release, checksum verified) with Git 2.56.0, using a native .git/hooks/pre-commit containing the hook command above. Every result quoted in this guide comes from that run. We did not run the pre-commit framework itself, the GitHub Actions workflow or GitHub push protection; those parts are taken from their documentation.
Push protection is the next layer, not a replacement. GitHub's 7 October 2026 changelog describes a fine-tuned model that reads surrounding code to find likely credentials, "including passwords without a recognizable token format", and an AI check in push protection that is in preview. Per the changelog, it needs GitHub Secret Protection or Advanced Security on GitHub Enterprise Cloud or GitHub Team, an administrator must enable it, and the opt-in checks consume GitHub AI Credits. The post gives no detection rates. Gitleaks rules are patterns with entropy thresholds, so an unformatted password is exactly the case where a context-aware check can add something. If anything gets past both layers, revoke and rotate the credential; removing the commit does not undo the exposure. For limiting what an agent can reach in the first place, see Sandbox an AI Coding Agent in a Dev Container.
Sources checked 2026-10-10
- Gitleaks repository — README, hook definitions, staged-scan source, fingerprints, allowlists and exit codes.
- Gitleaks releases — v8.30.1 date, assets and checksums.
- pre-commit — installation,
autoupdate --freeze, SKIP and golang hooks. - GitHub changelog, 7 October 2026 — AI secret detection in push protection, availability and billing.