fix: fail-closed capture surface (iss-29) - #41
Merged
Conversation
Move the error->exit-code mapping out of main and into a cli.Run(args, stdout, stderr) int seam so the front door's error surface is injectable and testable. Behaviour-preserving: main becomes a one-line delegate and the raw-text diagnostic is unchanged. Assisted-by: Claude:claude-opus-4-8
Three coupled instances behind the unrecognized-input-never-writes
detector, each with a watched-fail acceptance test:
- A mistyped capture subcommand (e.g. `capture resovle iss-1 note`) was
swallowed as free text and filed as a new issue. suspectedTypoedSubcommand
now refuses it with a did-you-mean and writes nothing. The guard is
high-precision — it fires only on a near-miss token that is alone or
followed by an iss-id, so genuine free-text prose still files.
- Errors requested with --json emitted raw Go text. cli.Run now renders the
diagnostic as a {"error": ...} JSON envelope under --json.
- `docs lint` with a missing/unreadable config surfaced a raw os.ReadFile
error that leaked the absolute config path. It now reports a clean,
repo-relative diagnostic; a *PathError's inner cause is reported without
its path.
Resolves iss-29 (folded into the branch). The systemic PathError-into-json
leak across other verbs is split out as iss-76.
Assisted-by: Claude:claude-opus-4-8
Filed during the iss-29 security review: cli.Run routes all command errors through the --json envelope, so any verb returning a bare *fs.PathError emits the absolute local path into machine JSON. iss-29 sanitised the docs-lint branch; the systemic boundary fix is iss-76. Assisted-by: Claude:claude-opus-4-8
This was referenced Jul 12, 2026
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.
Fixes ledger issue iss-29
fail-closed-capture-surface— three coupled instances behind theunrecognized-input-never-writesdetector, each armed with a watched-fail acceptance test.What was broken
abcd capture resovle iss-1 note—resovlebeing a typo forresolve— fell through thecaptureparent's arbitrary-argsRunEand was filed as a brand-new issue.--jsonerrors emitted raw Go text, not JSON — a machine caller could not parse a failure the way it parses a success.docs lintwith no config leaked the absolute config path in a rawos.ReadFileerror.The fix
suspectedTypoedSubcommandrefuses a mistyped subverb with a did-you-mean and writes nothing. It is deliberately high-precision: it fires only when the first token is a near-miss (edit distance 1–2) to a real subverb and the shape looks like a subcommand call — a lone token, or a token followed by aniss-Nid — so genuine multi-word free-text captures still file.cli.Run(args, stdout, stderr) intseam (extracted frommain.goin a separate behaviour-preserving refactor commit) renders the error diagnostic as a{"error": …}envelope under--json.docs lintnow reports a clean, repo-relative diagnostic for a missing or unreadable config; a*PathError's inner cause is reported without its path.Detector-first evidence
internal/surface/cli/capture_surface_test.go— 5 acceptance tests (typo-never-writes, free-text-still-writes guard,--jsonerror shape, missing-config clean error, unreadable-config no-path-leak) watched fail before the fix, green after.make preflightclean (build, gofmt, vet, test,-race).Review
*PathError; fixed (strip the path-bearing wrapper) and re-reviewed PASS.Scope split
The reviewers noted a systemic version of instance 3:
cli.Runnow routes all command errors through the--jsonenvelope, so any verb returning a bare*fs.PathErrorwidens a pre-existing stderr-text abspath leak into--json. That is out of iss-29's scope and pre-existing, so it is filed separately as iss-76 rather than fixed here.Commits
refactor:extractcli.Runfor testable error rendering (behaviour-preserving)fix:fail-closed capture surface (iss-29), resolve folded into the branchchore:capture iss-76Do not merge / no auto-merge — maintainer merges
fix:PRs.Assisted-by: Claude:claude-opus-4-8