ho-04.8 — encrypt-path hygiene + coverage-exclusion policy
First of the post-sweep hardening hos (fable sweep, 2026-07-03; Tier B candidates #1 and #9). Removes the plaintext temp file from the encrypt path, makes ciphertext destination writes atomic, enforces 0600 on materialized targets, and converts the CI coverage floor's silent headroom into an auditable exclusion list.
Authored light (decision record, not full 5 ceremony) per the FABLE 2 planning session: the sweep's findings document scoped the work; this document records what was decided and why.
Out of scope:
- Age-key temp-file signal hygiene (
AgeKeyBootstrap,KeychainAgeKeyProvider$TMPDIRwrites) — that is Tier B #2, ho-04.12, -gated. - Identifier/path on the inputs that name these files — Tier B #3, ho-04.9.
Forward-only supersession
Closed Ho-01--02 steps 2–4 specified the encrypt path verbatim as: write plaintext YAML to a temp file, the path to age, age -o straight to the destination. That specification is superseded here, not edited there. What changed since ho-01 closed: the sweep's security review measured the shipped behavior against SECURITY.md's own private-report criteria and found the spec itself — not the implementation of it — was the defect.
Decisions
-
Plaintext travels only through a pipe.
Shell.rungained optionalstdin: Data?(background-queue write mirroringPipeDrain,F_SETNOSIGPIPEon the write fd).encryptAndWritepipes the YAML toage --encryptvia stdin. No plaintext temp file exists anymore, so no scrub/permission policy for it is needed. -
Ciphertext destination writes are atomic.
age -otruncates its target before writing, so a crash mid-rotatecould destroy the only good ciphertext.agenow writes a staging sibling (.sharibako-tmp-<uuid>, same directory ⇒ same volume) renamed over the destination via POSIXrename(2)— chosen overFileManager.replaceItemAt, which requires an existing destination andaddSecrettargets are new files. -
Crash leftovers stay visible and never sync. A leftover staging file is ciphertext-only. Conduit's
git add -Aexcludes the staging pattern via pathspec (:(exclude)*.sharibako-tmp-*); no vault.gitignoremachinery. Rationale: visible strays get healed, hidden ones rot. -
Materialized targets are 0600.
Materializer.writeAtomicallycreates its temp sibling with 0600 before plaintext lands in it (chmod-after-write leaves a default-permissions window) andrename(2)s into place, re-tightening a looser pre-existing target on every materialize. Vault.agefiles stay default-permission: they are ciphertext, and git does not preserve mode bits across clones — a 0600 there would be a promise the sync path cannot keep. -
Coverage exclusions are named, not blanket. CI's
llvm-covnow carries-ignore-filename-regexfor exactly three files, each with an in-workflow comment naming why it is unreachable in CI by :KeychainAgeKeyProvider(Keychain + Touch ID entitlement exists only in the signed install),SignalForwarder(handler bodies need real signals against a live child),TerminalDetector(isatty is constant under CI pipes). The 90% floor applies to everything outside the list. Post-change: 94.57% line coverage vs 90.10% under the blanket floor. -
Linux: scoped to macOS until real. Tier B #8 decided here without building: docs are already honest post-sweep; passphrase-protected identities and a Linux CI job become a when a real Linux consumer exists.
Closed-ho reconciliation (from the sweep, named here per )
ho-04-cli.md:116plannedaddas a no-private-key verb; shippedAddCommandloads the identity, so Touch ID fires on encrypt-only operations. Named as an open optimization — not fixed in this ho.ho-04-cli.md:122said write verbs don't get--json; shipped--jsonis global. The shipped behavior the plan; harmless.- kamae-2's init walkthrough was superseded in three places by ho-04.2 (re-init reconcile) and kamae-2.2 (no materialize step; no auto commit/push). Public docs already reflect this; the supersession is now part of the record.
SignalForwarder's doc comment claimed "coverage-excluded" with no mechanism behind it. Decision 5 is the mechanism.
Verification
- 516 tests green (
swift test), including new specifications: stdin round-trip and >64 KiB deadlock guards onShell.run; failed-rotate-preserves-ciphertext (fails against the pre-ho behavior); no-staging-leftovers on success; Conduit commit excludes staging strays whilegit statusstill shows them; materialize enforces 0600 and re-tightens 0644. swift-format lint --strict,swiftlint lint --strict, warning-free build.- Coverage 94.57% by CI's exact computation with the named exclusions.
Rendered from the corpus, verbatim · source on GitHub →