On this page
The screen deletion shows before it removes anything: every live process holding the profile, with the daemon pre-ticked, stopped in place as the user confirms.
#claudewheel.deletion_checklist
#claudewheel.deletion_checklist
Present everything holding a profile, and stop exactly what the user ticks.
Deleting a profile directory does not free it. Every surviving Claude Code process still carries CLAUDE_CONFIG_DIR pointing at the path that was just removed, and any invocation of the client -- including a read-only status query -- recreates its configuration directory before doing anything else, so the deleted profile reappears as a husk. That is why deletion asks first: rather than promising a completeness it cannot deliver, it shows every live process registered under the profile and lets the user decide, per row, which ones to stop.
What is ticked before the user touches anything -----------------------------------------------
The daemon and its workers, and nothing else. Those are claudewheel's own consequence -- the profile's daemon exists because the profile was launched -- while a background job is work someone started deliberately, so it is listed, never pre-selected. Nothing is stopped without an explicit tick.
The screen does not close on confirmation: each ticked row's state line goes from a green running to a red stopped as its process really goes, and escape leaves afterwards.
Stop-then-remove, never the reverse -----------------------------------
The caller stops holders before removing the directory. Reversing that order does not merely risk a husk, it guarantees one: the daemon-stop command is itself an invocation of the client, so run against an already-deleted profile it recreates the directory it was asked to shut down. This ordering will look wrong to a future reader -- "stop the thing, then delete its home" reads like an optimisation -- so it is stated here and at the call sites.
#Holder
One live process holding the profile, and what the screen knows about it.
#row
def row(self) -> ListRowThis holder as a row of the shared list component.
#ChecklistOutcome
What the screen decided and what it managed to stop.
still_holding is every holder whose process is really still up when the screen closes -- typically the ones left unticked plus any whose stop did not take, but it is re-probed rather than subtracted from the snapshot, so a holder that exited on its own while the screen was open is not in it. It is what lets the deletion say the directory may come back instead of claiming it is gone.
#gather_holders
def gather_holders(config_dir: Path) -> list[Holder]Every live process registered under config_dir, memory included.
Resident memory is measured for all of them in one call, so opening the screen spawns one ps rather than one per row.
#stop_order
def stop_order(holders: Sequence[Holder]) -> list[Holder]The ticked holders, supervisor first.
A worker stopped before its supervisor can simply be put back; stopping the supervisor first makes the rest stay stopped.
#still_the_registered_process
def still_the_registered_process(record: SessionRecord) -> boolTrue when record's pid still names the process that registered it.
The checklist gathers its holders once and then waits on a human, so every pid it holds is a snapshot of unbounded age by the time anything acts on it. The kernel recycles pid numbers freely, so "the pid exists" answers the wrong question -- this asks the phantom filter's question, comparing the kernel start token the record recorded against the token the pid carries now.
#Stopper
Stops one holder by whichever mechanism its kind calls for.
The daemon-stop command is issued at most once however many daemon rows are ticked: it shuts the supervisor down, and a second invocation would only re-create the config directory it was pointed at.
#stop
def stop(self, holder: Holder) -> boolStop holder and wait for it to go. True when it really went.
Identity is re-checked immediately before anything is signalled, and again on every poll of the wait: a pid whose start token no longer matches belongs to a different process now, so the one the row names is already gone. That counts as stopped -- the profile is not held by it -- and nothing at all is signalled, because the signal would land on a stranger.
A preview issues the stop, so the would-do log gets it, but does not wait on it: the chokepoint recorded the signal instead of sending it, so the process is still there and the poll could only burn its whole timeout per ticked row before reporting a failure that never happened.
#run_checklist
def run_checklist(holders: list[Holder], *, profile_name: str, config_dir: Path, binary: Path, env: Mapping[str, str], theme: ThemeColors, terminal: Terminal, now_ms: int, identity: SessionIdentity | None=None) -> ChecklistOutcomeRun the checklist over holders and return what it decided.
Two phases in one screen. While selecting, up/down move, space toggles and enter confirms; escape cancels and stops nothing at all. After confirming, the screen stays where it is and each ticked row is stopped in turn, its state line redrawn as it goes, and the loop then waits for a key before handing the terminal back.