hosystem Engagements

ho-04.11 — CLI trust boundary & scan resilience

created 2026-07-03
status complete
type ho-document
project sharibako
ho 04.11
kamae 5
shape ha

Fourth post-sweep hardening (fable sweep, 2026-07-03; Tier B candidate #6 plus the scan-resilience deferral from ho-04.9). The theme is the boundary between the CLI and whoever is driving it: what a script can trust about exit codes, what a human can trust about prompts, and what neither should have to fear from one bad file in a scan root.

A pre-session scout confirmed the findings and softened one: the --force/--yes "naming drift" is in fact a nearly coherent grammar — --force authorizes a destructive variant, --yes skips a confirmation, and key generate needing both is the grammar working. The decision ratifies and documents rather than renames.

Out of scope:


Phase 1 —

Seven decisions, ratified in session.

Decision 1 — clean fails loudly on non-interactive stdin. Without --yes, clean prompts; on non-TTY stdin the prompt reads nothing, declines by default, and exits 0 having cleaned nothing — a hygiene false positive for every script that calls it. New behavior: --yes absent and stdin not a TTY → user error naming the fix (--yes), the same pattern init's notInteractiveTerminal established. Declining by default is only safe when a human actually declined.

Decision 2 — .aborted exits 130 and drops the "Error:" prefix. An abort currently exits 0 while printing "Error: Aborted." — the text says failure, the code says success, and both are wrong: an abort is the user choosing not to proceed. New userAbort = 130 in the exit taxonomy (the 128 + SIGINT shell convention; what fzf and shells already mean by user-cancel), message rendered as plain Aborted. on stderr.

Decision 3 — no-echo interactive value entry as the default path. --value transits argv and shell history — the convenient path is the leaky path, inverting the product's stated posture. add/rotate on a TTY with no value flag now prompt Value: with echo off (termios); non-TTY without a flag keeps valueInputRequired. --value stays for scripts that accept the tradeoff; SECURITY.md gains an honest paragraph naming the argv/history exposure and pointing at the prompt and --from-stdin as the hygienic forms.

Decision 4 — init's scope-type prompt re-asks on an unrecognized answer. A typo'd scope type silently became project-dev. Same loop the scope-ID prompt got in ho-04.9: empty answer accepts the default (a deliberate Enter), an unrecognized non-empty answer re-prompts with the valid options listed.

Decision 5 — scan skips bad markers and reports each one. Ho-04.9 made markers stricter; since then one malformed or hostile marker aborts an entire scan — and status, clean, and scope resolution all ride on scan. Materializer.scan now returns a ScanReport carrying loaded markers plus per-marker failures (path + reason); verbs print each failure as a stderr warning and proceed. The hostile-marker signal ho-04.9 created becomes a visible finding instead of a denial-of-service on the vault's own tooling. Direct loadMarker paths keep throwing — acting on a bad marker is an error; surveying past one is not.

Decision 6 — flag grammar ratified, not renamed. --force authorizes the destructive variant (add, materialize, key generate); --yes skips an interactive confirmation (clean, key generate). The two consents are genuinely different and key generate needs both. Help text states the grammar uniformly; the README's command section carries a line. No renames — no breaking change to purchase consistency that already exists.

Decision 7 — InteractiveSelect is removed, tests and all. Dead since ho-04.6's plain-prompt redesign; nothing references it but its own tests. Decision 3's echo-off reader is written fresh (~a dozen termios lines) rather than kept hostage to a whole selection widget. Git history retains the widget if a future flow wants one.


Phase 2 — Execute

Single-session execution, no agent-task children — the 04.9/04.10 posture. Three work areas:

Prompt and exit contract (Decisions 1, 2, 4). CleanCommand gains the TTY guard; SharibakoExitCode gains userAbort = 130 and ErrorReporter renders .aborted without the error prefix; InitCommand.promptScopeIDAndType gains the scope-type re-prompt loop.

Value entry and dead code (Decisions 3, 7). A fresh SecureValuePrompt (echo-off termios reader, injectable for tests) wired into ValueInput as the TTY default for add/rotate; InteractiveSelect.swift and its test file deleted; SECURITY.md paragraph on argv/history exposure.

Scan resilience (Decision 5). Materializer.scan returns ScanReport { markers, failures }; scan/status/ScopeResolver/resolveMarker(forScope:) consume it; ScanCommand and StatusCommand print failure warnings to stderr (and carry them in --json output).

Documentation (Decision 6). Flag help text unified; README command section updated.

Testing and iteration approach

Extend suites in place: CleanCommand non-TTY spec (injectable TTY check), abort-exit-code specs at the ErrorReporter seam, init scope-type re-prompt via scripted lineReader, ValueInput TTY-prompt routing via injected reader, scan skip-and-report with one good and one hostile marker in a root (victim assertions that status still lists the good scope). InteractiveSelectTests deleted with its subject; coverage recomputed after removal.

Done means


Phase 3 — Reflect

Did the hold? All seven decisions implemented as ratified. Four things surfaced:

Decision review. 130 flows through cleanly — ErrorReporter.report exits with the taxonomy code directly, ArgumentParser isn't in that path. Removing InteractiveSelect plus excluding the TTY-only SecureValuePrompt raised coverage: 94.28% against 04.10's 93.77%.

What broke that the tests didn't catch? Nothing at runtime; the lint layer again caught the structural debt (file length, one trailing- idiom).

Numbers. 560 tests in 67 suites (563/68 before — InteractiveSelect's suite left, the trust-boundary and scan specs arrived); coverage 94.28% by CI's computation with SecureValuePrompt added to the named-exclusion list (same justification class as TerminalDetector). One known deprecation warning (kSecUseAuthenticationUIFail) stands until ho-04.12's Keychain modernization, per the sweep plan.

Followups.


Authored: 2026-07-03 (Think phase ratified in session). Executed: 2026-07-03, same session.

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ a97b22af9b61 · ho-system @ 79e96b801a13 · the glossary · the colophon