hosystem Engagements

ho-06.7 — Delete: the missing destructive verb

created 2026-07-12
status complete
type ho-document
project sharibako
ho 06.7
kamae 5
shape ha
splits-from ho-06
agent-tasks
  • ho-process/agent-tasks/Ho-06.7-AT-01.md
  • ho-process/agent-tasks/Ho-06.7-AT-02.md
  • ho-process/agent-tasks/Ho-06.7-AT-03.md

Build the verb the app was missing. A user cannot delete a scope or a shared entry from anywhere — not the Workshop, not the CLI, and — the real gap — not SharibakoCore. The commits that "deletion is always an explicit user action and only ever touches the vault" (kamae-2, line 174), but the Vault Core operation that sentence describes was never among ho-01's nine. The only path today is -deleting a directory inside the vault and committing it by hand — exactly the raw-filesystem surgery the app exists to spare its user.

This adds one destructive capability across all three surfaces at once:

This surfaced as the ho-06.5 gate's owed finding. It is new vault behavior, not chrome — which is why it is its own ho and not part of 06.5.

Out of scope:


Phase 1 —

Six decisions. Five were ratified with the practitioner before authoring; the sixth (CLI surface ) follows the established list [--shared] idiom and is logged for the record.

Decision 1 — Two Core verbs, filesystem-only, no crypto

deleteScope(_ id:) and deleteSharedEntry(_ id:force:) join VaultCore. Neither decrypts anything — a scope directory and a shared/<id>.age file are removed as files, not read. Both are reachable through VaultCore.init(vaultURL:) (the keyless initializer, line 42) — the same no-identity path link and unlink-adjacent filesystem work already uses. Deletion never acquires an age key, which is what makes Decision 4's "confirmation, not Touch ID" honest at the Core boundary: there is no key to gate on.

deleteScope removes scopes/<id>/ and its entire contents (scope.yaml, every <KEY>.age, every <KEY>.link). A scope's own .link files are pointers; they vanish with the scope and touch nothing in shared/. Absent scope → the existing scopeNotFound. Identifier is free — it rides VaultLayout's single path-building chokepoint, same as every other scope operation.

Decision 2 — Shared-delete linker guard: refuse by default, --force to orphan

Deleting a shared entry that other scopes link to is the one case with real blast radius, and the existing linkGraph() already answers "who links this." deleteSharedEntry computes the linkers; if any exist and force is false, it throws a new VaultError.sharedEntryLinked(id:, linkers:) naming every (scope, key) pair and removes nothing. With force: true, it removes the shared entry and leaves the linkers as dangling .link files — the explicit orphaning the practitioner ratified. Those dangling links surface through the machinery that already exists for it (linkTargetMissing on resolution, the heal/orphan surfaces). No cascade, no silent cleanup — the 's "deletion is always explicit" holds at the sharpest edge.

Decision 3 — Blast radius: only the vault

Restated from kamae-2 line 174 so the executing agent carries it without the system design open:

Thing Disposition Why
scopes/<id>/ (or shared/<id>.age) Removed The target.
shared/ entries the scope linked to Never touched A .link is a pointer; removing it ≠ removing the target.
Other scopes that link a deleted shared entry Left dangling (only under --force; otherwise refused) Decision 2.
.sharibako markers on disk Left in place Markers live in the user's repos, outside the vault boundary; they become orphans the "Unlinked markers" section already surfaces.
Already-materialized .env files Left in place The user's live working files; clean is the deliberate verb for those.
Git history Retained — recoverable Git is the backup; historical ciphertext is accepted by the threat model.
The commit Not automatic sync commits the removal on the user's boundary.

Decision 4 — Gating: confirmation, not Touch ID

Deletion is a mutation, not a reveal — it decrypts nothing — so it does not prompt for biometry. The gate is explicit confirmation, matching how the design gates on value-reveal, not on mutation. CLI: a mandatory confirm prompt unless --yes, erroring if stdin is not a TTY and --yes is absent, exactly CleanCommand's pattern (exit 130 on decline). GUI: a system confirmation dialog naming what will be removed.

Decision 5 — GUI: scope-delete only, rust's first in-window consumer

The Workshop sidebar lists scopes, so Delete Scope has a natural home — a destructive action in the panel's scope group. This is the first real in-window destructive affordance in the app, and it consumes Color.drift (pālana rust), closing the gap ho-06.4 flagged and ho-06.5 confirmed had no consumer. The ho-06.5 gate proved the precise boundary: an in-window control can wear rust; the confirmation dialog cannot (dialogs render out of the view hierarchy, take no tint, and paint the system's own destructive red). So the panel button reads rust; the confirm dialog stays fully system-rendered. No workaround attempted — that was settled empirically at the 06.5 gate.

Deleting a shared entry from the GUI has no surface to attach to (there is no shared-entry list in the Workshop until ho-07). It defers there, named in Out of scope. Core and CLI carry full shared-delete this ho.

Decision 7 — Per-key deletion folded in (scope widened mid-build)

The ratified scope was scope + shared deletion. Dogfooding the built app surfaced the same gap one level down — no way to delete a single secret within a scope, on any surface. Rather than close 06.7 and open a near-identical follow-up, the practitioner folded per-key deletion into this ho (a deliberate scope widening, recorded here, not a quiet edit). It reuses every decision above: VaultCore.deleteSecret(_:inScope:) (keyless — removes the key's .age or .link; deleting a link drops only the pointer, the shared entry is untouched), sharibako delete <scope> <key> (optional second positional, same confirmation), and a rust-toned Delete Secret button in the detail pane raising the same system-rendered confirm dialog. The three Core deletion verbs moved to VaultCore+Deletion.swift (the additions crossed SwiftLint's body-length ceiling; split by extension per the VaultCore+Encryption precedent).

Decision 6 — CLI surface: one delete verb, --shared flag

A single sharibako delete <id> command, with --shared selecting the shared pool (mirroring list [--shared]), --yes to skip confirmation, and --force to orphan linked shared entries. One verb, not a delete scope / delete shared subcommand pair — the flag idiom is already the project's convention for the scope/shared split.

Discovery (deferred to execution)


Phase 2 — Execute

Branch delete-verb off main. Three agent tasks, one per surface, because the seams are real: the Core verbs are pure library work verified by filesystem tests with no age; the CLI verb is ArgumentParser + confirmation verified by command tests; the GUI affordance is view + WorkshopModel intent verified by the model's unit tests and the signed-install gate. Each is its own conversation with its own acceptance surface.

AT-02 and AT-03 both depend only on AT-01, not on each other; either order after Core.

Testing and iteration approach

Per change, the standing rhythm: swift package clean && swift test directly (never clean→build→test — the SwiftPM incremental-link bug after SharibakoCore changes) → swift build -Xswiftc -warnings-as-errorsswift-format lint --strict --recursive Sources Testsswiftlint lint --strict → coverage ≥90%. Branching logic (the linker guard, panel-button enablement) lives tested in SharibakoCore / WorkshopModel; declarative View bodies are CI-excluded by name with justification. Tests never touch live user state — injected home / roots / temp vaults only. Core deletion tests need no age on PATH (they assert on filesystem presence, not ciphertext).

Done means

Verification and the gate

  1. The rhythm above, green, on the delete-verb branch.

  2. CLI dogfood against the real vault (no signing needed for the CLI): create a throwaway scope and a throwaway shared entry, delete each; confirm the linked-shared refusal names the linker; unlink it and retry (succeeds); re-add, --force it and confirm the linker goes dangling and is surfaced by heal/status; confirm sync is what actually commits the removal.

  3. Signed-install gate for the Workshop — the committed Xcode project is the only real build path (the 06.4/06.5 lesson: not swift run, not scripts/install.sh):

    xcodebuild -project xcode/Sharibako.xcodeproj -scheme Sharibako \
      -configuration Release -derivedDataPath .build/xcode build
    

    Replace /Applications/Sharibako.app and launch from /Applications explicitly (Spotlight resurfaces stale DerivedData builds). Against the real vault and Keychain:

    • Delete Scope fires from the panel, reads rust, is disabled with no selection, and opens a confirmation dialog naming what is removed.
    • On confirm, the scope leaves the sidebar; the materialized .env for a deleted scope is still on disk (blast radius held); the removal is uncommitted until Sync.
    • The visual gate is the operator's eyeball — rust reads right on the in-window button, the system dialog is acceptable, the deletion feels safe and explicit. Not called passed until Andrew confirms on the themed build.

Phase 3 —

Filled at close (2026-07-12, after the signed-install gate)

The gate passed on the operator's eyeball. The signed /Applications build was driven against the real vault: the rust reads right on both destructive buttons, the system-rendered confirmation dialogs are acceptable, and deleting a scope / a key feels deliberate and safe. No Touch ID fires (deletion decrypts nothing), the scope/secret leaves its pane, the status line names the outcome, and the removal stays uncommitted until Sync. Rust finally has real in-window consumers — the gap 06.4 flagged and 06.5 could find no home for is closed.

Scope widened mid-build: per-key deletion folded in (Decision 7). Dogfooding the built app surfaced the same gap one level down — no way to delete a single secret. Rather than close and reopen a near-identical follow-up, the practitioner folded deleteSecret + sharibako delete <scope> <key> + a detail-pane Delete Secret button into this ho. All four destructive verbs now exist (scope, key, shared) across Core and CLI; the GUI carries scope and key. The three Core deletion verbs live in VaultCore+Deletion.swift (split from VaultCore for the SwiftLint body-length ceiling, per the +Encryption precedent).

Decision review. Decisions 1–6 held as ratified. Decision 5's rust/dialog split held empirically — the in-window buttons take Color.drift, the dialogs stay system-rendered (the 06.5 finding, unchanged). Decision 2's linker guard worked end-to-end at the CLI dogfood (refuse → name linkers → unlink → retry; --force → orphan surfaced by heal). Decision 7 is the one addition beyond the ratified scope.

Still deferred, named: the GUI delete-shared affordance — no shared-entry surface exists in the Workshop until ho-07's shared-secret browser. Core + CLI delete shared entries today; only the button waits.

What the tests didn't catch — view-layer, by design (the gate's job): the rust color reading right on the two buttons and the system dialogs' acceptability. Everything underneath is covered — 759 tests (+28: 11 Core, 9 CLI, 8 GUI-model), warnings-as-errors and both linters clean; ActionPanel / WorkshopWindow / SecretDetail stay CI-excluded views.

Branch state at close: delete-verb is stacked on panel-chrome because 06.5's PR #15 is not yet merged. Push + PR are the practitioner's ritual and were not done here; when #15 merges, delete-verb rebases onto main.

Closing this ho

Closing = fill this Reflect + flip status: complete + write the state-summary block to the project's K6 (/kamae-6-sharibako-state-memory.md, refreshing the block at the top, verbatim labels and order) + append a build-record entry to K4 (ho-process/kamae-4-sharibako-ho-overview.md, , append-only). The block:

STATE-SUMMARY

Also flag for the K4 overview pass (out of this skill's scope to edit — it is ho-kamae-4-overview-collaborator's territory): the overview still shows 06.5 as owed/next and does not list 06.7. Mark 06.5 closed and 06.7 in the Phase-4 tail. The GUI delete-shared boundary (Decision 5) is a note for ho-07's scope: its shared-secret browser is where that affordance lands.


Authored 2026-07-12 (Think ratified before authoring; five decisions carried in from the practitioner conversation, one logged from convention). The response to ho-06.5's gate finding — the missing delete verb across Core, CLI, and the Workshop, and destructive-rust's first real in-window home.

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ 75a79d08a2b3 · ho-system @ 0f93b7fa32f7 · the glossary · the colophon