hosystem Engagements

ho-04.8 — encrypt-path hygiene + coverage-exclusion policy

created 2026-07-03
status complete
type ho-document
project sharibako
ho 04.8
kamae 5
shape ri

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:


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

  1. Plaintext travels only through a pipe. Shell.run gained optional stdin: Data? (background-queue write mirroring PipeDrain, F_SETNOSIGPIPE on the write fd). encryptAndWrite pipes the YAML to age --encrypt via stdin. No plaintext temp file exists anymore, so no scrub/permission policy for it is needed.

  2. Ciphertext destination writes are atomic. age -o truncates its target before writing, so a crash mid-rotate could destroy the only good ciphertext. age now writes a staging sibling (.sharibako-tmp-<uuid>, same directory ⇒ same volume) renamed over the destination via POSIX rename(2) — chosen over FileManager.replaceItemAt, which requires an existing destination and addSecret targets are new files.

  3. Crash leftovers stay visible and never sync. A leftover staging file is ciphertext-only. Conduit's git add -A excludes the staging pattern via pathspec (:(exclude)*.sharibako-tmp-*); no vault .gitignore machinery. Rationale: visible strays get healed, hidden ones rot.

  4. Materialized targets are 0600. Materializer.writeAtomically creates its temp sibling with 0600 before plaintext lands in it (chmod-after-write leaves a default-permissions window) and rename(2)s into place, re-tightening a looser pre-existing target on every materialize. Vault .age files 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.

  5. Coverage exclusions are named, not blanket. CI's llvm-cov now carries -ignore-filename-regex for 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.

  6. 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 )

Verification

Rendered from the corpus, verbatim · source on GitHub →

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