This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

2026-07-30 — itd-74 (round 6), fix pass after two adversarial reviews (correctness + security) both returned BLOCK. Five decisions, each replacing a mechanism rather than patching an instance. (1) THE STORE DECLARES ITS FORMAT. The private banlist's first line — `# abcd-banlist: keyed` — decides the whole file: keyed means every line must parse as `KEY<space-or-tab>PATTERN`, no declaration means every line is one whole-line pattern under `entry-<line-number>`, and no line is ever split. The previous per-line heuristic ("is the first field key-shaped?") was wrong in two independent ways at once: it printed part of a legacy line as a key, and on this layer a pattern IS the secret, so the guard leaked what it existed to withhold; and it narrowed an old whole-line pattern to the remainder after its first field, so protection did NOT "never weaken because the format grew a column" — the earlier record line and the brief both claimed otherwise and were wrong. A declaration costs the user one line and makes both classes unrepresentable; `add`/`remove` refuse a non-empty legacy store rather than migrate it, because writing a keyed line in would silently reinterpret every other line. Both readers now strip ASCII space and tab only: `strings.TrimSpace` and bash `[[:space:]]` are different sets, so a U+00A0-indented line was keyed by one reader and dead to the other, and a U+000B-separated line the reverse. (2) VALIDATE AGAINST THE ENGINE THAT ENFORCES, not a convenient third one. A private pattern is screened for the constructs POSIX ERE does not implement (a backslash before an alphanumeric, `(?`) and then handed to grep ITSELF on stdin to accept or refuse; RE2 is the fallback only when grep cannot be run, and the refusal says so. Checking under RE2 accepted `\d` and `(?i)` as healthy (grep reads them as something else — inert protection reported live) and accepted `[a-z-.]`, which grep refuses (its fail-safe branch then blocks every commit). The public layer's engine is Go's regexp because `abcd docs lint` enforces it, so a public add compiles the EXACT string it stores through the linter's own compile path, and stores it with the `(?i)` prefix all sixteen hand-curated entries carry — without it a verb-written entry was case-sensitive while the docs promised otherwise. `list --private` reports unusable and inert lines APART: the first stops every commit, the second stops nothing, and one message for both misdirects an incident. (3) THE GUARD READS STAGED BLOBS, not diff text, and fails closed. Four shapes staged a banned name that no diff-text reading could see: a content line beginning `++` becomes `+++` and is dropped with the headers, a NUL-bearing blob has no textual diff at all, a committed `.gitattributes` with `-diff` disables the reading repo-wide in one line, and a rename is status R which the `ACM` filter excluded (as it excluded T). `git show :<path>` over `--name-only -z --diff-filter=ACMRT` asks the question the guard is actually asking and has no shape to route around. Every git step is checked, replacing a `|| true` that turned any failure into a clean pass: a check that could not run must never be indistinguishable from one that passed. (4) CONTAINMENT IS THE WRITE PATH'S JOB. Reads and writes resolve through `os.Root`, so a symlinked `.abcd/.work.local` cannot land the private patterns outside the repo while the verb reports the in-repo path; the verbs resolve the repo root as the rules loader does rather than trusting cwd, which had created a second nested store that the root-anchored gitignore does not match and the guard does not read; `add --private` refuses when git does not ignore the store path, because the layer's whole safety is that the file is untracked and the guard cannot catch its own source; and both stores hold the shared flock across load-modify-write. (5) A PATTERN NEVER TRAVELS IN ARGV. The hook passes it to grep via `-f -` on stdin, the verb accepts `-` to read one line from stdin (the documented form), and a flag-parse failure withholds the offending token instead of quoting it. Accepted trade on that last point: `SetInterspersed(false)` was rejected because the documented `add --private KEY "PATTERN" --json` puts a persistent flag after the positionals; a flag-error surface closes the leak without breaking it, and a test pins the trailing flag. Residual, stated rather than fixed: the guard still trusts an out-of-repo `sync-banlist` executable it neither verifies nor sandboxes, and a garbling refresh is only partly caught (the zero-entry warning); and the empty-`$toplevel` branch is guarded but not test-covered, because git resolves the repo before invoking a hook.

decisiondcn-2607300000000004

2026-07-30 — itd-74 (round 6), fix pass after two adversarial reviews (correctness + security) both returned BLOCK. Five decisions, each replacing a mechanism rather than patching an instance. (1) THE STORE DECLARES ITS FORMAT. The private banlist's first line — # abcd-banlist: keyed — decides the whole file: keyed means every line must parse as KEY<space-or-tab>PATTERN, no declaration means every line is one whole-line pattern under entry-<line-number>, and no line is ever split. The previous per-line heuristic ("is the first field key-shaped?") was wrong in two independent ways at once: it printed part of a legacy line as a key, and on this layer a pattern IS the secret, so the guard leaked what it existed to withhold; and it narrowed an old whole-line pattern to the remainder after its first field, so protection did NOT "never weaken because the format grew a column" — the earlier record line and the brief both claimed otherwise and were wrong. A declaration costs the user one line and makes both classes unrepresentable; add/remove refuse a non-empty legacy store rather than migrate it, because writing a keyed line in would silently reinterpret every other line. Both readers now strip ASCII space and tab only: strings.TrimSpace and bash [[:space:]] are different sets, so a U+00A0-indented line was keyed by one reader and dead to the other, and a U+000B-separated line the reverse. (2) VALIDATE AGAINST THE ENGINE THAT ENFORCES, not a convenient third one. A private pattern is screened for the constructs POSIX ERE does not implement (a backslash before an alphanumeric, (?) and then handed to grep ITSELF on stdin to accept or refuse; RE2 is the fallback only when grep cannot be run, and the refusal says so. Checking under RE2 accepted \d and (?i) as healthy (grep reads them as something else — inert protection reported live) and accepted [a-z-.], which grep refuses (its fail-safe branch then blocks every commit). The public layer's engine is Go's regexp because abcd docs lint enforces it, so a public add compiles the EXACT string it stores through the linter's own compile path, and stores it with the (?i) prefix all sixteen hand-curated entries carry — without it a verb-written entry was case-sensitive while the docs promised otherwise. list --private reports unusable and inert lines APART: the first stops every commit, the second stops nothing, and one message for both misdirects an incident. (3) THE GUARD READS STAGED BLOBS, not diff text, and fails closed. Four shapes staged a banned name that no diff-text reading could see: a content line beginning ++ becomes +++ and is dropped with the headers, a NUL-bearing blob has no textual diff at all, a committed .gitattributes with -diff disables the reading repo-wide in one line, and a rename is status R which the ACM filter excluded (as it excluded T). git show :<path> over --name-only -z --diff-filter=ACMRT asks the question the guard is actually asking and has no shape to route around. Every git step is checked, replacing a || true that turned any failure into a clean pass: a check that could not run must never be indistinguishable from one that passed. (4) CONTAINMENT IS THE WRITE PATH'S JOB. Reads and writes resolve through os.Root, so a symlinked .abcd/.work.local cannot land the private patterns outside the repo while the verb reports the in-repo path; the verbs resolve the repo root as the rules loader does rather than trusting cwd, which had created a second nested store that the root-anchored gitignore does not match and the guard does not read; add --private refuses when git does not ignore the store path, because the layer's whole safety is that the file is untracked and the guard cannot catch its own source; and both stores hold the shared flock across load-modify-write. (5) A PATTERN NEVER TRAVELS IN ARGV. The hook passes it to grep via -f - on stdin, the verb accepts - to read one line from stdin (the documented form), and a flag-parse failure withholds the offending token instead of quoting it. Accepted trade on that last point: SetInterspersed(false) was rejected because the documented add --private KEY "PATTERN" --json puts a persistent flag after the positionals; a flag-error surface closes the leak without breaking it, and a test pins the trailing flag. Residual, stated rather than fixed: the guard still trusts an out-of-repo sync-banlist executable it neither verifies nor sandboxes, and a garbling refresh is only partly caught (the zero-entry warning); and the empty-$toplevel branch is guarded but not test-covered, because git resolves the repo before invoking a hook.

Frontmatter

iddcn-2607300000000004
typedecision
date2026-07-30

mentions1