The live-sync heartbeat failed three hourly ticks (22:25, 23:27, 00:28) because a
managed skill that documents how to write a secret gate quotes the vendor's
all-zeros placeholder access-key id, and the shape rule fired on the
documentation. The machine tier is prose ABOUT credentials — the gate's own header
says a gate that fires there gets overridden until it means nothing — and a
heartbeat that cannot commit is a fleet that silently stops syncing config.
- shape hits are judged on the matched token, not the line: a documented fake is
inert, one live token sharing the file still blocks. The match is judged with
its following context, because a fixed quantifier truncates `EXAMPLE` to `EXA`
and the placeholder would vanish from what is examined.
- assignment values keep the stricter rule from the previous commit: a comment on
the same line cannot vouch for the token beside it.
- `ALLOW_SECRETS` remains the only override; nothing here widens what is scanned,
it only stops flagging text that was never a credential.
Verified against the payload that is stuck right now: the skill page that the
heartbeat cannot commit passes the fixed hook and was refused by the live one.
Refs #60
Three ways the fail-closed gate failed open, all reproduced by new tests first:
- `git diff --name-only` quotes any path it cannot print as UTF-8
(core.quotePath, on by default). The quoted string was handed back to
`git diff -- <path>`, which matches nothing, and an empty diff is `continue`d —
so a credential under `café/` committed cleanly. The list is now NUL-delimited
and read from a temp file, because command substitution deletes NUL bytes and
the names must survive to the loop that reads them.
- The placeholder filter matched the whole added line, so `API_TOKEN=<live> #
example, placeholder, changeme` was exempt on the strength of its own comment.
The filter now reads the assigned value only — the token up to whitespace or
`#`. Truncation errs toward blocking; ALLOW_SECRETS remains the way out.
- `report+=` ended on a command substitution, so the next file's name was glued
onto the previous file's last hit line and the report blamed the wrong file.
Three tests, all red before the change: a quoted path, a placeholder comment
beside a live value, and two leaking files each named on their own line.
Refs #60
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
Config in, state out: databases, WAL/SHM sidecars, caches, logs, sessions,
run dirs, ssh-control sockets stay OUT of history. Quarantine list: auth.json,
.env, trust.json, private keys never tracked. Pre-commit hook scans staged diff
for live-secret shapes (sk-, ghp_, github_pat_, glpat-, xox, AKIA, PEM headers,
JWTs) before anything reaches the Gitea remote.