Close the secret-gate scope hole, and make it fail closed

The gate scanned only `*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js`. A
`postgres.env`, a `.container` quadlet, an `.ini`, or an extension-less dotfile
— exactly the shapes the dotfiles tier is made of — was never read at all.
Discovered while deciding what else belongs in `dotfiles/`: `~/.config`
carries `containers/systemd/postgres.env` with a real `POSTGRES_PASSWORD`,
which the ignore list did not cover either (`git check-ignore` says so: `.env`
does not match `postgres.env`).

Token SHAPES now scan the entire staged diff, every file type; bcrypt hashes
and `Authorization: Basic` headers joined the shape list. Credential
ASSIGNMENTS are scanned in non-prose files only — the auto-learned memory tier
is prose *about* credentials (7 such hits measured on the tracked tree), and a
gate that fires there gets overridden until it means nothing. Markdown keeps
shape detection, which is what it had before.

Three invariants, each pinned by a test in tests/secret-gate.bats (13 tests,
black-box: the hook is copied into a scratch repo and git itself runs it):

- Fail closed. The first version of this rewrite passed `-I` to `git diff`,
  which is not valid there; the empty file list made every secret commit sail
  through, and a blind gate looks exactly like a clean tree. If the hook cannot
  read the staged diff it now refuses the commit.
- Only ADDED lines can leak. The gate refused the redaction commit in this
  branch because the diff still carried the removed password — a gate that
  blocks the removal of a secret holds the leak hostage.
- Path-valued variables (`…_FILE=`, `…_PATH=`) and placeholder/indirected
  values are not secrets, so the gate stays worth listening to.

`.gitignore` quarantines `*.env` at any name, and gains the theme-media rule
whose untracking lands in the next commit. `AGENTS.md` states the new contract.

Refs #40
This commit is contained in:
Frank committed 2026-10-20 16:48:33 -07:00
1 parent 7da5e9dfa8
commit 04f93133fc
4 files changed
+274 -15

No files matched your search

+15 -5
View File
@@ -111,12 +111,22 @@ Single-context, inside the existing knowledge base: `omp-docs/glossary.md` and
## Secret gate (HARD rule)
The pre-commit hook (`.githooks/pre-commit`, enabled via
`git config core.hooksPath .githooks`) scans every staged diff for live-secret
shapes (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM headers,
JWTs) and blocks the commit. Quarantine list — never tracked, ever: `auth.json`,
`.env`, `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
`git config core.hooksPath .githooks`) blocks the commit in two ways. Token
**shapes** (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM
headers, JWTs, bcrypt hashes, `Authorization: Basic` headers) are scanned across
the **entire** staged diff — every file type, because the first version of the
gate whitelisted extensions and never read a `.env`, `.container`, `.ini`, or
extension-less dotfile at all. Credential **assignments** (`PASSWORD=…`,
`secret_key: …`) are scanned in non-prose files only; markdown keeps shape
detection, because the auto-learned memory tier is prose *about* credentials and a
gate that fires there gets overridden until it means nothing. The gate **fails
closed**: if it cannot read the staged diff it refuses the commit rather than
passing it. Quarantine list — never tracked, ever: `auth.json`, `.env` and
`*.env` (`postgres.env`, `frank-job-hunt.env` are the ones that carry real
values), `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
`ALLOW_SECRETS=1 git commit …`. Remember why: a secret in a tracked file is a
secret in every agent's context window on every machine that pulls this repo.
secret in every agent's context window on every machine that pulls this repo, and
redacting it later does not un-leak the history.
## Machine facts (verify before trusting)