Skip to content

fix: bug-hunt round 7 — guard > bypass, frontmatter BOM bypass, and 14 more - #415

Merged
REPPL merged 17 commits into
mainfrom
bughunt-b/round-7
Aug 22, 2026
Merged

fix: bug-hunt round 7 — guard > bypass, frontmatter BOM bypass, and 14 more#415
REPPL merged 17 commits into
mainfrom
bughunt-b/round-7

Conversation

@REPPL

@REPPL REPPL commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

What

Round 7 of the autonomous all-dimensions bug hunt. Five parallel Opus 5 hunters swept the four dimensions; every candidate was adversarially refuted by an independent Opus 5 subagent before capture. 8 substantive (2 major, 6 minor) + 8 nitpick findings confirmed and fixed; 7 refuted or carried as prior art. Each finding is captured and resolved in .abcd/work/issues/.

Security (major):

  • Guard &> bypass (iss-2608220131352917) — the tokenizer read a leading & as a background/&& operator, so a glued or spaced &>/&>> both-streams redirection (git push &>/dev/null --force origin main) split the simple command and dropped its dangerous flag out of command position — a silent allow on every blocker-tier entry. Now recognised as a redirection before the list split; target dropped, fd digit kept.
  • Frontmatter BOM / multi-line-comment bypass (iss-2608220134344680) — the scanners anchored on line 0 and TrimSpace does not strip a UTF-8 BOM, so a BOM- or multi-line-comment-led record yielded an empty field map and slipped the no_git_metadata blocker and the record_schema id/supersession gates. Shared frontmatter.TrimBOM + stateful comment skip in the lint and glossary scanners.

Fixed (minor):

  • update/history home-path leak (iss-2608220142158516) — absolute home-rooted paths in the update refusal detail/target_path and history record path (text and --json), including the plugin-session refusal the plugin relays into agent chat; redacted to ~ via a new fsutil.RedactHome at the render boundary.
  • ahoy gitfile worktree (iss-2608220136593438) — .git tested for dir-ness misread a linked worktree/submodule as an unmanaged folder (ahoy install exited 0 aborted with a wrong reason); now tests existence, the last isDir(.git) holdout after iss-72.
  • ahoy guard-health zero value (iss-2608220136597127) — --json serialised a never-computed all-false guard object for a folder; now a pointer omitted for a folder, matching the banlist sibling.
  • repolint cap TOCTOU (iss-2608220144233519) — a file grown past the scan cap during the read was scanned as a truncated prefix and reported clean; now cap+1 with the grown file routed to the not-scanned path.
  • JSON null collections (iss-2608220147106835) — spec/intent/memory/capture emitted bare null for an empty collection; constructors seed them non-nil so an empty store marshals [].
  • brief zero-live-bundles claim (iss-2608220150151397) — reworded to "declared but not yet delivered" (residual of iss-123's README-only fix).

Nitpicks: update fail-closed switch (iss-2608220142154022), docs-lint engine-fault exit 2 (iss-2608220145356167), ADR filename-ordinal dispatch + spc-26 amend (iss-2608220148289898), pull_request_target checkout-guard test (iss-2608220149008905), and doc corrections to sota-per-intent.md links (iss-2608220150157497), version.md install_mode (iss-2608220150154972), the CI classifier standdown in AGENTS/CONTRIBUTING (iss-2608220150152332), and docs/requirements.txt transitives (iss-2608220150152535).

Refuted / prior art (not in the ledger): the abcd-cli name in commands/launch.md (the live internal project name, record-lint-prescribed); the spc-3 predecessor-store collision (a new instance of open iss-239, carried there); the .abcd/work roster omission (round 4's highlight-not-inventory doctrine governs); the CI gitleaks same-origin checksum (adr-46's accepted trust bar, tracked as open iss-379); the go.mod vs setup-go patch coupling (GOTOOLCHAIN=auto self-corrects); the Makefile CRLF eol pin (GNU make strips the CR on POSIX); and the wrangler/mkdocs docs-site deploy pin (dashboard-side, not an in-repo mechanism).

Verification

  • make preflight green (build, vet, test, -race ./internal/..., and the three lint gates) before any change and on the final tree; gofmt -l . silent; scripts/check-attribution.sh commits origin/main HEAD clean.
  • Every behaviour-changing code fix carries a test watched fail before the change and pass after — the guard &> block on all blocker forms, the BOM/comment blocker reach, the gitfile worktree classification, the guard-health omission, the home-path redaction, the docs-lint exit code, the fail-closed switch, the JSON [] invariant, and the ADR-ordinal dispatch. The repolint size TOCTOU is closed by construction; its boundary test guards the cap, not the race.

Assisted-by: Claude:claude-fable-5

REPPL added 10 commits August 22, 2026 01:55
The tokenizer read a leading `&` as a background/`&&` operator, so gluing or
spacing bash's both-streams redirection into a command
(`git push &>/dev/null --force origin main`) split the simple command in two and
dropped the dangerous flag out of command position -- a silent allow on every
blocker-tier entry. `&>` / `&>>` are now recognised as a redirection before the
list-operator split; the target is dropped and, unlike `>`/`<`, a preceding digit
is kept as a real argument (bash does not read it as an fd for `&>`). Token- and
decision-level tests watch each blocker form flip from allow to block.

Assisted-by: Claude:claude-fable-5
The frontmatter scanners anchored on line 0, and TrimSpace does not strip a
UTF-8 byte-order mark, so a record led by a BOM or a multi-line `<!-- ... -->`
comment yielded an empty field map and slipped the no_git_metadata blocker and
the record_schema id/supersession gates entirely -- the same silent fail-open the
single-line-comment fix closed. A shared frontmatter.TrimBOM strips a leading
mark in Fields, and the lint and glossary scanners now skip a multi-line comment
statefully before the `---`, with absolute line numbers preserved.

Assisted-by: Claude:claude-fable-5
The privacy scanner read exactly maxScanBytes, so a tracked file that grew past
the cap between the fstat and the read was scanned as a truncated prefix and
reported clean -- a false "scanned" on a privacy control that bypasses the
not-scanned warning the same file over the cap at stat time already takes. The
reader now probes one byte past the cap and routes a grown file to that same
not-scanned path. A boundary test pins the cap; the growth race itself is closed
by construction.

Assisted-by: Claude:claude-fable-5
abcd spec, abcd intent, abcd memory, and abcd capture emitted bare null for an
empty collection, so a consumer iterating the value (jq `.[]`, an agent following
the command doc) errored on null. The core constructors now seed the collections
non-nil, so an empty store marshals an empty array -- the invariant history list
already held.

Assisted-by: Claude:claude-fable-5
abcd adr-N routed by a fixed four-digit filename prefix, so a differently-padded
ADR file -- lint-green and citation-resolvable, since record-lint and the citation
resolver compare numerically -- was reported not found by the one reader that
pinned padding. Dispatch now routes by the filename's numeric ordinal, the
filename-ordinal residue of the earlier frontmatter-id fix; the spc-26 spec line
is amended to match.

Assisted-by: Claude:claude-fable-5
external-review.yml suppresses zizmor's dangerous-triggers audit on its
pull_request_target trigger, so the safety invariant it states in prose (PR code
is never checked out) is unenforced: a later checkout added to that job would pass
every zizmor persona -- a pwn request. A workflow-walking test now asserts that no
pull_request_target workflow references actions/checkout, arming the invariant the
comment only asserts.

Assisted-by: Claude:claude-fable-5
The update refusal detail and receipt (target_path) carried the developer-identity
home root raw into their success and --json envelopes -- which the CLI error scrub
never sees -- including the documented plugin-session refusal the plugin relays
into agent chat; a new fsutil.RedactHome redacts them to ~ at the point they enter
the string. Separately, the dispatch switch had no default, so a target kind it
could not classify fell through to fetch-and-swap; the regular-file case is now
explicit and every other kind a named refusal (a mutating verb never proceeds on
input it cannot classify).

Assisted-by: Claude:claude-fable-5
Four surface fixes that share internal/surface/cli/cli.go:

- ahoy detection tested .git for dir-ness, so a linked worktree or submodule
  (where .git is a gitfile) was misclassified as an unmanaged folder -- every gap
  detector skipped and `ahoy install` exiting 0 with a wrong "not a git
  repository" reason. It now tests existence, the last isDir(.git) holdout.
- ahoy --json serialised a never-computed all-false guard object for an unmanaged
  folder, reporting a broken guard beside a resolved plugin root; the guard field
  is now a pointer omitted for a folder, matching the banlist sibling.
- abcd history list/show/capture echoed the absolute home-rooted record path into
  their success and --json envelopes; the path is redacted to ~ at the render
  boundary (callers re-derive the file handle from disk, never this value).
- abcd docs lint returned an engine or config fault as exit 1 -- the code a blocker
  finding uses -- while abcd lint and record-lint exit 2; the fault path now exits
  2 with a path-scrubbed message.

Assisted-by: Claude:claude-fable-5
- The mental-model brief said the corpus has zero live bundles while four planned
  intents declare a bundle-member of spc-83; reworded to "declared but not yet
  delivered" (no shared spec minted), the residual of iss-123's README-only fix.
- sota-per-intent.md linked adr-22/adr-26 to the decisions directory rather than
  the ADR files; repointed.
- commands/version.md now notes install_mode is omitted from the JSON when no
  abcd-owned PATH entry is resolvable, rather than always naming it.
- AGENTS.md and CONTRIBUTING.md record that the CI classifier stands the macOS
  leg, race lane, zizmor, govulncheck and smoke down on a docs-only pull request
  (the merge-queue run still gates the merge with the full set).
- docs/requirements.txt no longer claims a reproducibility its unpinned transitive
  dependencies do not deliver.

Assisted-by: Claude:claude-fable-5
…ummary

Round 7 of the all-dimensions bug hunt: 8 substantive (2 major, 6 minor) and 8
nitpick findings confirmed and fixed, 7 refuted or carried as prior art. The two
majors are the guard &> redirection bypass and the frontmatter BOM/comment blocker
bypass. Records captured and resolved under .abcd/work/issues/, a CHANGELOG entry
per user-facing change, and the round summary appended to DECISIONS.md.

Assisted-by: Claude:claude-fable-5
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 22, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
abcd 8e716e1 Aug 22 2026, 12:32 PM

REPPL added 7 commits August 22, 2026 11:06
The Plan-refusal path built its receipt with the raw target path, so the
refusal envelope (text and --json) shipped the absolute home root beside
the already-redacted refusal detail — the one shape the CLI error-surface
scrub never touches, and the one the plugin relays into agent chat.
Construction now goes through a refusalReport seam that applies
fsutil.RedactHome, pinned by a test that fails when the redaction is
removed. Closes the residual the merge-gate review found in
iss-2608220142158516's fix.

Assisted-by: Claude:claude-fable-5
capture.List returned nilable issues/skipped slices, so an empty ledger
marshalled {"issues": null, "skipped": null} — on exactly the surface
commands/capture.md tells an agent to iterate. Seed both non-nil, matching
the Status envelope's recent_open seeding, and add the verb to the
class-wide empty-collections test (watched fail before the fix). Closes
the residual the merge-gate review found in iss-2608220147106835's fix.

Assisted-by: Claude:claude-fable-5
The cap-boundary test exercised only the fstat-time cases, which the
pre-fix read also handled — reverting the cap+1 probe left the suite
green, so nothing guarded iss-2608220144233519's fix. Extract the capped
read into capRead and drive it with a reader carrying one byte more than
the fstat-time cap: the grown file must be refused whole, not returned as
a truncated prefix scanned clean. Verified to fail against a capped read
without the +1. Also corrects the stale never-more-than-maxScanBytes doc
comment.

Assisted-by: Claude:claude-fable-5
Captures iss-2608221126066379 (frontmatter BOM tolerance now diverges from
the sibling parsers whose comments promise byte-exact parity) and
iss-2608221126066631 (guard process-substitution redirection family,
pre-existing) as open ledger records, and appends the merge-gate
remediation entry to the decision log.

Assisted-by: Claude:claude-fable-5
Resolves the one conflict — update.go's import block, where round 7 added
fsutil (the update-history home-path redaction) and main added term (the
banner's TTY-check consolidation). Both fixes are kept; preflight and
gofmt are clean on the merged tree.

Assisted-by: Claude:claude-fable-5
Picks up #428 (the interview-workstream records), which landed while the
previous merge was in flight. Clean auto-merge; preflight and gofmt are
green.

Assisted-by: Claude:claude-fable-5
@REPPL
REPPL enabled auto-merge August 22, 2026 12:32
@REPPL
REPPL added this pull request to the merge queue Aug 22, 2026
Merged via the queue into main with commit e090280 Aug 22, 2026
12 checks passed
@REPPL
REPPL deleted the bughunt-b/round-7 branch August 22, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant