| name | security-gitleaks |
| description | Set up gitleaks secret-scanning on a repo. Scans history for existing leaks first — stops if dirty, because installing CI on top of a polluted history makes CI permanently red. If history is clean, scaffolds .gitleaks.toml, a local pre-commit hook, and a pinned CI workflow that scans both git history and working tree. User-triggered only — activate when the user invokes `/security-gitleaks`. |
security-gitleaks
You are adding gitleaks secret-scanning to the current repo. The order matters:
- Baseline scan first. If
gitleaks already finds things in HEAD or in history, stop. Adding a pre-commit hook locks the user out of committing legit work until those findings are resolved; adding CI turns the default branch permanently red. The user has to triage (rotate? scrub history? allowlist a false positive?) before any of this is installed.
- Config, hook, workflow — in that order, once the baseline is clean (or the user has explicitly allowlisted what's there).
Do not commit on the user's behalf. Write the files, show diffs, let them stage and commit.
When to use
User-triggered only. Activate when the user invokes /security-gitleaks. Do not self-activate on related natural-language phrasing ("add gitleaks", "set up secret scanning", a mention of a leaked credential, etc.) — surface the slash command to the user instead and let them decide.
Hard prerequisites — check before doing anything
git rev-parse --show-toplevel >/dev/null 2>&1 \
|| { echo "Not a git repo. Abort."; exit 1; }
command -v gitleaks >/dev/null 2>&1 \
|| { echo "gitleaks not installed. Run: brew install gitleaks"; exit 1; }
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null \
|| { git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' | grep . ; } \
|| echo "main"
If gitleaks isn't installed, instruct the user to run brew install gitleaks (or the platform equivalent — see https://github.com/gitleaks/gitleaks#installing) and re-invoke. Don't try to install it yourself; this is a tool the user runs daily, they should know what's on their machine.
Phase 1 — Baseline scan
Run two scans. Both, every time — they cover different surfaces:
gitleaks git --redact --verbose .
gitleaks dir --redact --verbose .
--redact matters: without it, a matched secret is echoed to your terminal and shell history. Always pass it.
If the baseline is clean
Proceed to Phase 2. Tell the user briefly: "Baseline scan found 0 leaks across history and working tree. Installing config + hook + CI."
If the baseline finds something — STOP
Do not write the hook or CI. Do not silently add findings to an allowlist. Surface what was found (the redacted output is safe to show), and walk the user through the choice:
| What it is | What to do |
|---|
| A real, current secret | Rotate it first (revoke at the provider). Then decide whether to scrub history (git filter-repo, BFG) or accept that the value is in history and rely on rotation. Re-run the baseline once decided. |
| A real, already-rotated secret | Scrubbing history is optional. Most teams leave it — git history of a rotated credential is a hygiene problem, not an active risk. Add a path-specific allowlist entry once confirmed dead. |
| A false positive (test fixture, public sample, demo key) | Add to .gitleaks.toml's [allowlist].paths (path-scoped) or [allowlist].regexes (value-scoped). Path-scoped is safer — value-scoped allowlists can mask real future leaks that happen to match. |
| Unsure | Stop. Get the user's eyes on the actual file/line. Never auto-classify a finding as a false positive. |
End the session here when blocked. Tell the user explicitly:
Found N pre-existing finding(s). I haven't written the hook or CI — installing them now would block your next commit / break CI on first push. Resolve the findings (rotate, scrub, or allowlist), then re-invoke /security-gitleaks.
History rewrites (git filter-repo, BFG) are destructive on shared history — every collaborator has to re-fetch and discard their local branches. Do not run them on the user's behalf. Explain the cost; let them decide.
Phase 2 — Write .gitleaks.toml
Use the template at assets/gitleaks.toml (copied to the repo as .gitleaks.toml). Substitute __REPO_NAME__ with the repo name (just for the title = field — cosmetic).
The template:
- Extends gitleaks' default ruleset (
useDefault = true). Do not replace it — the default catches AWS keys, Stripe, Slack tokens, JWTs, etc.
- Has a small, explicit allowlist (LICENSE, the config file itself).
- Includes commented-out custom rule scaffolds. Ask the user which ones apply, then uncomment + adapt:
- Anthropic API key — only useful if they use Claude. Cheap to add either way.
- Google OAuth client secret (
GOCSPX-…) — useful if they touch Google APIs.
- Personal home path (
/Users/<name>/…) — useful for open-source repos to keep maintainer paths out of docs/scripts. Skip on internal-only repos. If enabled, substitute __USERNAME__ (whoami is the value) in both the regex and keywords fields — without this the rule matches only the literal string /Users/__USERNAME__/… and is silently inert.
Add new rules only when the user asks. The default ruleset is broad — extending it ad-hoc invites false positives.
Show the user the file before writing. Then write it to the repo root as .gitleaks.toml.
Re-baseline with the new config
Phase 1's baseline ran with no --config, so it only checked against gitleaks' default ruleset. Any custom rule you just added (Anthropic key, Google OAuth secret, personal home path) could have pre-existing matches in history that Phase 1 missed — installing the hook/CI on top of that would block the next commit and turn CI red on first push. Re-run the dual scan, this time with the new config:
gitleaks git --config .gitleaks.toml --redact --verbose .
gitleaks dir --config .gitleaks.toml --redact --verbose .
If clean, proceed to Phase 3. If anything is found, stop and route through the same triage table as Phase 1 (rotate / scrub / allowlist), then re-run this scan. Do not install the hook or CI on a dirty re-baseline.
Phase 3 — Add the pre-commit hook
The hook lives at .githooks/pre-commit (not .git/hooks/ — that's not checked in and won't sync to collaborators).
mkdir -p .githooks
Copy assets/pre-commit to .githooks/pre-commit. Make it executable:
chmod +x .githooks/pre-commit
The hook is inert until each clone opts in:
git config core.hooksPath .githooks
This is per-clone, not committed (it's in .git/config). Tell the user explicitly:
Each collaborator (including you, in this clone) needs to run git config core.hooksPath .githooks once. Without it, the hook won't fire. We deliberately don't auto-enable it — git refuses to run hooks the user hasn't opted into, and that's the right default.
Run it for the current clone if the user asks; otherwise just print the command.
Phase 4 — Add the CI workflow
CI is the backstop. The local hook protects committers who opted in; CI catches everyone else (force-pushers, web-UI edits, collaborators who skipped core.hooksPath).
Use the template at assets/gitleaks.yml. Substitute:
__DEFAULT_BRANCH__ → captured default branch (e.g. main)
__GITLEAKS_VERSION__ → the gitleaks version to pin
To pin to current latest:
gh release view --repo gitleaks/gitleaks --json tagName --jq '.tagName' | sed 's/^v//'
Show the user the resolved version. Write the file to .github/workflows/gitleaks.yml:
mkdir -p .github/workflows
Why curl-install vs. the gitleaks-action
The template installs gitleaks via a curl-pinned tarball rather than gitleaks/gitleaks-action. Two reasons:
- The version pin lives in the workflow file itself — visible in diffs, easy to bump.
- One fewer third-party action in the supply chain. Composability over convenience here.
If the user objects (some teams standardize on marketplace actions for CODEOWNERS / Dependabot reasons), respect that. Swap to gitleaks/gitleaks-action@<SHA> and note the trade-off.
End-of-session summary
Baseline scan: clean across history + working tree.
Wrote:
.gitleaks.toml — extends default ruleset
.githooks/pre-commit — staged-changes scan, --redact
.github/workflows/gitleaks.yml — CI scan on push/PR, gitleaks <version>
What to do next:
1. Activate the local hook (per clone):
git config core.hooksPath .githooks
2. Stage and commit the three new files.
3. Push. The CI workflow will run on the PR / next push to <default-branch>.
4. Share the core.hooksPath command with collaborators (README is a fine place).
Triage assistance (interstitial, optional)
If the user comes back later — CI failed, or the hook is blocking a commit — help them per-finding. The decision tree is the same as the baseline-scan table above. The high-leverage moves:
| Situation | First move |
|---|
| Hook blocks a commit that the user knows is fine | Look at the matched value redacted in the hook output. If it's genuinely a fixture / sample / docs, add a path-scoped allowlist entry in .gitleaks.toml. Re-stage, re-commit. |
| CI fails on a PR that didn't trigger the hook | Either the contributor didn't opt into the hook, or the finding is in history they pulled in. Treat as a fresh baseline-scan finding: rotate-or-allowlist. |
| Tons of fixture-file false positives | Path-scope them: [[rules]].allowlist.paths = ['''test/fixtures/'''] — or, simpler, a top-level paths = [...] in the [allowlist] table. |
| A whole rule is noisy and not useful for this repo | Disable it: [[rules]] id = "<id>" allowlist.regexes = ['''.*'''] is heavy-handed; better is extend.disabledRules = ["<id>"] at the top. |
User wants a .gitleaksignore file instead | Supported — one path per line, gitleaks reads it automatically. Simpler for path-only exclusions; .gitleaks.toml is needed for everything else. |
When advising on allowlists, repeat the rule: path-scope before value-scope. A regex allowlist can silently swallow a real future leak that happens to match. A path allowlist is bounded to one file.
Edge cases
- Repo has existing
.gitleaks.toml. Read it, diff against the template, ask before overwriting. The user may have rules you'd be deleting.
- Repo has existing
.githooks/pre-commit. Same — read it, see if it already calls gitleaks, ask before changing.
- Repo has existing
.github/workflows/gitleaks.yml (or any workflow named gitleaks). Read it; if it's roughly equivalent (scans history + working tree, version-pinned), say so and skip rewriting. The skill's job is to get the boilerplate in place, not to enforce a specific style.
- Repo is empty / no commits yet.
gitleaks git has nothing to scan but won't error. Run gitleaks dir only. Proceed normally; baseline is trivially clean.
- Repo uses husky or another git-hooks manager.
.githooks/pre-commit won't fire under husky. Detect (ls .husky/ or cat package.json | grep husky) and write the scan command into the existing husky pre-commit script instead: gitleaks git --staged --redact --verbose --config .gitleaks.toml .. Don't create a parallel .githooks/ directory the user will forget exists.
- User wants gitleaks on a monorepo with multiple roots. The CI workflow scans the repo root by default. If different packages want different rules, point them at gitleaks'
[[rules]].path matching — out of scope for this skill to scaffold, but worth a sentence.
- User asks to
gitleaks protect. That subcommand is deprecated as of gitleaks v8.18. Use gitleaks git --staged instead — it's the supported replacement and the hook template already uses it.
Guidelines
- Do not commit on the user's behalf. Write the files, show the diff, let them commit.
- Never silently allowlist a baseline finding. Every entry in
[allowlist] needs a comment explaining why that path / regex can never contain a real secret.
- Always pass
--redact to gitleaks invocations. Without it, matched secret values land in shell history and CI logs.
- Path-scope allowlists before value-scope. Value-scope is a footgun.
- Pin the CI gitleaks version in the workflow file. Floating versions turn "added a scan" into "scan broke and nobody noticed."
- If the local hook won't run (no
core.hooksPath opt-in), don't fight it — explain why opting in is a per-clone choice and let the user decide.