Add lefthook with gitleaks pre-commit hook (#39) #61

Merged
james merged 1 commit from 39-lefthook into main 2026-06-15 12:40:40 +00:00
Owner

Closes #39.

Summary

  • Add lefthook as a devDep (^2.1.9) and wire prepare: "lefthook install || true" in package.json. The || true keeps the Dockerfile's npm ci working — the build image has no .git, so lefthook install would otherwise fail.
  • New lefthook.yml with one pre-commit command: gitleaks scanning staged changes (gitleaks protect --staged --redact). --redact keeps detected secret values out of terminal output / shell history.
  • If gitleaks isn't on PATH, the hook fails closed with a clear "✗ gitleaks not installed — see README" message instead of silently passing. Avoids the "secret-scanning enabled" theatre when the tool isn't actually there.
  • README "Requirements" gains a gitleaks entry with install hint (brew install gitleaks or grab a binary from the project's releases page) plus a note that LEFTHOOK_EXCLUDE=<name> is the supported per-commit skip path.
  • CLAUDE.md "Working in this repo" gets a short bullet documenting the local-hooks convention so future contributors (and future me) don't have to dig.

Out of scope

  • Adding more hooks (lint-staged, prettier on staged files, typecheck). CI already covers all of those — pre-commit value lives in catching what CI can't (secrets that, once pushed, are leaked even after a force-revert). Bikeshed the additional hooks separately.
  • Adding gitleaks to CI itself. Worth a separate ticket — the failure modes (skipped local hook, contributors without gitleaks installed, force-pushes) make a server-side scan complementary, not redundant.

Verification (locally)

  • npm install auto-runs lefthook install; .git/hooks/pre-commit is now lefthook's wrapper.
  • Created a temp file containing a GitHub PAT-style literal (ghp_…) and ran git commit: gitleaks output WRN leaks found: 1, lefthook reported 🥊 gitleaks (blocked), exit status 1, no commit was made. Cleaned up the test file before committing the real changes.
  • Sanity test with the canonical AWS docs example key (AKIAIOSFODNN7EXAMPLE) was a no-op — gitleaks has it on a known-good allowlist as documentation example. Worth knowing for future bug reports.
  • The actual commit for this PR ran through the hook successfully (✔️ gitleaks, 88.7ms scanning ~7.9 KB).
  • npm run typecheck, npm run lint, npm test all green (86 passed, 24 skipped). Lefthook adds no test-runtime surface.

Test plan

  • Hook fires on a real commit and blocks a synthetic GitHub PAT.
  • Hook fails closed if gitleaks isn't installed (read the if ! command -v branch; not exercised in CI because the CI box has gitleaks via Linuxbrew on this dev machine).
  • No regression to typecheck / lint / test surfaces.
  • CI run: confirm npm ci succeeds despite prepare invoking lefthook install (the || true guard).

🤖 Generated with Claude Code

Closes #39. ## Summary - Add `lefthook` as a devDep (`^2.1.9`) and wire `prepare: "lefthook install || true"` in `package.json`. The `|| true` keeps the Dockerfile's `npm ci` working — the build image has no `.git`, so `lefthook install` would otherwise fail. - New `lefthook.yml` with one `pre-commit` command: gitleaks scanning staged changes (`gitleaks protect --staged --redact`). `--redact` keeps detected secret values out of terminal output / shell history. - If gitleaks isn't on `PATH`, the hook fails closed with a clear "✗ gitleaks not installed — see README" message instead of silently passing. Avoids the "secret-scanning enabled" theatre when the tool isn't actually there. - README "Requirements" gains a gitleaks entry with install hint (`brew install gitleaks` or grab a binary from the project's releases page) plus a note that `LEFTHOOK_EXCLUDE=<name>` is the supported per-commit skip path. - CLAUDE.md "Working in this repo" gets a short bullet documenting the local-hooks convention so future contributors (and future me) don't have to dig. ## Out of scope - Adding more hooks (lint-staged, prettier on staged files, typecheck). CI already covers all of those — pre-commit value lives in catching what CI can't (secrets that, once pushed, are leaked even after a force-revert). Bikeshed the additional hooks separately. - Adding gitleaks to CI itself. Worth a separate ticket — the failure modes (skipped local hook, contributors without gitleaks installed, force-pushes) make a server-side scan complementary, not redundant. ## Verification (locally) - `npm install` auto-runs `lefthook install`; `.git/hooks/pre-commit` is now lefthook's wrapper. - Created a temp file containing a GitHub PAT-style literal (`ghp_…`) and ran `git commit`: gitleaks output `WRN leaks found: 1`, lefthook reported `🥊 gitleaks` (blocked), exit status 1, no commit was made. Cleaned up the test file before committing the real changes. - Sanity test with the canonical AWS docs example key (`AKIAIOSFODNN7EXAMPLE`) was a no-op — gitleaks has it on a known-good allowlist as documentation example. Worth knowing for future bug reports. - The actual commit for this PR ran through the hook successfully (`✔️ gitleaks`, 88.7ms scanning ~7.9 KB). - `npm run typecheck`, `npm run lint`, `npm test` all green (86 passed, 24 skipped). Lefthook adds no test-runtime surface. ## Test plan - [x] Hook fires on a real commit and blocks a synthetic GitHub PAT. - [x] Hook fails closed if gitleaks isn't installed (read the `if ! command -v` branch; not exercised in CI because the CI box has gitleaks via Linuxbrew on this dev machine). - [x] No regression to typecheck / lint / test surfaces. - [ ] CI run: confirm `npm ci` succeeds despite `prepare` invoking `lefthook install` (the `|| true` guard). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add lefthook with gitleaks pre-commit hook (#39)
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 34s
PR / npm audit (pull_request) Successful in 39s
PR / Lint (pull_request) Successful in 40s
PR / Typecheck (pull_request) Successful in 42s
PR / Static analysis (Semgrep) (pull_request) Successful in 45s
PR / Test (sqlite) (pull_request) Successful in 51s
PR / Test (postgres) (pull_request) Successful in 56s
PR / Build (pull_request) Successful in 1m10s
PR / Trivy (image) (pull_request) Successful in 1m24s
8f27100724
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
james force-pushed 39-lefthook from 8f27100724
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 34s
PR / npm audit (pull_request) Successful in 39s
PR / Lint (pull_request) Successful in 40s
PR / Typecheck (pull_request) Successful in 42s
PR / Static analysis (Semgrep) (pull_request) Successful in 45s
PR / Test (sqlite) (pull_request) Successful in 51s
PR / Test (postgres) (pull_request) Successful in 56s
PR / Build (pull_request) Successful in 1m10s
PR / Trivy (image) (pull_request) Successful in 1m24s
to 851db45d6b
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 21s
PR / Static analysis (Semgrep) (pull_request) Successful in 34s
PR / Trivy (image) (pull_request) Successful in 55s
PR / Lint (pull_request) Successful in 1m30s
PR / npm audit (pull_request) Successful in 1m39s
PR / Typecheck (pull_request) Successful in 1m40s
PR / Build (pull_request) Successful in 1m45s
PR / Test (sqlite) (pull_request) Successful in 1m45s
PR / Test (postgres) (pull_request) Successful in 5m57s
2026-06-15 12:40:21 +00:00
Compare
james merged commit a633aa9898 into main 2026-06-15 12:40:40 +00:00
Sign in to join this conversation.
No description provided.