--- name: git-secret-gate-hook description: "Write a fail-closed pre-commit secret-gate hook (staged-content scan) with the grep/ERE traps that kill naive versions, plus its bats fixture test." --- # Git secret-gate pre-commit hook Use when a repo promises a "pre-commit secret gate" but none exists, or when porting the gate to another dank repo. Ships with three traps already paid for. ## Shape - Track the hook at `hooks/pre-commit` (chmod +x). Git never self-installs hooks — wire per clone: `git config core.hooksPath hooks` — run it in the MAIN checkout once; `core.hooksPath` lives in the **common** git dir config, so every linked worktree inherits it. - Do NOT put `hooks` in `package.json` `files` — an installed plugin under node_modules never runs git. - Scan **staged** content (`git show ":$f"`), not the working tree: that is what lands in history. - Denylist by path (README, docs/, tests/, `.env.example`) — files whose job is naming secrets or holding fixture dummies produce theatre, not safety. - Never echo the matched secret; print file + line number only. - Red-first test: fixture repo + `cp` the real hook in + `git config core.hooksPath hooks`, then drive REAL `git commit` attempts: credential-shaped file must exit non-zero AND `git log` must show no new commit (block, not warn); clean commit passes; hook must survive committing itself. ## The three traps (each cost a red test) 1. **`--` is a ONE-SHOT end-of-options marker.** `grep -nE -- 'p1' -- 'p2'` treats everything after the first `--` as FILE NAMES: `grep: --: No such file or directory`, rc=2. Multi-pattern = repeat `-e`: `grep -nE -e 'p1' -e 'p2'`. 2. **`if hits=$(grep ...)` is fail-OPEN on rc>=2** (rc=2 is truthy, hits empty). Capture `rc=$?` explicitly: `rc>=2` must BLOCK with "pattern error, refusing to pass unchecked" — a gate whose regex silently dies is worse than none. 3. **POSIX ERE (`grep -E`) gotchas:** no `(?:...)` groups (use plain `(...)`); a `[:-]` inside a bracket starts a collating element `[:` and dies with "repetition-operator operand invalid" — put `-` FIRST in every class (`[-A-Za-z0-9/+=_.:+]`). Also `-----BEGIN` literals: prefer printf-safe fixtures in tests. ## Pattern set (shapes, not entropy — say so in the header comment) ``` -----BEGIN[ A-Z0-9]*(OPENSSH|RSA|DSA|EC|PGP)? ?PRIVATE KEY----- AKIA[0-9A-Z]{16} gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{50,} xox[baprs]-[A-Za-z0-9-]{10,} https://hooks\.slack\.com/services/[A-Za-z0-9/]+ (aws_secret_access_key|secret(_access)?_key|(api|access|auth)[_-]?key|[a-z_]*token|passwd|password|credential)[[:space:]"']?[[:space:]]*[:=][[:space:]]*["']?[-A-Za-z0-9/+=_.:+]{16,} (Bearer|token|authorization)[[:space:]]*[:=][[:space:]]*["']?[-A-Za-z0-9._+/]{20,} ``` ## Test fixture hygiene Fake secrets must be inert (RFC 2606 domains, all-zero ids, `AKIA0000000000000EXAMPLE`) AND must not match the hook's own self-scan. Single quotes in ERE patterns: write `'"'"'`. ## Wiring gotcha (dank worktree policy) Set `core.hooksPath` AFTER the merge that lands `hooks/` into main — with the config set but the path missing, git fails every commit in worktrees lacking `hooks/` ("cannot run hooks/pre-commit: No such file or directory"). Order: merge -> land -> `git config core.hooksPath hooks` from main checkout. Reference implementation: `dank/dankvoice` `hooks/pre-commit` (PR #19), pinned by `tests/gate.bats`.