Skip to content

fix: bug-hunt round 10 — terminal capture entropy unblock, secret-route overlay supersede, fail-closed apt probe, ack-path redaction, bundle path gate, gitconfig escapes - #39

Merged
REPPL merged 10 commits into
mainfrom
bughunt/round-10
Aug 16, 2026

Conversation

@REPPL

@REPPL REPPL commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Bug-hunt round 10: fresh hunt across the four dimensions (code, docs-vs-functionality, infrastructure, internal consistency), ~30 unique candidates, each cluster adversarially reviewed before any fix. 8 substantive findings and ~19 nitpicks confirmed; ~10 candidates refuted. Baseline was fully green before the round and all gates are green on this head (build, gofmt, vet, unit + race, full eval suite with FERRY_BIN, consistency lint).

Substantive fixes

  1. Apple Terminal capture unblocked from the entropy gate. cmd/capture.go:1055-1058 filtered only iTerm2; the raw export's NSKeyedArchiver base64 data elements (any customised profile) scored ≥4.0 bits/char entropy (internal/secret/scan.go:561-573), and entropy findings are High confidence → BlockedFromRepo (internal/secret/gate.go:44-56), so the domain was uncapturable except reject/secret-store — and the store route makes it non-portable. The gate now scans a data-masked view (terminalGateInput in cmd/capture.go); string values stay fully scanned, an unterminated data element ends the masking, and the accepted bytes are unchanged.
  2. Secret-routed terminal capture supersedes a shadowing local overlay. captureBlockedTerminal (cmd/capture.go:1143-1166) wrote the placeholder behind a surviving overlay, so apply (cmd/apply.go:1748-1779) kept importing — and re-applying — the stale bytes while status/capture re-offered the domain forever. The overlay-removal loop is factored out of acceptTerminalShared and shared; guard-first semantics preserved.
  3. Status-side terminal resolver probes both overlay spellings. cmd/status.go:265-278 probed only the .plist spelling where apply probes the extensionless spelling too, splitting what apply imports from what status/capture compare.
  4. apt installed-set probe fails closed on an output-less error. internal/deps/install.go:248-270 read a no-output probe failure as "absent": a before-snapshot failure recorded a pre-existing package as ferry-installed (feeding a privileged restore --packages removal), and a symmetric lookup failure silently lost the record with no SnapshotUnreliable warning. The brew prober had exactly this guard and regression test; apt now has both, and the test fake models combined output.
  5. work pack --acknowledge no longer echoes a withheld path. cmd/work.go:236-249 printed the raw item/path for a PathSecret finding in the same message that renders it "(name withheld)" (internal/work/pack.go:66-78). The unmatched label masks the secret-shaped component via the existing redaction helper.
  6. bundle import gates secret-shaped path components. internal/bundle/validate.go checked only canonical path shape while export withheld such entries (cmd/export.go:275-285); a handcrafted bundle could plant a token-named file that wedged every later sync (cmd/sync.go:1046-1048) far from the cause. The per-component predicate lives in internal/secret and both sides share it; the refusal withholds the path.
  7. gitconfig header parsing honours git's in-quote escapes. internal/gitconfig/parse.go:193-212 and parseSectionHeader each mis-scanned an escaped quote (and an unquoted ] in quotes) in a subsection name; a same-line assignment was swallowed into the header's raw bytes, which for non-includeIf sections passed the identity firewall into SharedContent — the non-conservative direction. One shared close-bracket scanner serves both; round-trip fuzz re-proved byte-exact reassembly (1.5M execs).
  8. CHANGELOG [Unreleased] no longer contradicts itself. The round-7 entry described the directory walk the round-9 entry replaced (cmd/sync.go:1127-1136 uses git ls-files --others --exclude-standard); both would have shipped verbatim in the Release body (release.yml:386). Consolidated to one entry; the stale walk-parity comment in cmd/sync_round3_test.go corrected; two stray blank lines removed.

Ledger correction folded into the docs: the round-9 decision-log line "the release how-to documents step 7's local NEXT.md reset" described a sentence that did not exist; the how-to now documents the reset (verified against scripts/release.sh:193-225), making the ledger line true.

Nitpick fixes

release.sh refuses a version that is not the newest dated CHANGELOG heading (the driver tags HEAD — the mis-pointing auto-release.yml:73-74 refuses) and pre-checks NEXT.md's carry markers before the irreversible push; its "sourced by tests" comment matched nothing. Sync's branch-rename hint names the remote default-branch step. Scaffold help names NEXT.md. Docs: drift explanation gains the risky-gate qualifier (cmd/guided_apply.go:125-126, cmd/apply.go:705,759 refuse non-interactively — the page claimed unconditional deployment); release how-to documents the post-tag NEXT.md reset and the widened heal predicate (missing/draft/short of assets, auto-release.yml:112-114); configuration reference documents the terminal secret route + placeholder and scopes the bundle local-layer gate to local/** + ferry.local.toml; deps/README names the brew = true gate; evals map lists the 40th file (deps_local_overlay_ignore_test.go); roadmap clauses dropped; init --fresh noted in commands.md; allowlist.go "extend" advice disambiguated. Hygiene: registry_test six-domain enumeration, writeDomain dead sort, groupRisky fallback comment/test rename (branch kept), unreachable SkipDir arm in agents adoption (a latent trap — firing on a non-dir would skip remaining siblings), printPlan consumer comments, Overlay comment's missing gitconfig.

Considered and rejected (refuted by adversarial review)

  • gitleaks/zizmor gate no release path: "blocking" is the required-check sense fixed in the v0.4.0 plan of record; release.yml:128-131 shows the three-gate mirror was a considered boundary; a tag names an already-scanned merged main commit.
  • Release how-to omits the verify jobs: the sentence introducing the step list names verify; round-8 non-exhaustiveness adjudication bars the rest.
  • Prune has undocumented refusal modes: the unnamed exits are listing-integrity error handling, not retention policy.
  • install.sh "verifies each binary": the set it downloads is a singleton, stated one line later.
  • Push-range scan runs the text gate over binary blobs: ScanText is a strict superset of HasBinarySecret; the residual divergence over-blocks (fail-closed).
  • PathSecret findings unacknowledgeable: a correctly-typed ack works (hash-pinned); the real defect was the echo, fixed above.
  • reDump existedBefore on Lstat failure: any non-NotExist failure also fails the dump; documented tolerance, summary-only.
  • planSummary parameter order: deliberate, documented in place, pinned by test.
  • .gitignore non-atomic write: the "every other repo write is atomic" premise is false — init-route writers are uniformly plain; a class-wide change is an owner call.
  • groupRisky unreachable fallback as a defect: defensive default kept; only its comment misattributed the reason (fixed).
  • Bundle carrying a force-tracked deps overlay adjudicated a docs fix, not a wider gate (round 9's eval pins gitignore as the remedy).

Carried forward

A present-but-refused local terminal overlay still falls back to shared in status where apply fails closed (needs a tri-state through reportTerminalStatus; an existing test pins the fallback); work pack's path gate scans the whole rel where export gates per component (new lead from the refutation pass, unadjudicated); the heal predicate's 5-asset hardcode; path-keyed managed carry; the secret scan's Lstat-to-read TOCTOU (O_NOFOLLOW); unpruned journal runs; the duplicated deps-overlay glob; the forward-only placeholder render.

claude added 10 commits August 16, 2026 00:51
The capture secret gate scans a view of the exported plist with <data>
payloads masked: NSKeyedArchiver base64 archives (any customised profile
colour or font) scored as high-entropy credentials, blocking both repo
routes and leaving the domain uncapturable. <string> values stay fully
scanned; an unterminated <data> element ends the masking so malformed
input is scanned in full.

The secret route's placeholder write removes a superseded local overlay
exactly as a shared accept does — the placeholder landed behind the
overlay, so apply kept importing and re-applying the stale bytes while
status and capture re-offered the domain forever. A symlinked overlay is
left untouched.

The status-side resolver probes both overlay spellings apply probes, so
an extensionless overlay no longer splits what apply imports from what
status and capture compare.

Assisted-by: Claude:claude-fable-5
dpkg-query reports a not-installed package with a diagnostic in its
combined output; an exec-level failure returns no output at all, which
the installed-set prober read as absent. A probe failure in the before
snapshot recorded a pre-existing package as ferry-installed — handing a
later privileged restore --packages a package the user already had — and
a lookup failure hitting both snapshots silently lost the run's record
with no warning. A probe error with no output marks the snapshot
unreliable, suppressing the restore record and saying so. The apt fake
models combined output, and the apt rail gains the failed-before-snapshot
regression test the brew rail already had.

Assisted-by: Claude:claude-fable-5
A finding on a secret-shaped file name renders as (name withheld), but
the retry's unmatched list printed the raw item/path in the same error.
The unmatched label masks the secret-shaped component via the existing
redaction helper, keeping the surrounding path visible; the fail-closed
abort, post-check state save, and hash-pinned acknowledgements are
unchanged.

Assisted-by: Claude:claude-fable-5
Export withholds entries whose path carries a secret-shaped component;
import validated only the canonical path shape, so a handcrafted bundle
could plant a token-named file that wedged every later sync far from the
cause. The per-component predicate moves to internal/secret, export
delegates to it, and import applies it first per entry, refusing with the
path withheld.

Assisted-by: Claude:claude-fable-5
The inline-header split and the subsection derivation each scanned for
the closing bracket without git's in-quote escapes, so a legal escaped
quote (or an unquoted ] inside quotes) in a subsection name swallowed a
same-line assignment into the header's raw bytes — which, for sections
the identity firewall does not drop wholesale, passed through to the
shared repo. One shared scanner serves both call sites; parsing stays
total and reassembly byte-exact (round-trip fuzz re-run).

Assisted-by: Claude:claude-fable-5
The driver tags HEAD, so accepting any dated CHANGELOG version let a
hand-run tag an older still-untagged version onto newer main code — the
mis-pointing the automatic path refuses by tagging only the first dated
heading; the CHANGELOG gate asserts the requested version is the newest.
The NEXT.md carry markers are checked in the preconditions gate, so a
malformed local file fails the run before the irreversible tag push
instead of after it. The functions comment stops claiming test coverage
that does not exist.

Assisted-by: Claude:claude-fable-5
The branch-rename hint names the remote follow-up (set main as the
remote's default branch so other clones follow), and the scaffold help
lists NEXT.md in the runtime layout both modes create — the --private
list already named it, so the omission read as a false negative. The
generated scaffold reference page is regenerated.

Assisted-by: Claude:claude-fable-5
The drift explanation subjects first-touch adoption to the risky-change
gate (confirmed interactively, refused non-zero unattended) instead of
claiming unconditional deployment. The release how-to documents the
driver's post-tag NEXT.md reset and the real re-release condition
(missing, still a draft, or short of its assets). The configuration
reference documents the terminal secret route and its placeholder, and
scopes the bundle's local-layer gate to local/** and ferry.local.toml.
deps/README names the brew = true gate its npm section already mirrors.
The evals file map lists the deps-overlay eval. Roadmap clauses leave the
commands reference and the tutorial, and the commands reference notes
init --fresh.

Assisted-by: Claude:claude-fable-5
The registry-order test comment names all six FileDomains; the secret
store drops a sort into a fresh map that could not affect encoding (the
TOML encoder documents key sorting) and credits the real mechanism;
groupRisky's fallback comment and test name describe the defensive
default instead of a preference-item path that cannot reach it; the
round-3 sync test comment describes the ls-files enumeration; the agents
adoption walk drops an unreachable SkipDir arm that would skip remaining
siblings if it ever fired; the printPlan consumer comments name diff and
init's plan preview; the allowlist comment says extending means changing
ferry's compiled-in source; the Overlay comment lists gitconfig among the
include-sidecar names.

Assisted-by: Claude:claude-fable-5
The round-7 directory-walk entry contradicted the round-9 ls-files entry
that replaced it — both would have shipped verbatim in the Release body —
and two stray blank lines split the Fixed list. The round's fixes gain
their entries, and the decision log records the round.

Assisted-by: Claude:claude-fable-5
@REPPL
REPPL merged commit 82b95fe into main Aug 16, 2026
15 checks passed
@REPPL
REPPL deleted the bughunt/round-10 branch August 16, 2026 01:17
@REPPL REPPL mentioned this pull request Aug 16, 2026
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.

2 participants