ho-06.7 — Delete: the missing destructive verb
- 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:
SharibakoCore—deleteScopeanddeleteSharedEntry, the tenth and eleventh vault operations, filesystem-only (they decrypt nothing).- CLI — a
sharibako deleteverb over both, followingclean's confirmation discipline. - Workshop — a Delete Scope affordance in the action panel, wired to the pālana rust token — the first real in-window consumer of the destructive voice ho-06.4 reserved and ho-06.5 could find no home for.
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:
- The GUI delete-shared affordance. No surface in the Workshop lists shared entries yet — the shared-secret browser is ho-07. Core and CLI delete shared entries this ho; the Workshop affordance for it rides ho-07 when there is a shared list to attach it to. Named as a deferred boundary, not skipped.
- Cascade-unlink convenience.
--forceorphans linked entries; it does not auto-convert linkers back to local values. The clean path isunlinkfirst (it already preserves the value locally);--forceis the escape hatch. - History scrubbing. A deleted secret remains in git history and is recoverable — consistent with the threat model, not a leak (SECURITY.md already treats git-as-backup as accepting historical ciphertext).
- Auto-commit. Deletion writes the working tree;
synccommits it, on the user's existing commit boundary. No new git behavior. - Marker or materialized-
.envremoval. Deletion only touches the vault (Decision 3).
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)
- Confirmation copy. The exact prompt/dialog wording that names the blast radius plainly (and, for shared, the linker count). Execution-level.
- Exit-code mapping for
sharedEntryLinked. Whether it reuses the user-error code or earns its own; anErrorReporter/ExitCodefind. - Where the Delete Scope button sits in the panel scope group, and how its
disabled state reads (no scope selected /
model.activity). Execution-level, following the existing panel-verb pattern.
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.
- Ho-06.7-AT-01 — Core:
deleteScope+deleteSharedEntry. No dependency. ThesharedEntryLinkederror case, the linker guard, filesystem tests (noageneeded). - Ho-06.7-AT-02 — CLI:
sharibako delete. Depends on -01.LinkCommand's keyless construction +CleanCommand's confirmation discipline;--shared,--yes,--force. - Ho-06.7-AT-03 — GUI: Delete Scope in the
panel. Depends on AT-01.
WorkshopModeldelete intent (tested), the panel button on rust, system confirm dialog.
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-errors → swift-format lint --strict --recursive Sources Tests → swiftlint 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
SharibakoCoredeletes a scope (whole directory) and a shared entry, both through the keyless initializer; the linker guard refuses a linked shared delete naming the linkers, and--force/force:orphans them;shared/is untouched by a scope delete; absent targets throw the right existing errors.sharibako delete <scope>andsharibako delete --shared <id>work against a real vault, confirmation-gated (unless--yes), no Touch ID; the linked-shared refusal prints the linkers and the unlink-or---forcehint; success thatsynccommits the removal.- The Workshop's action panel offers Delete Scope, rust-toned, disabled with no scope selected; it opens a system confirmation dialog naming the blast radius; on confirm the scope leaves the sidebar and the vault; nothing else moves.
- Blast radius holds: markers and materialized
.envfiles are left in place; the removal is not committed untilsync. - The full rhythm is green across all three tasks; coverage ≥90% with any new
View exclusions named and justified in
ci.yml. - Dogfood gate passed (below).
Verification and the gate
-
The rhythm above, green, on the
delete-verbbranch. -
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;
unlinkit and retry (succeeds); re-add,--forceit and confirm the linker goes dangling and is surfaced byheal/status; confirmsyncis what actually commits the removal. -
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, notscripts/install.sh):xcodebuild -project xcode/Sharibako.xcodeproj -scheme Sharibako \ -configuration Release -derivedDataPath .build/xcode buildReplace
/Applications/Sharibako.appand launch from/Applicationsexplicitly (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
.envfor 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
- COMPLETED — <what this ho finished>
- NEXT — <the single pointer to what comes next>
- ACTION ITEMS / BLOCKS — <open items; blocks loudly, or
none> - PROJECT LIFECYCLE — <kamae | dev | beta | production>
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 →