hosystem Engagements

ho-04 — The Tool: CLI MVP wired to the core

created 2026-07-02
status complete
type ho-document
project sharibako
ho 04
kamae 5
shape ha
agent-tasks
  • Ho-04-AT-01.md
  • Ho-04-AT-02.md

The first practitioner-visible surface. SharibakoCLI grows a Swift ArgumentParser subcommand tree that binds SharibakoCore to the terminal: macOS Keychain-gated age key with Touch ID, all the non-interactive verbs (status, scan, list, get, heal, add, rotate, link, unlink, materialize, update, sync, clean), plus key management (key generate, key import, key export). Human-readable output by default, --json on inspection verbs, exit code taxonomy for scripting. When this lands, the practitioner can use sharibako against real secrets from the terminal — dogfooding begins.

Out of scope:

Resolves deferred decisions (from the ho-overview):

Structurally part of the ho-overview's ho-04 scope:


Phase 1 —

Decision 1 — Scope split: pull init out, keep everything else

The overview's ho-04 bundled four substantive workstreams: (a) CLI scaffold + non-interactive commands, (b) interactive init with the four-way ingest matrix, (c) Keychain-gated age key + Touch ID, (d) Linux fallback. That's on the order of ho-01 + ho-02 + ho-03 combined.

This ho: (a) + (c) + (d). Non-interactive command surface + Keychain integration + Linux fallback.

ho-04.2 (new): (b). The interactive init flow with the four-way ingest decision matrix (Materializer.ingest → per-key prompts → Materializer.acceptIngest). init's UX load is qualitatively different from the other commands' — they're thin wrappers over SharibakoCore, init is a real terminal-UX design problem. Doing it inline while also getting the boring verbs working splits attention badly.

Keychain stays in ho-04. The age key story only becomes real when there's a surface, and Keychain integration is one focused unit (SecAccessControl with .userPresence, SecItemCopyMatching, a small key-management verb set). It belongs with the CLI that uses it.

Decision 2 — Command surface for ho-04

Every non-interactive verb the practitioner needs to use sharibako in real work.

Inspection (some Touch-ID gated where they reveal plaintext):

Write (Touch-ID gated on decrypt paths):

Key management (permanent utilities, not just dogfooding):

Deferred out of this ho:

clean moves from ho-04.5 to ho-04. Rationale: clean is a one-shot Materializer call with the same as materialize and update. Keeping the Materializer-verb-triad together makes the CLI surface easier to reason about — everything that touches .env in one ho. The run/clean "retract-vs-inject" pairing the overview named still works — SECURITY.md can discuss both even if the code landed a session apart.

Decision 3 — Vault + age key location

Vault directory. Default ~/.sharibako/vault/. SHARIBAKO_VAULT environment variable overrides. --vault <path> CLI flag overrides both. No YAML config file for v1 — env + flag is enough surface and adding a config file is ceremony we don't need until we do.

Age key storage on macOS. Keychain generic-password item, label sharibako.age-key, access control SecAccessControl with .userPresence (biometry-or-password fallback). Retrieval via SecItemCopyMatching triggers Touch ID.

Age key storage on Linux. Default: passphrase-encrypted age key file at ~/.config/sharibako/age-key (via age -p at generate time). Passphrase prompt per command.

Linux hardware-token support. Comes for free through age's plugin architecture. If the user has age-plugin-yubikey, age-plugin-tpm, or age-plugin-fido2-hmac installed and has generated an identity via the corresponding tool, they point sharibako at the resulting identity file (env var or --age-key <path> flag), sharibako invokes age --identity <file> as normal, the plugin handles the hardware handshake, the hardware prompts (YubiKey touch, TPM PCR, FIDO2 tap). sharibako needs no code for this beyond what already exists — the abstraction is at the age binary layer.

Documented in SECURITY.md during ho-04.5 polish. Explicit Linux fingerprint/fprintd integration is not v1 (no clean age integration path).

Key-management on-ramp. sharibako key generate runs age-keygen, stores the private key in Keychain (macOS) or passphrase-encrypts to ~/.config/sharibako/age-key (Linux), and prints the public key to stdout so the user can capture it for backup. sharibako key import <path> migrates an existing key file into Keychain, with confirmation before deleting the source file. sharibako key export --public prints the public key (safe to publish). sharibako key export --private --i-know-this-is-plaintext is the escape hatch for recovery workflows.

Decision 4 — Touch ID cadence: one prompt per command, no daemon in v1

Mechanism. A CLI has no session — each sharibako invocation is a fresh process. macOS Keychain does not have a "cache unlocked state across processes" mode; the LAContext that holds auth state is per-process. On any command that needs the private key (decrypt path), the CLI calls SecItemCopyMatching once at command start. Touch ID prompt fires. Key is loaded into process memory. The command decrypts all owned keys it needs — for materialize, that's all owned keys in one prompt, not one prompt per key. Process exits; memory freed.

Best-effort scrub on exit. The plaintext buffer holding the age key gets memset_s'd before the CLI process exits, wrapped in a defer around the key-load. Not a magic bullet (Swift string internals may have made copies) but signals intent and covers the common path. SECURITY.md already commits to this pattern for run in ho-04.5; extending to ho-04's key handling is the same discipline.

Which commands need the private key. get, rotate, unlink, materialize, update, heal. Which don't: status, scan, list, add (public key sufficient for encrypt), link (writes a .link file, no crypto), sync (git only), clean (uses inspect which reads filenames).

Why per-command and not a daemon. A daemon (long-running background process holding the key across invocations via local IPC — the ssh-agent shape) would give proper session semantics. The overview explicitly defers this ("sharibako-agent Touch-ID-friction daemon, Deferred Decision #3, post-MVP"). Per-command friction is real UX cost, but it also means the age key lives in memory for milliseconds per invocation rather than minutes — a smaller blast radius. This ho ships per-command, feels the friction concretely during dogfooding, and the phase-boundary replan checkpoint after ho-04.5 answers whether the daemon earns its scope. See Followups for what data to bring to that conversation.

Decision 5 — Output, errors, distribution

Output. Human-readable by default with ANSI color when stdout is a TTY, plain text when piped. --json flag on inspection verbs (status, scan, list, heal) — the surfaces where scripting is plausible. Write verbs don't get --json; they signal success/failure via exit code. sharibako get prints the raw value + trailing newline on stdout (so $(sharibako get kanyo-dev API_KEY) in shell scripts just works) — no --json for get, the value is the payload.

Errors. Every failure writes a human-readable message to stderr with actionable remediation ("Vault not found at /Users/atmarcus/.sharibako/vault. Run sharibako key generate first.") Exit code taxonomy:

Small and inspectable in scripts. Every VaultError case maps to a specific exit code in one place. --json errors emit {"error": "...", "code": N} instead of human text on stderr.

Distribution during dogfooding. scripts/install.sh at repo root: runs swift build -c release, copies .build/release/sharibako to /usr/local/bin/sharibako, prints the destination path, exits nonzero on build failure. Not a Makefile — SwiftPM is the whole build system and Make for one target is ceremony. Bundling / signing / notarization / Homebrew tap deferred to Phase 6.

Decision 6 — Testability posture

Keychain access sits behind a protocol (AgeKeyProvider or similar) so tests can substitute a file-based provider that doesn't require biometry. Two implementations: KeychainAgeKeyProvider for macOS production, FileAgeKeyProvider for Linux, tests, and the --age-key <path> flag. Everything else about internal CLI structure — per-command file organization, service-layer patterns, exact test-file layout — is execution-time detail for the agent to decide while writing.

Decision 7 — Scope resolution when no scope arg is given

Many verbs take a scope: materialize <scope>, update <scope>, heal <scope>, clean <scope>, get <scope> <key>. Requiring the scope name every time when the practitioner is standing in a project directory with a .sharibako marker is friction.

Behavior. When a scope arg is omitted (where the CLI allows it — inspection verbs and Materializer-triad verbs), the CLI walks up from cwd looking for .sharibako, uses the scope named in that marker. Same shape as git's .git/ discovery. Materializer.resolveMarker(startingFrom:) from ho-03 supplies the plumbing. Explicit sharibako materialize kanyo-dev still works when the practitioner is not in a project dir or wants a different scope.

Commands that require an explicit scope even in a project dir. get <scope> <key>, add <scope> <key> ..., rotate <scope> <key> ..., link <scope> <key> <shared-id>, unlink <scope> <key> — because these take a <key> arg that follows <scope> in the argument order, dropping scope would introduce ambiguity in a way the Materializer-triad verbs don't have. (An --auto-scope fallback for these could be added later if the friction is real.)

Deferred to execution


Phase 2 — Execute

Two agent tasks with a clean seam between "how the CLI works" (structure, auth, output infrastructure, key management, inspection verbs) and "what the CLI does for encryption-touching operations" (write verbs). Same shape as ho-01/02/03's two- pattern.

Ho-04-AT-01 — CLI scaffold, Keychain integration, key management, inspection verbs

Sources/SharibakoCLI/ grows a real subcommand tree. SharibakoCommand (the top-level AsyncParsableCommand) registers all subcommands but only lands implementations for: key generate, key import, key export, status, scan, list, heal. The AgeKeyProvider protocol with both implementations, --vault / --age-key global flags, exit code taxonomy in one place, human/JSON output rendering, scripts/install.sh. Every Touch ID / Keychain moment is under test infrastructure that swaps in a file-based provider.

/agent-tasks/Ho-04-AT-01.md

Ho-04-AT-02 — Write verbs

get, add, rotate, link, unlink, materialize, update, sync, clean. Each is a thin adapter over a SharibakoCore operation. Scope-resolution-from-cwd where applicable. --force on materialize mapping to overwriteDrift: true. --json on the parts of heal-shaped verbs where structured output makes sense. Real Touch ID prompts for the decrypt path, mocked in tests via FileAgeKeyProvider.

ho-process/agent-tasks/Ho-04-AT-02.md

Testing and iteration approach

AT-01 lands and dogfoods before AT-02 opens — a CLI that can status, scan, list, heal, plus generate + manage a key, is already useful for exploring an existing vault. AT-02 builds on that foundation. Coverage floor stays 90% for the CLI target; realistically we expect around 85% because the Keychain code paths are hard to exercise headless. Integration tests use real subprocess execution against the built binary for a handful of critical paths (key generate, status, materialize + get round-trip); most tests are in-process against service-layer types with injected dependencies.

Done means


Phase 3 —

AT-01 and AT-02 committed at ba5c1e6 and 713f252 respectively. Dogfood ran against the debug binary (swift build without -c release); see Release binary collision note below.

What verified correctly

AT-01 verbs (key, status, scan, list, heal) verified via --age-key bypass during AT-01 dogfooding. heal drift detection and scan marker enumeration both correct. list --shared and list (scopes) both work; list does not take a positional argument (correct — flags only).

AT-02 verbs — full terminal run:

Issues surfaced

Release binary name collision. swift build -c release writes both Sharibako (the SwiftUI app entry point) and sharibako (the CLI) into .build/release/. On macOS's case-insensitive APFS the names collide; the GUI binary wins and .build/release/sharibako hangs on any invocation (no AppKit run loop). The debug binary is unaffected — swift build only produces the CLI. Workaround for now: from .build/debug/sharibako. The real fix is in ho-05 (Xcode project), where the GUI app gets a separate Xcode-managed build output path and the CLI builds separately. Until then, scripts/install.sh (ho-04.3) should build the CLI with swift build -c release --product sharibako and verify the installed binary, not the path.

Marker walker surfaces file-system error on home-directory boundary. Running materialize from a directory with no .sharibako ancestor (the repo root was the test case) produces "File system error at /Users/atmarcus/.sharibako: The file couldn't be opened." rather than the cleaner "No .sharibako marker found starting from…" error that appears when the run is explicitly scoped. The walker appears to attempt opening ~/.sharibako as a candidate and surfaces the open-failure as a filesystem error rather than a no-marker condition. This is a minor UX issue but wants a fix before ho-04.2 ships, since init relies heavily on the marker-resolution path.

link / unlink not dogfooded end-to-end. There is no add-shared verb — shared entries are created by init (ho-04.2). A manually crafted .age file without the SecretContent YAML wrapper caused a "Failed to decode YAML" error when get attempted to decrypt a linked key. Unit tests cover the link / unlink path correctly; end-to-end CLI dogfood will happen when ho-04.2 delivers init.

Touch ID friction log

Not started. All AT-01 and AT-02 dogfooding bypassed Keychain via --age-key. The log called for in the ho-04.5 followup begins once ho-04.3 signs the binary and Keychain prompts are live.

Keychain integration status

Written but not verified on a real signed binary. Deferred to ho-04.3 (see Followups). The --age-key bypass confirmed that every verb that touches encryption works correctly when given a key file; the biometry-gate is the only open question.


Followups tracked for future hos

Deliberate hand-offs. Not code changes; things the later hos should read here and pick up.

For ho-04.2 (sharibako init — interactive)

For ho-04.3 (sign the dev binary; verify Keychain biometry end-to-end)

Surfaced during AT-01 dogfooding: sharibako key generate on macOS hit errSecMissingEntitlement (-34018) when writing a Keychain item with SecAccessControl .userPresence. This is Apple's security model, not a code bug — unsigned binaries categorically cannot use biometry-gated Keychain access. The Keychain code in AT-01 is written but is not verified to work on a real device until this is resolved. AT-01 dogfooding used --age-key <path> bypass; AT-02 will do the same.

A small dedicated ho (well under ho-04.2 / ho-04.5 in scope):

Not urgent. AT-02's dogfooding proceeds on --age-key bypass. ho-04.3 clears the debt at whatever point real biometry is worth verifying before ho-04.2's init flow (which will interact heavily with Keychain during first-run).

For ho-04.5 (sharibako run + SECURITY.md polish + daemon-decision replan)

For ho-05 / ho-06 (Workshop / GUI polish)

Post-MVP

Post-ho-04.3 addendum. Keychain biometry verified end-to-end on 2026-07-02 after ho-04.3 landed. sharibako key generate on the signed binary prompts Touch ID, writes to Keychain, and both key export --private (re-prompt) and key export --public (no prompt) work as specified. Ho-04.3 required more than the original spec anticipated: keychain-access-groups is a restricted entitlement on modern macOS that kills an unsigned binary at load time, and codesign for flat CLI binaries does not support --provisioning-profile directly. The working solution is a thin app bundle (sharibako.app) with embedded.provisionprofile, installed permanently at $BUNDLE_INSTALL_DIR, with a symlink in $DEST pointing back to the binary inside the bundle. macOS resolves the symlink's real path, finds the bundle structure, validates the provisioning profile, and honours the entitlement. The App ID net.sageframe.sharibako and provisioning profile (Sharibako Developer ID, Developer ID type) were registered manually in the Apple Developer portal — Xcode normally does this silently for GUI apps.


Authored: 2026-07-02. Execute: complete (AT-01 ba5c1e6, AT-02 713f252). Reflect: complete 2026-07-02.

Rendered from the corpus, verbatim · source on GitHub →

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