Short lesson. Important lesson.
The static-layer lab has you create a sample-config.json file containing a fake API key. That’s on purpose. It’s bait. The bait is there because an agent will, given half a chance, copy a “real example” secret from one file to another file that’s about to be committed, and I want us to see the feedback loop fire on a fake before you ever deploy to a real project.
This is not hypothetical. I have personally watched Claude Code, Cursor, and Codex all commit credentials in the last year. Every time, the fix was the same: install gitleaks, configure the hook, and the mistake becomes impossible to repeat. Every time, the reason it happened in the first place was that secret scanning wasn’t installed.
Install it now. Before the agent commits something you actually have to rotate.
What Gitleaks does
Gitleaks is a single binary. It scans files (or git history) for patterns that match known secret formats: AWS keys, GitHub tokens, Slack tokens, Stripe keys, OpenAI API keys, private keys, JWTs, generic “looks like a secret” strings. When it finds one, it fails loudly with the file path, line number, and a redacted preview of the match.
It has two practical modes in this workshop:
- Full repository or history scan. Current Gitleaks releases expose this via commands like
gitleaks git ..., and older tutorials often showgitleaks detect .... Run the full scan once when you install it, and again in CI as a safety net. - Staged-content scan. In the completed static-layer version of Shelf, this is a repo-local helper script that snapshots the exact git index and runs
gitleaks diron that temporary directory. Fast. Use this in the pre-commit hook.
The combination catches both “the agent is about to commit a secret” and “a secret has already snuck in through some other path.”
In Shelf, this slot sits inside a wider static layer. The same lab that wires gitleaks also has you create or extend eslint.config.js, tsconfig.json, knip.json, lefthook.yml, .gitleaks.toml, and scripts/run-gitleaks-staged.ts, then run npm run lint, npm run typecheck, npm run knip, npm run test, npm run pre-push, and the lightweight npm run lint -- --quiet loop. Secret scanning is one layer in that stack, not a side quest.
Installing Gitleaks
Gitleaks is a Go binary, not an npm package. Install it with your OS package manager:
# macOS
brew install gitleaks
# Linux (various)
# See https://github.com/gitleaks/gitleaks#installingThere’s also a Docker image and a GitHub Action. I prefer the binary because it runs fast locally. Check gitleaks is on your $PATH:
gitleaks versionWiring it into the pre-commit hook
Add lint, format, and secrets commands to the pre-commit block in lefthook.yml:
pre-commit:
parallel: true
commands:
lint:
glob: '*.{ts,svelte,js,mjs,cjs}'
run: npx eslint --fix --max-warnings=0 {staged_files}
stage_fixed: true
format:
glob: '*.{ts,svelte,js,mjs,cjs,json,md,yml,yaml,css}'
run: npx prettier --write {staged_files}
stage_fixed: true
secrets:
run: npx tsx scripts/run-gitleaks-staged.tsIn Shelf, that script materializes the exact staged snapshot into a temporary directory and runs gitleaks dir on it. That is more reliable than depending on whichever staged-file flags your installed Gitleaks version happens to support this month. Note the deliberate lack of a glob field on the secrets command — the wrapper script re-enumerates the staged index itself, so handing it a pre-filtered {staged_files} list would double-filter.
Test it:
echo "AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" > fake.env
git add fake.env
git commit -m "testing secret scanner"You should see a gitleaks report and an aborted commit. Clean up:
git reset HEAD fake.env
rm fake.envIf the hook didn’t fire, double-check gitleaks is installed and the secrets command is wired into lefthook.yml correctly.
Running it on history, once
If you’re adding Gitleaks to an existing project, run a full scan over the repository or git history first. There are almost always findings.
# Current releases usually expose this as:
gitleaks git --redact
# Older tutorials may show:
gitleaks detect --redactFor each finding:
- Was it real? If yes, rotate the credential immediately. That’s step one, before anything else. A committed secret is compromised, period, even if nobody “found” it. Rotate the key, then deal with the git history.
- Scrub history. Use
git filter-repoorbfgto remove the secret from every past commit. This rewrites history, which means everyone on the team has to re-clone. Coordinate. - Document the incident. Not blame. Incident. “On this date, this credential was exposed, we rotated it, we scrubbed it from history, here’s the gitleaks rule that would have caught it.” This becomes the training data for why you have the hook.
If the finding is a false positive—a placeholder like sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx—add an allowlist entry for the specific pattern so gitleaks stops flagging it. More on the allowlist below.
The .gitleaks.toml config
Most of the time you don’t need a config file—gitleaks’ defaults are good. But two use cases justify one.
Allowlisting false positives. In the completed static-layer lab, you create a sample-config.json bait file with a deliberately fake API key so you can prove the scanner works on a known-bad input. Then, after you’ve seen the failure, you add sample-config.json to the allowlist so the team isn’t tripping the rule on the same intentional file forever after. If you want to allowlist the starter’s seeded workshop credentials too, use tests/data/ rather than an older fixture path. The config below is the “after the lab” version:
# .gitleaks.toml
[extend]
# Use gitleaks' default rules as a base.
useDefault = true
[allowlist]
description = "Global allowlist"
paths = [
'''sample-config\.json$''',
'''tests/data/.*''',
]The paths list skips files that match. sample-config.json is excluded after the lab demonstrates the bait getting caught. tests/data/ is an optional follow-up allowlist because the starter stores fake workshop credentials there for the seeding labs.
Adding custom rules. If your organization uses a proprietary token format that gitleaks’ defaults don’t cover, you can add a rule:
[[rules]]
id = "shelf-internal-token"
description = "Shelf internal service token"
regex = '''shelf_tok_[a-zA-Z0-9]{40}'''
tags = ["token", "shelf"]Gitleaks will now flag anything matching that pattern. Useful for “we have an internal secret format nobody else knows about.”
The .gitleaksignore file
Sometimes Gitleaks flags a specific finding that you genuinely want to keep. Maybe it’s an example in documentation, maybe it’s a literal constant that happens to match a secret pattern. For one-off exemptions, use .gitleaksignore:
# .gitleaksignore
path/to/file.md:generic-api-key:42The example above is already in the fingerprint format Gitleaks prints in its own output: path/to/file:rule-id:line-number. Copy the exact identifier straight from the report rather than hand-typing a bare line number—the fingerprint is how Gitleaks maps an ignore entry back to a specific finding. Much finer-grained than the path allowlist, which skips entire files.
Do not let .gitleaksignore become a dumping ground. Every line is a promise you made to keep ignoring a finding, and every promise should be checked periodically. Review the file on a schedule.
CI as the safety net
Even with a rock-solid pre-commit hook, run gitleaks in CI too. The pre-commit hook can be bypassed (--no-verify), can fail to install (new team member didn’t run bun install), can be misconfigured in a way that passes locally but not in CI. The CI run is the catch-all.
In GitHub Actions (we’ll go deeper on CI in CI as the Loop of Last Resort):
# .github/workflows/security.yml
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Organization repos also require a free license key since v2.0.0:
# GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}The official action is the easy path. It runs Gitleaks on every push and PR, fails the build on any finding, and gives you a clean CI gate even when someone bypasses the local hook. Personal repos can use only GITHUB_TOKEN; repos owned by an organization account additionally need GITLEAKS_LICENSE set to a free license key (obtainable from the gitleaks site).
Check the current licensing and setup requirements for gitleaks/gitleaks-action@v2 before you standardize on it for organization repos. If the official action is a bad fit for your plan, invoke the CLI directly in a shell step instead. The important part is the CI gate, not the wrapper.
The agent rules
## Secrets
- Never commit a real API key, access token, password, or private key
to this repository. Real secrets live in `.env.local` (gitignored)
or in the deployment environment's secret manager.
- Sample configuration files (`sample-config.json`, `.env.example`)
may contain placeholder values that look like credentials. Use
obviously-fake values like `your_api_key_here`, not values that could
be mistaken for real keys.
- Gitleaks runs in the pre-commit hook. If it flags your commit, do
not bypass it. Remove the secret and replace it with a placeholder.
- If you believe a gitleaks finding is a false positive, add an
allowlist entry in `.gitleaks.toml` with a comment explaining why.
Do not add to `.gitleaksignore` without a comment.The “obviously-fake values” rule is specifically to prevent the agent’s favorite move: copying AKIAIOSFODNN7EXAMPLE out of a tutorial and pasting it into a real config file, where gitleaks then flags it because it looks real. Use obviously-fake values that don’t pattern-match any known secret format. placeholder_value, replace_me, your_key_here—all fine, all unambiguously not real.
The 30-second version of this whole lesson
- Install gitleaks.
- Add the staged-snapshot helper script as a
secretscommand inlefthook.yml. - Run a full Gitleaks scan once on your existing history. Rotate anything real that it finds.
- Add the gitleaks GitHub Action to your CI as a safety net.
- Never bypass the hook. If gitleaks says it found a secret, it found a secret.
That’s the whole thing. Five steps, most of a Tuesday morning, and the problem of “the agent committed an API key” disappears from your life.
The one thing to remember
Secret scanning is one of the cheapest, highest-value things in the static layer. The hook catches mistakes before they’re public, the CI run catches mistakes the hook missed, and the allowlist lets you keep false positives out of the way. Install it this afternoon. Thank me later.