fix: bug-hunt round 9 — ssh-safe secret scan, terminal capture convergence, npm stream split, truthful apply preview, deps overlay ignore, recoverable release publish - #38
Merged
Conversation
…le set The changed-file scan opened paths with a symlink-following read, so an untracked repo symlink pointing at ~/.ssh/id_ed25519 was read in full, breaching the ssh boundary, and a dirty submodule or symlink-to-directory EISDIR-wedged every sync fail-closed. Changed paths are now Lstat-gated (git commits a symlink as its link text, which the push-range blob scan covers; a gitlink's content never enters the push range). The collapsed untracked-directory walk is replaced by git ls-files --others --exclude-standard, so gitignored files and nested .git trees no longer false-block a sync that would never publish them. Assisted-by: Claude:claude-fable-5
A shared-route terminal capture wrote the shared plist while a per-machine overlay from an earlier local capture kept winning both the drift comparison and apply, so the domain re-offered forever and the shared bytes did nothing; a shared accept now removes the superseded overlay and says so, leaving symlinked overlays untouched. A secret-routed capture wrote a placeholder that status and capture compared raw against the live export, reporting drift forever while apply rendered it in-sync; both compare sites now render placeholders through the secret store first (raw-compare fallback when the store or ref is unavailable), and the placeholder write round-trips byte-exactly. Assisted-by: Claude:claude-fable-5
The dump parsed CombinedOutput, so npm's stderr made the documented non-zero-exit tolerance unreachable and any peer-dependency warning disabled the domain. The listing query now captures streams separately (optional SeparateRunner on the exec runner; plain fakes fall back unchanged) and a genuine failure splices npm's stderr into the error instead of a bare exit status. Assisted-by: Claude:claude-fable-5
The data-loss guard that aborts apply when a near-empty repo file would replace a substantial live file was invisible to the preview, which showed a plain would-update; the plan now renders such items as would refuse and the abort path states that the changes reported earlier in the run were rolled back (the abort policy itself is deliberate and unchanged). The guided walkthrough tells the user that confirming a conflict does not overwrite it, a clean skip-always target is silent and counted in sync, and the receive planner accumulates lock releases so a second union-merge item cannot leak a lock. Assisted-by: Claude:claude-fable-5
deps/Brewfile.<os>.local is documented as belonging to one machine only, but the generated .gitignore covered only ferry.local.toml and local/, so sync committed the overlay, every clone installed it via apply --deps, and bundle export carried it. The ignore pattern now ships from the single chokepoint every init route uses, and the init --github pre-create gate model is rendered from the writer's own pattern set so the two can never diverge. Assisted-by: Claude:claude-fable-5
A failed post-publish check left the release run permanently red (the create errors once the Release exists) while a full auto-release re-run saw the release as done and skipped the bypassed gates; publishing is now idempotent and the heal predicate requires a published, asset- complete release. The zizmor SARIF upload is skipped on fork pull requests where the token is read-only (the audit still gates), the consistency lint's old-path check exempts only Go test files, install.sh names a failed binary download, the checksums Makefile comment states that the build prerequisite rebuilds with this invocation's VERSION, and the ci.yml cross-compile comment stops claiming a pre-tag gate that auto-release's concurrent tag outruns. Assisted-by: Claude:claude-fable-5
The compatibility contract names restore snapshots as the second version-independent store; the configuration reference states the iTerm2 allowlist is compiled in, lists all eight kept categories, gives the committed-plist route for extra keys, and documents the gitignored deps overlay with its untracking step; the single-branch main constraint on sync enters the commands reference, the tutorial, and the sync help; route jargon is replaced with plain language; ssh.md states the ~/.ssh invariant universally including the cargo- store guard; the release how-to corrects the failed-check claim, documents post-publish recovery and the local NEXT.md reset, and stamps the by-hand checksum recipe; the scaffold help names exactly what it creates; AGENTS.md's CI list names the gitleaks and zizmor gates; the tutorial distinguishes local drift from a conflict. Assisted-by: Claude:claude-fable-5
Assisted-by: Claude:claude-fable-5
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug-hunt round 9. Fresh hunt across four dimensions (code, docs-vs-code, infrastructure, internal-doc consistency): 36 candidates, each adversarially reviewed by an independent refuter before any fix — 28 confirmed (13 substantive, 15 nitpick), 8 refuted/rejected.
Confirmed substantive, fixed
~/.ssh(cmd/sync.go:1172): the changed-file scan used a symlink-following read, so an untracked repo symlink at~/.ssh/id_ed25519was read in full on the default sync path — a direct breach of the untouchable-~/.sshboundary. Changed paths are now Lstat-gated; non-regular entries skip (link text is covered by the push-range blob scan).cmd/sync.go:1128): a dirty submodule (M sub) or symlink-to-directory (?? link) arrives without the trailing slash the scan keyed on, EISDIR-failing fail-closed with unsatisfiable advice. Same Lstat gate; gitlink content never enters the push range.cmd/sync.go:1128-1171): the walk read gitignored files and nested.gittrees thatgit add -Awould never stage (a nested repo'shttps://user:token@remote was a dead end). Enumeration now usesgit ls-files --others --exclude-standard.cmd/capture.go:1063/1079/1114): the compare used the local-wins source while the shared route wrote the shared path — permanent re-offers, stale overlay kept applying. A shared accept removes the superseded overlay (reported; symlinked overlays untouched).cmd/status.go:201-218,cmd/capture.go:1079-1091): status/capture byte-compared the raw{{ferry.secret …}}placeholder against the live export while apply rendered it. Both now render via the secret store before comparing; the placeholder write round-trips byte-exactly.internal/deps/npm.go:53-64,runner.go): CombinedOutput fused npm's stderr into the JSON parse, making the documented non-zero-exit tolerance dead code. Stdout-only parse; stderr spliced into real errors.ferry diffcannot predict the empty-over-substantial refusal (cmd/apply.go,internal/dotfile/apply.go): the preview showed "would update" for an item apply refuses; abort left scrollback claiming writes that were rolled back. Preview renders "would refuse"; the abort states its rollback. The abort policy itself was adjudicated deliberate and is unchanged.cmd/init.go:918,cmd/init_github.go:355):deps/Brewfile.*.local— documented "one machine only" and code-commented "gitignored" — matched no generated ignore pattern, so it was pushed, cloned, installed fleet-wide, and bundled. Pattern ships from the single chokepoint on every init route; the--githubgate model is rendered from the writer's pattern set. End-to-end eval reproduced the publish pre-fix..github/workflows/release.yml:418-489,auto-release.yml:97-99): a red post-publish check could not be re-run (create 422s on the existing release) while a full re-run went green with the bypassed gates unrun. Publish is idempotent (view → upload --clobber + edit, else create); the heal predicate requires a published, asset-complete release; recovery documented.docs/how-to/cutting-a-release.md:161-166,Makefile:61-62):make checksumsre-runs the phony build without VERSION, silently discarding stamped binaries under a self-consistent manifest. Recipe is the singlemake checksums VERSION=vX.Y.Z; Makefile comment corrected.docs/reference/compatibility.md:45-62vsinternal/backup/snapshot.go:21-23,93-97): the page granted one exception (baseline) while the snapshot manifest (snapshots/ID/manifest.json) is unversioned andrestore --undoreplays it ungated. Snapshots are now the named second exception.docs/reference/configuration.md:187,193-194): no manifest surface exists. The page now states the allowlist is compiled in and documents the route that works (committed plist /local/overlay, imported verbatim by apply; capture rewrites to the filtered set).ferry syncis stated nowhere user-facing (cmd/sync.go:23,290-293): every sibling precondition was documented; the single-branch constraint was not. Now in the commands reference, tutorial (with the remote-side rename steps), and sync help.Confirmed nitpicks, fixed
Conflict items in the guided walkthrough say confirming will not overwrite them (and the commands reference lists the fourth risky class); clean skip-always targets are silent and counted in-sync; receive's unlock accumulates releases (latent double-lock leak); consistency-lint invariant 3 exempts only
*_test.go; zizmor's SARIF upload skips fork PRs (audit still gates); install.sh names a failed download; ci.yml's cross-compile comment stops claiming a pre-tag gate (auto-release tags the same push in ~20s, measured); the release how-to documents step 7's local handover reset; allowlist categories completed; route jargon replaced; ssh.md's enumeration made universal (incl. the cargo-store guard); scaffold help names what it creates; AGENTS.md's CI list names gitleaks/zizmor; the tutorial distinguishes drift from conflict; the wizard row states the tty fallback;evals/harness.go's ApplyConfirmed comment corrected.Considered and rejected (refuted)
restore --packagesover-count: unreachable via ferry-written state (both rails record bare names); tampered-input direction is under-removal, the safe side.edited-arming refutation — no privilege delta, one-admin actor set, and a disarm step is racy and clobbers hand-armed PRs.+build-metadata tag becomeslatest+ escapes retention: round-8 adjudication stands — the workflow comment weighs exactly this consequence, and retention-escape applies to every suffixed tag by documented design.--wizard=interactivesilent fallback as substantive: disclosed twice on the same reference page; the fallback is conservative and secret-safe by deliberate design (redirected runs must not paint a TUI into a file); wording tightened as a nitpick instead.fileDomainIsRepoAuthoritativeregistry rebuild: hunter-self-refuted (allocation-only, no I/O, once per plan item).Carried forward
terminalRepoStatusSource's extensionless/poisoned-overlay divergences fromterminalExportBlob(hand-authored state only; one shared resolver would close it); Apple Terminal's unfiltered export routinely trips the entropy gate on NSKeyedArchiver base64 — the domain may be uncapturable on customised machines (new lead from the refutation pass, unadjudicated); the deps-overlay glob duplicated incmd/init.gorather than shared frominternal/deps;release.sh'srender_next_md"also sourced by tests" claim matches nothing, and a NEXT.md missing its carry markers exits the driver non-zero after a successful tag push; the heal predicate hardcodes the 5-asset count; sync's branch-rename hint omits the remote-side default-branch step;managedcarry remains path-keyed.Gates
make build,gofmt -l(empty),go vet ./...,go test ./...,go test -race ./internal/..., full eval suite withFERRY_BINset,scripts/consistency-lint.sh, andmake gen-docs(no drift) — all green locally. Every behaviour change carries a test watched failing before the fix; the overlay fix additionally carries an end-to-end eval reproduced against a pre-fix binary.