Files
command-center/agent/managed-skills/harden-precommit-secret-gate/SKILL.md
T
Frank 8af40c402b chore(agent): commit the machine tier and the doctrine text the checkout was already running
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
2026-10-21 01:44:51 -07:00

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

  1. Read the hook and list what it actually inspects. The classic scope hole: git diff --cached -- '*.env' '*.pem' — an extension whitelist. A credential in agent/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.
  2. 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 grep and inherits grep's exit code from the wrong stage, or set -e swallowing the verdict. Verdict must be: unknown ⇒ block.
  3. Quarantine list, never tracked: .env, auth.json, trust.json, *.pem, *.key, identity* — and the .gitignore policy must quarantine env files at any depth under any name (**/.env*, not .env), or config/prod.env slips past.
  4. Escape hatch must be explicit, per-commit, and loud: ALLOW_SECRETS=1 git commit …, and the hook says so when it honours it.
  5. Gate the gate: a test that commits a fixture repo through the hook and asserts it blocks. tests/secret_gate.bats here 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_SECRETS override.

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:

  1. Describe the shape instead of printing a matching literal — AKIA followed by zeroes and EXAMPLE, spelled out in prose, cannot match. One line, the gate stays strict, the next timer run drains the queue.
  2. Per-line allow marker honoured by the hook (e.g. a trailing secret-gate: allow on 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.
  3. 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.