diff --git a/FABRIC-3.md b/FABRIC-3.md
index d39e21e..7f80d14 100644
--- a/FABRIC-3.md
+++ b/FABRIC-3.md
@@ -160,9 +160,11 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
as a `compudynamics.c`-driven decision generically, not a `physics_hotwords_cache.c`
(`DictEntry*`-hardcoded) reuse question specifically.
-- [ ] Decide and implement unclean-removal handling (Section U's explicitly flagged open
- question — never answered) — at minimum, detect a mid-flush disconnect via Milestone 2e's
- disconnect signal and decide what state that leaves affected blocks in.
+- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.10), scoped alongside `DETACH`.** Unclean removal
+ runs the exact same VM-kill path `EJECT` (M5) uses, minus the flush step — skipped outright
+ since the device is confirmed gone by the time the existing `bot_msc_detach_pending` signal
+ fires, not attempted-and-ignored. Design done; the actual wiring into `sk_repl_idle()`'s
+ existing detach handling is not yet written.
### From FABRIC-2.md §X, Milestone 4 — Drive/credential security
@@ -687,7 +689,11 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
already-working, currently-unguarded retarget word — check re-verifies the attached drive
live on every call; `installed=0` VMs stay freely targetable.
-- [ ] Implement detach behavior on console disconnect or VM teardown.
+- [ ] Implement detach behavior on console disconnect or VM teardown. **SCOPED 2026-08-27
+ (`FABRIC-3.md` §F.10)**: a new `EJECT` word — flush via the existing `blk_vm_flush_all()`,
+ reset the console's active VM if bound to the departing VM, then `capsule_vm_kill()` (already
+ complete, no new teardown mechanism needed). Needs one small new piece of state `WIREBIND`
+ must own: which `VMUuid` is currently attached via the home-blocks USB path.
### From FABRIC-2.md §X, Milestone 6 — Kernel/capsule PKI signing chain
@@ -1425,12 +1431,12 @@ graph TD
BMAPWRITE["❌ Write block-map to drive (M3)"]
BMAPREAD["❌ Read/validate block-map on insert (M3)"]
MIGSM["🟡 Migration state machine (M3)
partially answered: ACL decides *when*"]
- UNCLEAN["❌ Unclean-removal handling (M3)"]
+ UNCLEAN["✅ Unclean-removal handling (M3)
CLOSED 2026-08-27 (§F.10) — same kill path as EJECT, minus flush"]
ACLKEY["❓ ACL 'bumps and holes' comparison
semantics — SAME GAP in M5 and D.4"]
WIREBIND["❌ Wire hotplug → capsule_birth_baby() (M5)"]
BINDSTEP["❌ Extend sk_repl_set_active_vm()
with key-match (M5) — SCOPED 2026-08-27 (§F.9)"]
- DETACH["❌ Detach behavior (M5)"]
+ DETACH["❌ Detach behavior / new EJECT word (M5)
SCOPED 2026-08-27 (§F.10)"]
EXPIRE["✅ EXPIRE reframed: session end
= VM detach via COOL (§B + D.2)"]
RUNCAP["❌ Runtime capsule construction
from thumbdrive content — SCOPED 2026-08-27 (§F.6)"]
@@ -1466,6 +1472,7 @@ graph TD
ACLKEY --> BINDSTEP
WIREBIND --> BINDSTEP
BINDSTEP --> DETACH
+ UNCLEAN -.->|same kill path, no flush| DETACH
DETACH -->|triggers| EXPIRE
MSGSHAPE --> MSGMIGRATE
@@ -1479,8 +1486,8 @@ graph TD
classDef unblocked fill:#a83,stroke:#333,color:#fff
classDef open fill:#666,stroke:#333,color:#fff
classDef partial fill:#883,stroke:#333,color:#fff
- class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG done
- class W10,STALL,FIRSTTOUCH,BMAPWRITE,BMAPREAD,UNCLEAN,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP blocked
+ class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,UNCLEAN done
+ class W10,STALL,FIRSTTOUCH,BMAPWRITE,BMAPREAD,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP blocked
class ACLKEY,MSGMIGRATE,SSDSCOPE,ROUNDTRIP,POLYBLOCK open
class MIGSM partial
```
@@ -1526,6 +1533,14 @@ finished). Dashed arrows = softer "gates/informs" relationships.
(`mama_forth_words.c`) is a real, working, currently-unguarded retarget word. The only
genuinely new code this node needs is a small cross-file accessor for the currently-attached
device (mirroring `sk_repl_get_active_vm()`'s own precedent) plus the comparison itself.
+- **`UNCLEAN` (M3) and `DETACH` (M5) were the same node wearing two names, in a different way
+ than `ACLKEY` was** (§F.10) — not identical questions asked twice, but one shared teardown
+ path (`capsule_vm_kill()`, already complete) forked by whether a flush is physically possible.
+ Scoping them together closed a long-open M3 item as a side effect of an M5 pass.
+- **Found a real, pre-existing, independent bug while tracing `DETACH`** (§F.10): plain `KILL`
+ today never resets `g_repl_active_vm`, so killing the VM a console is currently `USE`'d onto
+ leaves a dangling pointer. Reported, not fixed — the new `EJECT`/hot-unplug call sites handle
+ this correctly, but the existing `KILL` word's own call sites do not.
**Not yet done:** an ordered plan (which node to attack first, given the graph). Per Captain
Bob's own framing, that's the next pass — "start asking and answering questions iteratively
@@ -1924,6 +1939,70 @@ refusal message text (matching `USE`'s existing console-message style); whether
this — `capsule_parity_log_birth_failed()` and friends, `parity.c` — but nothing says a
security-relevant refusal here needs the same treatment; not decided).
+### F.10 — `DETACH` (the last M5 node) — and `UNCLEAN` closed alongside it
+
+Traced `capsule_vm_kill()` (`capsule_birth.c:295-364`) fully before scoping, per D.2's own
+framing that detach should reuse it: it's real and complete — evicts the Stadium patron cell
+via `stadium_evict()`, `vm_cleanup()`+`sf_free()`s the VM, marks it `DEAD`, logs via
+`capsule_parity_log_kill()`, is idempotent, and refuses to kill Hera. `DETACH` doesn't need a
+new teardown mechanism, only a caller for this one plus the flush/bookkeeping around it.
+
+**Real finding: D.3's "flush before eject" only makes physical sense as a deliberate
+pre-removal step — once a drive is physically pulled, there's nothing left to write to.**
+That splits this into two genuinely different paths, and per direct instruction this pass
+scopes **both**, closing the previously-separate, long-open `UNCLEAN` node (M3) at the same
+time rather than leaving it for later:
+
+- **Graceful path (new `EJECT` word, no name conflict found):** the drive is still physically
+ present. Flush, then kill, then it's safe to remove.
+- **Abrupt path (the existing `bot_msc_detach_pending` hot-unplug signal, already wired and
+ consumed in `sk_repl_idle()` for `blk_subsys_detach_device()`):** the drive is already gone.
+ No flush is attempted — data since the last flush is lost, which *is* correct unclean-removal
+ semantics, matching what every real OS does on a surprise USB yank. This is `UNCLEAN`'s
+ entire remaining scope, now closed as "same kill path as `EJECT`, minus the flush attempt."
+
+**One genuinely new, small piece of state required for both paths** (not a new mechanism, a
+missing link): a way to know *which VM* corresponds to the currently home-blocks-attached
+drive. `BINDSTEP`'s live re-verify-and-compare approach (§F.9) only works while the drive is
+still physically present and readable — useless for the abrupt path, where the device is
+already gone by the time the signal fires. Decided: `WIREBIND` (§F.5, itself not yet built)
+must set a small tracked `VMUuid` — "the VM currently attached via the home-blocks USB path" —
+at successful birth; both `EJECT` and the hot-unplug handler read and clear it. No new registry
+machinery needed beyond this: `capsule_vm_registry_get(vm_id, &entry)` (already exists) resolves
+the `VMUuid` to a live entry, and `capsule_vm_kill(entry.name)` (already exists, takes a name
+not an ID — chain the two rather than adding a kill-by-id variant) does the rest.
+
+**Decisions made 2026-08-27:**
+
+1. **`EJECT` (graceful):** resolve the tracked attached-VM id → entry; `blk_vm_flush_all(vm)`
+ (already exists, same function `sk_repl_idle()`'s own periodic sync already calls) while the
+ VM is still alive; if the console's active VM (`sk_repl_get_active_vm()`) is this same VM,
+ reset it to Hera (`sk_repl_set_active_vm(NULL)`) *before* teardown — necessary, not
+ optional, see the dangling-pointer finding below; then `capsule_vm_kill(entry.name)`; clear
+ the tracked attached-VM state. `capsule_vm_kill()`'s own idempotency covers the case where
+ the VM was already gone by some other path — no extra guard needed.
+2. **Hot-unplug (`UNCLEAN`, closed):** identical steps minus the flush — skipped outright, not
+ attempted-and-ignored-on-failure, since the device is confirmed gone by the time this signal
+ fires. Same active-VM reset-before-kill step applies.
+3. **`W10`'s edge to `DETACH` in the §E graph is specifically about the `EJECT` path's flush
+ step** — the hot-unplug/`UNCLEAN` path never touches `WRITE(10)` at all (nothing is written
+ during an unclean removal), so `UNCLEAN` itself is *not* blocked on `WRITE(10)`, only
+ `EJECT` is. Worth keeping distinct now that both share one node.
+
+**Separate, already-existing bug found while tracing this, reported per standing instruction
+(not fixed):** `capsule_vm_kill()` never touches `g_repl_active_vm`. Today, plain `KILL` on a
+VM the console is currently `USE`'d onto leaves `g_repl_active_vm` dangling — the REPL would
+fault on the next command dispatched through it. This is real and reachable *today*,
+independent of `DETACH`/`EJECT`; decision 1 above only fixes it for the new `EJECT`/hot-unplug
+call sites, not the pre-existing plain `KILL` word.
+
+**Not yet scoped (deferred within this node):** the exact new tracked-state variable's
+name/location (logically `WIREBIND`'s to own, but `WIREBIND` itself isn't built yet); whether
+`EJECT` needs a name argument or always targets "whatever's currently attached" (leaning toward
+the latter, given the single-USB-device constraint confirmed in `F.8` means there's never more
+than one candidate — not committed here); the console message text distinguishing a graceful
+eject from an unclean one in the transcript.
+
### D.5 — Scope expansion (2026-08-27): identity is common to every VM, not just users
Surfaced while scoping `ACLKEY`, stated directly: *"the whole object is to deliver a