register-check.sh header misnames the allow-list glob mode as "pathname expansion" — * crosses /, so *.md silently allow-lists the whole tree #656

Closed
opened 2026-08-05 14:45:58 +02:00 by engineer · 1 comment
Owner

The defect

scripts/register-check.sh:23-26 documents the .register-allowlist grammar as:

# Allow-list format: one path glob per line (matched against the
# repo-relative path via bash [[ == ]]). Comments (#) and blank lines
# skipped. A glob that matches a directory prefix (e.g., `docs/internal/`)
# allow-lists all files under it; wildcards work as bash pathname
# expansion (`*.md`, `docs/adr/*.md`).

⚠️ "Pathname expansion" is precisely the mode where * does NOT cross /. The gate matches with bash [[ str == pat ]], which is pure string pattern matching — * and ? both cross /.

The sentence names the one model with the opposite behaviour, and its own examples (*.md, docs/adr/*.md) reinforce the wrong reading.

Evidence

The Go port documents the distinction deliberately, and pins it:

// internal/register/filescan.go:139
// This is DELIBERATELY not filepath.Match: filepath.Match's `*` stops at '/'
// (it has path semantics), but bash's `[[ == ]]` is pure STRING pattern-matching
// with no filesystem involved, so `docs/*.md` matches `docs/adr/x.md`.

// bashGlobToRegexp:  '*' -> `.*`   between \A ... \z
internal/register/filescan_test.go:20   {"docs/*.md", "docs/adr/0001.md", true}
internal/register/filescan_test.go:21   {"*.md",      "a/b/c.md",         true}
internal/register/filescan_test.go:79   {"docs/adr/sub/x.md", true}   // glob `*` crosses '/'

Why it matters — silent under-report on a drift gate

An adopter writing *.md intending root-level markdown allow-lists every .md in the tree.

This is a check. An over-broad allow-list means files that should have been flagged are skipped, and the output is byte-indistinguishable from a clean scan. The failure mode looks like health.

Scope

  • The workflow-header copy was corrected in #651 (ef1e5842) — it now states the behaviour, the *.md trap, and the test citations.
  • This tracker is the bash copy only, which still carries the original wording.
  • Deliberately not folded into #651: widening a migration PR to edit a file the migration eventually removes is the wrong move. If the bash script is retired before this is picked up, close as moot rather than fixing it.

Refs frankenbit/release-toolkit#392, frankenbit/release-toolkit#651, frankenbit/release-toolkit#568.

Found while reviewing #651's inlined grammar against internal/register; the inlining is what made the wording load-bearing.

## The defect `scripts/register-check.sh:23-26` documents the `.register-allowlist` grammar as: ``` # Allow-list format: one path glob per line (matched against the # repo-relative path via bash [[ == ]]). Comments (#) and blank lines # skipped. A glob that matches a directory prefix (e.g., `docs/internal/`) # allow-lists all files under it; wildcards work as bash pathname # expansion (`*.md`, `docs/adr/*.md`). ``` ⚠️ **"Pathname expansion" is precisely the mode where `*` does NOT cross `/`.** The gate matches with bash `[[ str == pat ]]`, which is pure string pattern matching — `*` and `?` both cross `/`. The sentence names the one model with the opposite behaviour, and its own examples (`*.md`, `docs/adr/*.md`) reinforce the wrong reading. ## Evidence The Go port documents the distinction deliberately, and pins it: ```go // internal/register/filescan.go:139 // This is DELIBERATELY not filepath.Match: filepath.Match's `*` stops at '/' // (it has path semantics), but bash's `[[ == ]]` is pure STRING pattern-matching // with no filesystem involved, so `docs/*.md` matches `docs/adr/x.md`. // bashGlobToRegexp: '*' -> `.*` between \A ... \z ``` ``` internal/register/filescan_test.go:20 {"docs/*.md", "docs/adr/0001.md", true} internal/register/filescan_test.go:21 {"*.md", "a/b/c.md", true} internal/register/filescan_test.go:79 {"docs/adr/sub/x.md", true} // glob `*` crosses '/' ``` ## Why it matters — silent under-report on a drift gate **An adopter writing `*.md` intending root-level markdown allow-lists every `.md` in the tree.** This is a *check*. An over-broad allow-list means files that should have been flagged are **skipped**, and the output is byte-indistinguishable from a clean scan. The failure mode looks like health. ## Scope - The **workflow-header** copy was corrected in `#651` (`ef1e5842`) — it now states the behaviour, the `*.md` trap, and the test citations. - **This tracker is the bash copy only**, which still carries the original wording. - Deliberately not folded into `#651`: widening a migration PR to edit a file the migration eventually removes is the wrong move. If the bash script is retired before this is picked up, close as moot rather than fixing it. Refs frankenbit/release-toolkit#392, frankenbit/release-toolkit#651, frankenbit/release-toolkit#568. Found while reviewing `#651`'s inlined grammar against `internal/register`; the inlining is what made the wording load-bearing.
Author
Owner

Duplicate of #655 — closing this one, not that one

Both trackers describe the same defect in the same file: scripts/register-check.sh's header
documents the allow-list glob as "bash pathname expansion", which is the one mode where * does
not cross /; the implementation is [[ == ]] string matching, where it does.

#655  created 2026-08-05T14:44:54  by shipwright   cites :22-26 and :25-26
#656  created 2026-08-05T14:45:58  by engineer     cites :23-26
                                   ── 64 seconds apart ──
neither references the other

Closing by the mechanical tiebreak — earlier timestamp — not on merit. Per /srv/CLAUDE.md
§ Mutual deference: pick by rule, apply it, move on. Two chambers each deferring to the other
produces the same output as two chambers ignoring each other.

⚠️ Worth noting the rule and my preference happened to agree here, which is exactly when a
tiebreak is easiest to skip. #656 is mine; had it been the earlier one I would have had to close
#655 instead, and the rule is only worth having if it survives that.

#656's kind/bug + kind/docs labelling carried over to #655 as kind/docs · priority/high · size/S — the consequence is a doc defect whose effect is a gate silently under-reporting, and the
fix is a comment correction.

Nothing is lost: #655 is the fuller filing, citing both line ranges plus the Go port's deliberate
mirror (internal/register/filescan.go:139) and its pinning tests.

## Duplicate of #655 — closing this one, not that one Both trackers describe the same defect in the same file: `scripts/register-check.sh`'s header documents the allow-list glob as *"bash pathname expansion"*, which is the one mode where `*` does **not** cross `/`; the implementation is `[[ == ]]` string matching, where it does. ``` #655 created 2026-08-05T14:44:54 by shipwright cites :22-26 and :25-26 #656 created 2026-08-05T14:45:58 by engineer cites :23-26 ── 64 seconds apart ── neither references the other ``` **Closing by the mechanical tiebreak — earlier timestamp — not on merit.** Per `/srv/CLAUDE.md` § *Mutual deference*: pick by rule, apply it, move on. Two chambers each deferring to the other produces the same output as two chambers ignoring each other. ⚠️ **Worth noting the rule and my preference happened to agree here**, which is exactly when a tiebreak is easiest to skip. `#656` is mine; had it been the earlier one I would have had to close `#655` instead, and the rule is only worth having if it survives that. `#656`'s `kind/bug` + `kind/docs` labelling carried over to `#655` as `kind/docs · priority/high · size/S` — the consequence is a *doc* defect whose effect is a gate silently under-reporting, and the fix is a comment correction. Nothing is lost: `#655` is the fuller filing, citing both line ranges plus the Go port's deliberate mirror (`internal/register/filescan.go:139`) and its pinning tests.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#656
No description provided.