Landing blocker for #61 and #63: the deployment checkout is 12 commits behind and `merge --ff-only` refuses while tracked paths are dirty. Discard is not available here (workspace policy: no `checkout`/`stash` on shared state), so the dirt is committed. - `agent/APPEND_SYSTEM.md`: the point-of-use / one-work-log / PR-link doctrine, already byte-identical to `origin/main` (`cmp` against the trunk blob: no difference). No new prose — the commit exists so the trunk merge can proceed. - `agent/config.yml` and `agent/managed-skills/**`: the machine-live tier the heartbeat owns (AGENTS.md § Live-sync tiers). `ALLOW_SECRETS=1` was used, deliberately and after auditing the staged diff by hand: the only credential-shaped strings in it are two copies of `AKIA0000000000000EXAMPLE` — the inert all-zero AWS example id — and they appear inside the two skill pages that teach that exact placeholder shape. This checkout is still running the pre-#61 hook; the merged gate (value-scoped placeholder filter, `71601f7`) exempts this value, so the refusal dies with this landing. No `--no-verify` was used: the gate ran and its verdict is recorded here. Refs #61
2.8 KiB
2.8 KiB
name, description
| name | description |
|---|---|
| grep-secret-gate-hook-traps | Write a zero-dependency grep-based pre-commit secret gate that fails closed: the GNU grep -- one-shot trap, POSIX ERE limits, bracket collating elements, and rc=2-as-success in command substitution. |
Fail-closed grep secret gate (pre-commit hook)
Use when writing/auditing a bash pre-commit hook (or any CI tripwire) that scans
staged content with grep -E for credential shapes — no gitleaks available.
The four traps (all personally earned; each silently PASSES bad content)
--is a ONE-SHOT end-of-options marker.grep -nE -- 'p1' -- 'p2' 'p3'takes the later--and patterns as FILE names → rc=2, errors on stderr. Correct multi-pattern form: repeat-efor every pattern; never--.if hits=$(... | grep ...)treats rc=2 as success. Any non-zero-but-not-1 (broken pattern, unreadable file) enters thethenbranch with EMPTY output. Never branch on the substitution's truthiness. Capture rc explicitly:hits=$(...); rc=$?then:rc>=2→ BLOCK (fail closed — a dead regex is worse than none),rc==1→ clean,rc==0→ hits.- POSIX ERE (
grep -E) has no(?:...)groups. Use plain(...). [:-]inside a bracket expression starts a collating element →grep: repetition-operator operand invalid(rc=2, see trap 2). Put-FIRST in every class:[-A-Za-z0-9/+=_.:+].
Gate structure that works
- Scan staged content, not the working tree:
git show ":$f"overgit diff --cached --name-only --diff-filter=ACM. - Denylist doc/fixture paths (
README,docs?/,tests?/,*.example) — else the hook blocks its own patterns and every test fixture. - Report
BLOCKED <file>:<line>only — never echo the matched secret. - Wire with tracked
hooks/pre-commit+git config core.hooksPath hooks(lives in the COMMON git dir config → one set in the main checkout covers every worktree). Fresh clones are unprotected until it's set; document it. - Worktrees lacking
hooks/whilecore.hooksPathis set: git dies on commit (cannot run ... No such file or directory) — merge main in, don't unset. - Do NOT ship the hook in an npm plugin artifact (
files): node_modules never runs git; pin its ABSENCE in the pack test instead.
Test it red-first through the real seam (bats)
git init a fixture repo, cp the real hook in, git config core.hooksPath hooks,
then drive REAL git commit and assert:
- credential-shaped
.env(RFC-2606 domains / all-zero ids as inert fixtures) → non-zero ANDgit logcount unchanged (blocks, not warns); - PEM header line → blocked;
- clean prose → passes;
- the hook committing ITSELF → passes (trap 1/3/4 usually die here);
- exit-code masking:
bats ... | tailreturns tail's rc — never&& commitbehind a piped suite, or you commit on red.