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
5.4 KiB
name, description
| name | description |
|---|---|
| harden-precommit-secret-gate | Fix or build a pre-commit secret-scan hook that actually blocks leaks: scope hole, fail-closed, quarantine list, and the self-trip that kills an automated committer. |
Fix or build a pre-commit secret-scan hook that actually blocks leaks: scope hole, fail-closed, quarantine list, and the self-trip that kills an automated committer. Use when a secret gate is wrong (misses a leak) or too right (rejects an inert example and bricks the heartbeat).
The two failure directions
A gate can be too weak (misses a real key) or too strong (rejects its own documentation and takes down an automated committer). Both are outages; the second is silent.
Audit recipe for an existing gate
- Read the hook and list what it actually inspects. The classic scope hole:
git diff --cached -- '*.env' '*.pem'— an extension whitelist. A credential inagent/config.yml, a quadlet unit, or a SKILL.md is then invisible to the gate. The rule that survives: scan the entire staged diff, every path, and let only binary content out. - Test fail-closed behaviour: can the hook be made to print nothing and still exit 0? Common escapes — running outside a git repo, unreadable staged diff, a hook that pipes through
grepand inheritsgrep's exit code from the wrong stage, orset -eswallowing the verdict. Verdict must be: unknown ⇒ block. - Quarantine list, never tracked:
.env,auth.json,trust.json,*.pem,*.key,identity*— and the.gitignorepolicy must quarantine env files at any depth under any name (**/.env*, not.env), orconfig/prod.envslips past. - Escape hatch must be explicit, per-commit, and loud:
ALLOW_SECRETS=1 git commit …, and the hook says so when it honours it. - Gate the gate: a test that commits a fixture repo through the hook and asserts it blocks.
tests/secret_gate.batshere covers: shape in an unexpected extension, bcrypt hash, JWT, placeholder/indirection allowed, prose allowed, binary ignored, unreadable diff refused, deletion of a credential never blocked,ALLOW_SECRETSoverride.
Self-trip: the gate rejects its own example (live 2026-10-21)
Symptom: a systemd timer that commits machine-written files fails every hour, and the journal shows the gate's own refusal:
SECRET GATE: staged diff contains 1 secret-shaped string(s):
agent/managed-skills/git-secret-gate-hook/SKILL.md
Fake secrets must be inert (RFC 2606 domains, all-zero ids, `AKIA0000000000000EXAMPLE`) AND must not
The offender is the AWS documentation example key — entirely inert — and AKIA[0-9A-Z]{16} cannot tell it from a leak. So the gate's own advice about writing an inert fake is uncommittable by the gate. Auto-learned skills describe credential shapes for a living, so this recurs by construction, and an automated committer cannot use ALLOW_SECRETS=1: that would blind the gate for the other 64 staged files.
Diagnose, in this order:
journalctl --user -u live-sync.service -b --no-pager | tail -25 # which file, which line
git -C ~/.omp status --short | head # how much is queued behind it
# then scan the WHOLE staged queue, not just the reported file:
git diff --cached -U0 | awk '/^diff --git/{f=$3;sub("^a/","",f)} /^\+/ && !/^\+\+\+/ {
l=substr($0,2)
if (l ~ /sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.|-----BEGIN [A-Z ]*PRIVATE KEY-----/)
print f": "substr(l,1,90) }'
The second count is the decision input: one hit ⇒ fix the line; many ⇒ the gate needs an exemption channel before anything lands.
Fixes, in order of preference:
- Describe the shape instead of printing a matching literal —
AKIAfollowed by zeroes andEXAMPLE, spelled out in prose, cannot match. One line, the gate stays strict, the next timer run drains the queue. - Per-line allow marker honoured by the hook (e.g. a trailing
secret-gate: allowon the flagged line) — durable, keeps documentation honest, still fail-closed everywhere else. Needs a test both ways: a marked line passes, an unmarked one still blocks. Note the risk honestly: an attacker-adjacent string can self-authorise, so the marker belongs in review-able prose tiers, not in a diff that also touches secrets config. - Never widen the regex to skip markdown, never run the automation with the override env var set.
Inert-fake rule for anything that must show a credential shape: RFC 2606 domains, all-zero ids, the word EXAMPLE — and if the shape itself trips the gate, describe it in prose rather than print it.
Prose must not be collaterally blocked
Markdown explaining credential handling is the bulk of what a knowledge repo commits. xox inside a sentence about Slack token prefixes, or a sk- prefix discussed abstractly, must pass; the shapes are anchored long-suffix patterns for exactly that reason. When a gate fires on prose, that is a bug in the gate — but if the prose contains a literal matching token, the fix is the prose (option 1 above), not the pattern.
Verifying a gate change
Write the fixture repo in a temp dir, commit through the hook, assert block/allow for each case. Do not test the regex by grepping the hook's source for the pattern — a copy of the pattern in a comment is not a gate that runs.