← Back to projects
DC disk-clean-checklist / docket

A cleanup tool that would rather do nothing than guess wrong, simplifying the manual process cleaning the OS yourself

disk-clean-checklist scans a machine for reclaimable space — old caches, leftover build folders, Docker bloat — and shows a checklist. Nothing gets deleted until specific items are picked and confirmed. The actual engineering problem here isn't finding big folders; it's proving, for each one, that deleting it is genuinely safe before it's ever offered as an option.

19/19Tests passing
4Evidence tiers for "is this safe"
0Items labeled Safe on a guess

Why the WSL scanner almost deleted the wrong kind of "cache"

One scanner walks a Linux environment looking for node_modules folders — the disposable, regenerable dependency folders every JavaScript project creates — so it can flag them as safe to delete. Early on, that walk covered the entire home directory, which meant it also wandered into the folders VS Code itself uses to run its own remote server components. Those folders happen to contain their own node_modules too, but deleting them isn't the same as deleting a project's dependencies — it can break the tool that's currently running.

The fix wasn't a simple "don't touch that folder" exception, because the next tool update could add a new folder the exception doesn't know about. Instead, every hit now has to prove itself: a folder is only ever called safe if a real project marker (like package.json) sits right next to it. No marker, no safe label — regardless of what the folder is named or where it lives.

Show the technical version

Known remote-dev-server directories (.vscode-server, .cursor-server, etc.) are pulled out as Info/no-action rows instead of walked into at all. Every other node_modules/target hit is classified Safe only if a project marker (package.json, Cargo.toml, pom.xml, build.sbt) is confirmed adjacent to it — otherwise it's downgraded to Review with an explicit reason. The marker check, not the exclusion list, is what actually protects an unlisted tool on a different machine.

Why an AI tool's own folder can't be offered as one clickable row

Both Claude Code and Codex keep their working files in a single folder in the home directory. It's tempting to offer that whole folder as one "clean this up" checklist item — except that same folder also holds login credentials, settings, and persistent memory files sitting right next to genuinely disposable cache data. One click on a folder like that would recycle-bin the useful parts along with the junk.

The fix is an allowlist, not a denylist: only specific, individually verified-safe subfolders are ever offered, and the root folder itself is never offered as a whole. A denylist ("don't touch these specific files") would quietly fail the moment a future update added a new file nobody thought to exclude yet — an allowlist fails safe instead, by simply not offering anything it hasn't already checked.

Show the technical version

Safe subpaths: .claude/shell-snapshots, paste-cache, debug, file-history; .codex/cache, log, tmp, .tmp. Explicitly excluded after direct verification: .claude/sessions and ide (live PID-keyed state for running instances, not history), and .codex/.sandbox* (live executables and ACL state). Session transcript files get their own per-file safety check — for Claude, the exact session ID is cross-checked against live PID files before anything currently open is ever offered; Codex lacks that same live-mapping, so anything modified in the last ~30 minutes is treated as possibly still active instead, a known, deliberate asymmetry rather than an oversight.

Grading each "safe to delete" claim by how it's actually known to be true

Every single label in this tool — Safe, Review, Info — rests on some claim about what a given file or folder actually is. Once AI assistance was involved in helping write those classification rules, that raised a real question: how would a subtly wrong or unverified assumption baked into one of those rules ever get caught?

The answer was a ranking system for the claims themselves, not just the files. Every rule is sorted into one of four tiers, from "the tool told us directly" down to "this is just a pattern that's probably right." The one rule that follows from that: nothing is allowed to be labeled Safe if the only evidence behind it is the weakest tier — a guess stays visibly a guess, routed to Review with the reasoning shown, instead of being quietly upgraded to Safe because it seemed reasonable.

Show the technical version

Tier 1 — tool/OS self-report (docker system df, Get-AppxPackage, the registry). Tier 2 — documented spec or convention (XDG cache spec, npm/pnpm docs). Tier 2b — verified by direct inspection of the tool, no formal spec exists. Tier 3 — heuristic inference (age, size, name pattern) — never allowed to sit in Safe. Tier 4 — incident-informed, meaning the rule exists because something already broke once before. As of the last audit, every purely Tier-3 item (Downloads/Personal top-N folders, stale-package heuristics, AI session files) is correctly Review and Recycle-Bin-backed rather than Safe — the one item flagged as worth a deliberate one-time manual check despite testing as Tier 2 is the pnpm shared store, since it has a larger blast radius than the other Tier 2 items.

A release that looked fine and produced literally nothing when run

The first published download didn't crash, didn't show an error, and didn't show up in any Windows security log — it just silently did nothing when double-clicked. Before touching the fix, every plausible cause got ruled out one at a time: not SmartScreen, not antivirus, not a missing .NET runtime, not a corrupted download. The actual cause was simpler and less dramatic than any of those — the release only contained the app's exe, not the handful of files it actually needs sitting right next to it to run.

Fixing it took two attempts. The first "fixed" build still failed the exact same silent way when tested in isolation, because a few native files were still missing from the bundle — a reminder that "it launched on my machine" isn't proof a packaged build is actually self-contained until it's tested somewhere nothing else is already sitting around to cover the gap.

Show the technical version

The release build had no <SelfContained>/<RuntimeIdentifier> set, so dotnet publish produced a framework-dependent apphost that needs its .dll/.deps.json/.runtimeconfig.json companions alongside it — only the bare exe was ever uploaded. The first republish attempt (--self-contained true -p:PublishSingleFile=true) still left five native WPF DLLs as loose files next to the exe, since PublishSingleFile doesn't bundle non-IL native dependencies. The working command adds -p:IncludeNativeLibrariesForSelfExtract=true, which self-extracts those DLLs to a temp folder at first launch. Verified by copying the exe alone into an isolated folder and confirming a real window opened — not just "no crash." Fix is built and verified locally; not yet uploaded to replace the broken release asset.

SPEC.md · v2, C#

disk-clean-checklist — project spec

Windows CLI + WPF clean loop: feature-complete for daily use · no open safety issues

Current status

Scan → checklist → select → execute → warnings on both scan and delete failures → before/after free space. Goal expanded 2026-07-02 to target Linux/Mac too — see “Cross-platform MVP scope” before starting Unit 4 or the Avalonia port. No open safety issues — the WSL node_modules scope bug is fixed, see “Resolved.”

Native node_modules/build-artifact scanning (NativeBuildDirs / FindNativeBuildDirs, Scanners.cs:156 / :1019) shipped after being spec’d 2026-07-09 — wired into Program.cs and MainWindow.xaml.cs, tested in ScannersTests.cs. The “Planned unit” section was never marked done after shipping; corrected 2026-07-15.

WindowsTempFolder() (system-wide C:\Windows\Temp, distinct from user %TEMP%) — Done verified live 2026-07-15 via console scan + Clean Selected. Result: 121/123 entries cleared without elevation, 2 correctly skipped as genuinely locked (an active session log, Office Click-to-Run’s live streaming log) — matches ActionExecutor.DeleteContents’s partial-delete format exactly (Cleared N entries, skipped M. Blocked by: …), no crash, no silent failure. Correction to the original caution: elevation was not required in practice — most of C:\Windows\Temp was writable under a normal user account on this machine.

Next up: Avalonia UI port. A real blocker was found 2026-07-15 before any port work started — see “Avalonia port — blocker found” below.

2026-08-07: the published v0.0.0 release does not run on a clean download — see “v0.0.0 release doesn’t run” in the Build Log tab. Self-contained single-file rebuild is built and verified locally; not yet uploaded to replace the broken release asset.

Location

C:\Users\ASUS ROG\Downloads\disk-clean-checklist\ — .NET solution, C#

  • DiskCleanup.Core\ — scanners, action executor, selection logic (shared)
  • DiskCleanup\ — console app (Units 1–2)
  • DiskCleanup.Wpf\ — GUI widget (Unit 3): DataGrid checklist, risk filter, Clean Selected
  • DiskCleanup.Tests\ — xUnit tests against Core

What it does

  1. Scans these categories and computes sizes:
    • Recycle Bin
    • User Temp (%TEMP%), system-wide C:\Windows\Temp, and SoftwareDistribution\Download
    • VS Code CachedExtensionVSIXs
    • WSL: ~/.cache, ~/.npm, ~/.local/share/pnpm/store, and any node_modules/target dirs found under $HOME (depth 6) — classified Safe (project marker present), Review (marker missing), or Info (inside a known remote-dev-server dir like .vscode-server, excluded rather than walked into)
    • Docker reclaimable space (docker system df)
    • Top N largest folders in Downloads
    • AppData\Local\Packages folders untouched for 6+ months (bloatware candidates)
    • Top installed apps by size (registry) — informational only
    • AI-related folders (.claude/.codex), flagging unnecessary chat logs, temp files, bloated memory files
  2. Prints a numbered checklist, e.g. [1] Recycle Bin — 8.2GB — SAFE, [2] Downloads\Foo — 5GB — REVIEW
  3. Prompts the user to type numbers (e.g. 1,2,5 or all-safe) to act on
  4. Executes the chosen actions (delete/clear/prune) and reprints free space before/after

New: scheduled check (background automation)

  • A separate run mode (--check) does the scan silently, no prompts
  • If free disk space < 45GB, sends a Windows toast notification with a short summary (top 3–5 reclaimable items + total reclaimable size)
  • Triggered by Windows Task Scheduler at user-configured times (app does not self-schedule — setup command is printed for the user to run once)
  • Clicking/opening from the notification just launches the normal interactive checklist run — no auto-cleanup ever happens from --check

What it does NOT do

  • --check mode never deletes or modifies anything — notify only
  • Never touches Downloads project folders or installed apps without an explicit number pick
  • Anything requiring admin (Docker vhdx compaction, MSI uninstalls) is printed as a command to copy into an elevated terminal, not executed directly

Done =

DiskCleanup.exe → see the checklist → type numbers → selected items get cleaned → see new free space before/after.

DiskCleanup.exe --check (run by Task Scheduler) → if space < 45GB, toast notification with checklist summary appears.

Build plan — incremental, one unit at a time, stop for evaluation between

  1. Scanner + checklist printer (no actions yet) Done
  2. Selection input + action executor Done
  3. WPF widget: checklist with checkboxes, risk filter, Clean Selected Done
  4. --check mode + toast + Task Scheduler setup Deferred — see “Cross-platform MVP scope” (toast mechanism is platform-specific; wait for the platform-abstraction seam)
  5. StalePackages AppX cross-check (Get-AppxPackage) — distinguishes “app uninstalled, orphaned data” from “app still installed, folder just looks stale” Done
  6. DockerVhdxBloat scanner (flags Docker Desktop’s WSL2 .vhdx once it never-shrinks past 20GB, suggests a diskpart compaction command) Done
  7. AiFolders root-folder safety fix (allowlist of verified-safe subpaths only, never .claude/.codex root) + session-file staleness/active-session detection Done
  8. Scan-side + delete-side warning collection (Review.md finding #3: catch{} no longer silently swallows scan/delete errors; both CLI and WPF surface which specific file/folder blocked an action) Done — verified live 2026-07-02 against real locked Temp files and an access-denied SoftwareDistribution run

Unit 2 amendment

  • all-safe no longer auto-executes. It pre-selects all SAFE-risk items, prints them as a confirmation list, and requires a final y/n before any deletion happens — same as a manual numeric selection
  • No item, regardless of risk level, is deleted without an explicit confirm step (reason: a SAFE-categorized item like a WSL project’s node_modules may still be one the user wants to keep)

Phase 2 (remaining, future)

  • Filter by category/size (risk-level filter done in Unit 3)
  • Background/scheduled run of the widget (ties into Unit 4’s --check mode, deferred above)

Known issues (open, safety-relevant)

None currently open. See “Resolved” for the WSL node_modules scope fix.

Resolved

Subagent folder cascade + orphan detectionFixed 2026-07-15

ScanClaudeFolder (Scanners.cs:513-586) now matches each session’s .jsonl against its sibling <sessionId>/ folder (subagents/, tool-results/): when both exist, deleting the conversation deletes the folder in the same action (CheckItem.SecondaryPath, wired through ActionExecutor.MoveToRecycleBin); when only the folder is left (parent conversation already deleted by a prior cleanup), it gets its own SAFE row. memory/ stays excluded either way. Not WSL-specific — one fix in the shared scanner covers both native Windows and WSL’s .claude, since Wsl() calls the same function.

Verified live 2026-07-15 against the real .claude folder: found and cleanly deleted 19 pre-existing orphaned session folders across multiple projects, zero failures, all recoverable via Recycle Bin. .codex was checked too — the folder no longer exists on this machine (user-deleted), so ScanCodexFolder was left untouched.

Test coverage: ScannersTests.csScanClaudeFolder_CascadesSessionFolderIntoMatchingJsonl, ScanClaudeFolder_OrphanedSessionFolder_FlaggedAsSafeRow, ScanClaudeFolder_ActiveSessionOrphanFolder_NotFlagged.

FindBuildDirs scope bugFixed 2026-07-08

Wsl()’s node_modules/target discovery (Scanners.cs FindBuildDirs) walks the entire WSL home directory (depth 6), not just ~/projects. Confirmed live 2026-07-02: ~/.vscode-server contains 7 real node_modules dirs (VS Code Server’s own bundled extensions), which used to surface as ordinary SAFE/DeleteFolder rows — indistinguishable from a project’s own dependencies, the same failure class as a prior incident where WSL cleanup corrupted ~/.vscode-server.

Fix: kept the whole-home walk, but FindBuildDirs now classifies every hit three ways — known remote-dev-server dirs (.vscode-server, .vscode-server-insiders, .cursor-server, .windsurf-server) pulled out as Info/no-action rows instead of walked into; .cache/.npm skipped (already their own CheckItems); every other node_modules/target hit is Safe only if a project marker sits next to it (package.json, or Cargo.toml/pom.xml/build.sbt), otherwise Review with a reason explaining the marker couldn’t be confirmed.

Verified: VS Code’s own troubleshooting docs confirm deleting ~/.vscode-server is an official, auto-healing recovery step — so the real risk guarded against is a partial delete of one piece while the server may still be running, not “this folder is untouchable.” The exclusion list is a UX nicety; the marker-file check is the actual safety net for unlisted tools on other machines.

Test coverage: ScannersTests.csFindBuildDirs_*, HasProjectMarker_*. all-safe is safe to run on a WSL scan again.

Native node_modules/build-artifact scanning Done

Spec’d 2026-07-09, shipped since, correction recorded 2026-07-15.

NativeBuildDirs()/FindNativeBuildDirs() (Scanners.cs:156/:1019) exist, are wired into Program.cs and MainWindow.xaml.cs, and have test coverage in ScannersTests.cs. This section was left unmarked after shipping — kept below for the original design reasoning, not as an open item.

Before this, node_modules/target discovery only ran inside WSL (Wsl()FindBuildDirs). Native Windows project folders (e.g. Downloads\bank-transaction, Downloads\AccountabilityApp) got zero build-artifact visibility — only ever a lump “this folder is large” REVIEW row via DownloadsTopFolders, never “here’s the regenerable part inside it.”

What it does

  • Walks a bounded set of real project roots — Downloads, Documents, Desktop — depth-limited, same shape as WSL’s Walk(root, depth, 6)
  • Reuses HasProjectMarker and the SAFE/REVIEW split as-is (already OS-agnostic — a plain File.Exists check): SAFE only when package.json or Cargo.toml/pom.xml/build.sbt sits next to it, REVIEW otherwise
  • Excludes AppData, Program Files, Program Files (x86), Windows, ProgramData from the walk entirely — never descended into, since installed Electron/VS Code-family apps ship their own bundled node_modules under those paths (same failure class as the WSL .vscode-server incident). A native equivalent of WslAppServerDirNames may still be needed for portable/no-installer apps living elsewhere

What it does NOT do

  • No drive-wide C:\ walk — bounded to the same known user-content roots as existing scanners
  • No auto-detection of arbitrary project roots outside those folders (e.g. a repo cloned to C:\dev) — out of scope for v1

Planned: system-root clutter + user dotfolder scanners

Spec’d 2026-07-09, not yet built.

  • SystemRootClutter — curated exact-name allowlist of C:\ root vendor/installer leftovers. Tier A (intelFPGA, HP eSupport, RyzenPPKG Driver, WCH.CN, DumpStack.log, vfcompat.dll, appverifUI.dll) → REVIEW, delete offered. Tier B (inetpub, flexlm) → INFO only, delete never offered (can be actively-serving IIS sites / license servers). Python3xx, Ruby34-x64, xampp explicitly excluded — installed runtimes, uninstall properly, not a folder-delete target.
  • UserDotfolders.cargo/.cisco/.cursor etc., cross-checked against install/PATH state before ever flagging REVIEW (same honesty as StalePackages — a failed check says “couldn’t confirm,” never silently SAFE). .config excluded entirely (generic dumping ground, no single “still in use” signal). Confirmed via live check 2026-07-09: .cursor is NOT temp/cache despite the app being uninstalled — plans/ and projects/ hold real authored planning docs and per-project session history — so “app uninstalled” alone is not sufficient signal for SAFE.

Known gaps vs. a full manual OS-level cleanup pass

Audited 2026-07-09 against Storage Sense, Disk Cleanup, restore points, optional features, language packs, dev package caches. Not committed to building all of it — recorded so future prioritization starts from a real audit.

Not covered, ranked by likely payoff

  1. Windows.old / previous Windows installation — often 10–20GB, single largest possible one-time win, not investigated whether it currently exists on this machine
  2. Native-Windows node_modules/build-artifact scanning Done
  3. Native dev package caches: NuGet (~/.nuget/packages), pip, Maven (~/.m2, confirmed on PATH), Gradle (~/.gradle), Android SDK. Only Docker and WSL’s ~/.npm are currently handled. Next by this ranking
  4. C:\Windows\Temp (system-wide temp) Done — WindowsTempFolder()
  5. AI model weight caches (Ollama/HuggingFace-style) — different from the .claude/.codex session-log scanning already done; not investigated
  6. WinSxS component cleanup, old driver store packages, Delivery Optimization files, thumbnail cache, DirectX shader cache, System Restore points, optional Windows features (Hyper-V, Sandbox, IIS, SMB1), unused language packs — all untouched, lower estimated payoff/higher effort or risk than the above

Partial vs. the manual checklist’s version

  • DownloadsTopFolders/PersonalFolders show top-N largest items only, not a full drive-wide *.iso/*.zip/gigantic-size sweep
  • InstalledAppsBySize is visibility-only by design — the manual checklist’s “uninstall large programs” step still requires acting via Settings

Deferred (explicitly out of scope for now)

  • Locked-file-owner detail (“who is holding this handle” for a delete failure) — feasible on all 3 target OSes (Windows: Restart Manager API rstrtmgr.dll; Linux: walk /proc/[pid]/fd; macOS: lsof/libproc) but each is a fully different mechanism. Deferred rather than built Windows-only; revisit as a plugin behind the platform-abstraction seam.
  • Unit 4 (--check + toast notification) — the toast mechanism is also platform-specific (Windows Toast vs. Linux libnotify vs. macOS UserNotifications). Deferred until the seam below exists.

Cross-platform MVP scope

Added 2026-07-02. Goal expanded this session: scale to Linux and Mac, not stay Windows-only. That changes what “MVP” means — full feature parity across 3 OSes is not the bar; a shared Core with clean platform seams is.

Per-scanner scope decision — finalized 2026-07-13

Every scanner in Scanners.cs, sorted by what it actually needs — verified against each scanner’s real mechanism, not a guess.

CategoryScannersReasoning
A
already portable
VsCodeCache, NativeBuildDirs, Docker, DownloadsTopFolders, PersonalFolders, AiFolders Ports with zero changes — already use Environment.SpecialFolder/Path.GetTempPath()/plain Path.Combine, none Windows-specific. MyMusic/MyVideos worth a Linux smoke test (XDG mapping less consistent) but not a blocker.
B
needs a seam
RecycleBin, TempFolders Cross-platform concept, per-OS mechanism. RecycleBin’s move-to-trash already has ITrashProvider; empty-the-bin (SHEmptyRecycleBinW) still needs its own seam. Linux target: freedesktop.org trash spec. Mac: ~/.Trash. TempFolders splits into a portable %TEMP% half and a Windows-only WindowsUpdateCache().
C
Windows-only, dropped
Wsl (+ FindBuildDirs) WSL doesn’t exist on Linux/Mac. Not “ported” — just doesn’t run.
D
new scanner, not a port
InstalledAppsBySize, StalePackages, DockerVhdxBloat Windows-only mechanism, concept has an analogue but that’s new scanner work. Linux: dpkg/rpm; Mac: Homebrew casks. StalePackages is UWP/AppX-specific, no Linux/Mac equivalent model. DockerVhdxBloat’s Mac analogue lives under ~/Library/Containers/…, a different shape entirely.

Avalonia port — blocker found (2026-07-15, before any port work started)

Checked the actual .csproj files rather than assuming the seam work made the port ready. It didn’t:

DiskCleanup.Core.csproj:  net10.0-windows
DiskCleanup.csproj:       net10.0-windows
DiskCleanup.Wpf.csproj:   net10.0-windows
DiskCleanup.Tests.csproj: net10.0-windows

All four projects — including Core — target net10.0-windows, a Windows-only TFM. An Avalonia project on Linux/Mac cannot reference Core at all as it stands, independent of whether ITrashProvider is fully seamed. Core also still has Windows-only code mixed directly into shared files:

  • WindowsIdentity in RecycleBin() (Scanners.cs)
  • Microsoft.Win32 registry calls (StalePackages/InstalledAppsBySize — expected per Category D, but confirms the mixing)
  • WindowsTrashProvider.cs itself lives in Core, not a Windows-specific project

Not one unit

There’s a prerequisite unit first: make Core build as portable net10.0 — either multi-target Core, or physically split the Windows-only scanners + WindowsTrashProvider into a separate Windows-specific project that both the WPF app and the future Avalonia build reference. Category A scanners don’t need to move; only the Category B/D Windows-only code does. Not yet decided: multi-target vs. physical split, and exactly which files move — the next thing to spec before writing any Avalonia code.

Recommended next steps, ranked

  1. Fix the FindBuildDirs safety issue Done 2026-07-08
  2. Introduce the platform-abstraction seam in Core Done 2026-07-13 — both Win32 calls that used to live in ActionExecutor (SHFileOperation, SHEmptyRecycleBinW) now go through ITrashProvider, implemented by WindowsTrashProvider. Zero DllImports left in ActionExecutor.
  3. Decide per-OS scanner scope Done 2026-07-13 — see table above
  4. Split TempFolders Done 2026-07-13TempFolders() is now just the portable %TEMP% line; WindowsUpdateCache() is the new Windows-only scanner, wired into both apps
  5. Split/multi-target Core so it builds as portable net10.0 Not started — actual next unit
  6. Only then: the Avalonia UI port and Unit 4/toast, since both depend on the seam existing first Depends on #5

Context / background

  • Language: C# (.NET, self-contained console app)
  • Folder is stateless — each run scans fresh, no saved history between runs
  • 45GB free-space threshold is the trigger for the --check notification

Risk levels used in the earlier manual cleanup pass, for reference

Safe Recycle Bin, build caches (node_modules, target/, npm/VSIX caches) — fully regenerable ·  Review Downloads project folders, AppData\Local\Packages bloatware candidates, Docker prune/compaction ·  Info only installed apps list — no action, just visibility

Notes.md · build journal

Session log

Done

  1. Self-deletion guard (#1)DownloadsTopFolders now excludes the disk-clean-checklist tool’s own folder via GetSelfRootUnder().
  2. Recycle-bin safety net (#2)ActionKind.MoveFolderToRecycleBin added to CheckItem.cs; the three REVIEW-risk folder scanners (DownloadsTopFolders, StalePackages, AiFolders) use it instead of permanent DeleteFolder; ActionExecutor now handles it via SHFileOperation with FOF_ALLOWUNDO (soft-delete, recoverable).
  3. Symlink guard (#3), scan sideGetDirectorySize skips reparse points (won’t follow symlinks/junctions when computing sizes).
  4. Symlink guard (#3), delete sideDeleteFolder and DeleteContents now use SafeDeleteTree, which deletes a reparse point link itself rather than recursing into its target.
  5. WSL UNC path note (#5)DeleteFolder and MoveFolderToRecycleBin append a compaction reminder to the result message when the path starts with \\wsl.localhost\.
  6. WPF “Clean Selected” checkbox fix — ticking a row’s checkbox visually worked but never committed to CheckItemViewModel.IsSelected before CleanButton_Click read it.
    Root cause & fix

    DataGrid’s cell-edit/binding-commit timing: neither switching DataGridCheckBoxColumn to DataGridTemplateColumn nor adding an explicit ItemsGrid.CommitEdit() fixed it. Fixed by bypassing the binding commit entirely — the CheckBox’s Click event now calls ItemCheckBox_Click, which sets vm.IsSelected directly (same direct-assignment mechanism SelectAllSafeButton_Click already used successfully). IsChecked binding is now OneWay (display only); CommitEdit() was left in as a harmless no-op safety net.

Tests

  • ActionExecutorTests covers: DeleteFolder, DeleteContents, MoveFolderToRecycleBin, SuggestCommand, None, and the reparse-point guard (DeleteFolder_DoesNotRecurseIntoJunction, built with mklink /J — no Developer Mode/elevation needed, so it runs for real on every machine instead of silently skipping like the earlier symlink-based version did).
  • Confirmed: Developer Mode is not required for end users. The guard only reads the ReparsePoint attribute and deletes existing links — both privilege-free operations. The privilege requirement only applied to .NET’s CreateSymbolicLink, which the old test used to manufacture a throwaway symlink for itself.
  • All 19 tests passing (verified 2026-06-17).
  • Checkbox fix diagnosed via the CLI (DiskCleanup console app): same Scanners/ActionExecutor Core, but a completely different selection mechanism (typed numbers, no DataGrid). CLI cleaning worked first try, which proved Core/ActionExecutor were never the problem and isolated the bug to the WPF checkbox specifically. No automated test covers WPF UI binding behavior — Core remains the only tested layer; this fix is manual-verification only.

v0.0.0 release doesn’t run Open — fix decided, not shipped

Found 2026-08-07, diagnosing a downloaded exe that wouldn’t launch on a machine that should have worked.

The v0.0.0 GitHub release has exactly one asset, DiskCleanup.Wpf.exe (162,304 bytes, confirmed via the GitHub API) — an exact byte-match to the file that fails to launch locally, ruling out a corrupted/partial download. DiskCleanup.Wpf.csproj has no <SelfContained> or <RuntimeIdentifier>, so dotnet publish produced a framework-dependent build: a small apphost stub plus DiskCleanup.Wpf.dll, DiskCleanup.Core.dll, .deps.json, and .runtimeconfig.json, all required next to the exe to run. Only the bare exe got attached to the release — none of the companion files. The apphost can’t find its own managed assembly and exits before it gets far enough to show a window or log a crash, which is why the exe produces literally nothing on launch: no window, no error dialog, no entry in the Application or Defender event logs either way.

What was ruled out first
  • Not SmartScreen — reproduced after explicitly clicking “Run anyway”.
  • Not antivirus — Windows Defender’s operational log showed only routine health-check entries, no detection/quarantine events, for the whole window around the failed launch.
  • Not a missing .NET runtime — dotnet --list-runtimes confirmed Microsoft.WindowsDesktop.App 10.0.2 installed on the test machine.
  • The README’s “self-contained, no runtime needed” claim is also inaccurate for how this build was actually published — worth fixing regardless of which packaging fix ships.

Fix, built and verified 2026-08-07: republished as self-contained + single-file. First attempt (dotnet publish -r win-x64 --self-contained true -p:PublishSingleFile=true) still left five native WPF DLLs (D3DCompiler_47_cor3.dll, PresentationNative_cor3.dll, wpfgfx_cor3.dll, PenImc_cor3.dll, vcruntime140_cor3.dll) as loose files next to the exe — copying just the exe into an isolated folder reproduced the exact same silent-failure shape as the original bug (confirmed as a real DllNotFoundException, not silent, when run from a console). PublishSingleFile alone doesn’t bundle native (non-IL) dependencies for WPF.

Working command: dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true — the extra flag makes those native DLLs self-extract to a temp folder at first launch instead of needing to sit alongside the exe. Verified: exe copied alone into an isolated folder, launched via Start-Process, confirmed a real window (title “Disk Cleanup”, valid MainWindowHandle) opened — not just “no crash.” Output is one ~133MB exe, no companion files. Not yet uploaded to the GitHub release — that’s the remaining step.

Next

  • Republish v0.0.0 (or cut v0.0.1) self-contained + single-file per “v0.0.0 release doesn’t run” above.
  • Decide on git init + release strategy (tag Windows version, branch for Avalonia work).
  • Avalonia refactor (cross-platform — Windows, Linux, Mac). Plan: new DiskCleanup.Avalonia project; retarget Core to net10.0 with RuntimeInformation guards around Win32 APIs; port XAML (Avalonia differs from WPF on DataGrid, styles, dialogs); add Linux/Mac scanner branches.

    Research this session: SHFileOperation/SHEmptyRecycleBinW (the REVIEW-risk soft-delete path) have no Linux/Mac equivalent, and .NET has no built-in cross-platform trash API. A real port means implementing the freedesktop.org trash spec by hand on Linux (move to ~/.local/share/Trash/files + a .trashinfo sidecar) and the simpler ~/.Trash convention on Mac — not just swapping the UI framework.

External review (Review.md) findings

  1. Fixed Docker scanner showed 0B-reclaimable rows as actionable (Scanners.cs ~126) — now skips any row whose reclaimable string starts with 0B instead of matching on % (which "0B (0%)" also contains).
  2. Fixed FindBuildDirs didn’t skip reparse points while walking (Scanners.cs ~339) — now checks FileAttributes.ReparsePoint before recursing into or collecting a subdirectory, matching GetDirectorySize’s existing guard.
  3. Not fixed — design tradeoff catch{} swallows scan-time errors silently across Scanners.cs — no visibility when Docker/WSL/registry access fails. Same pattern on the delete side: SafeDeleteTree (ActionExecutor.cs ~140–144) swallows per-file/per-subfolder failures, surfacing only a vague “directory not empty” with no indication of which nested file blocked it. Seen live this session on WSL node_modules under ~/projects/kan — likely Windows’ 260-char path limit colliding with pnpm’s deeply-nested .pnpm store plus the \\wsl.localhost\… UNC prefix. Would need a “collect warnings, show them” design decision before touching code — not done yet.
  4. Fixed CleanButton_Click ran synchronously on the UI thread, freezing the window on large deletes with no feedback. ActionExecutor.Execute calls now run via Task.Run (same pattern ScanButton_Click already used for scanning); Clean/Scan buttons disable and StatusText shows “Cleaning N item(s)…” while it runs. Still no per-item progress bar — bigger feature, not in scope for this fix.
  5. Not a fix SHFileOperation is legacy but defensible — review frames this as “know how to justify it,” not a fix; folded into the Avalonia research above instead.

Real-detection scope

Fixed WSL pnpm store size detection

Per-package node_modules reported near-zero bytes because GetDirectorySize skips the symlinks pnpm uses internally (confirmed live: a real node_modules under ~/projects/kan/apps/web reported 8KB via the tool’s logic vs. 13MB following symlinks); the real bytes live in pnpm’s content-addressable store (~/.local/share/pnpm/store, confirmed 1.7GB on this machine), which Wsl() never visited. Added as its own SAFE/DeleteFolder row (same tier as ~/.cache, ~/.npm) plus a Reason explaining it’s a regenerable shared cache, not project data. Default path only — no handling for a custom pnpm config set store-dir. No new test — Wsl() has no path-injection seam; verification is live against the real distro instead.

Fixed Docker vhdx bloat (Unit 6)

The old “deferred, Docker not installed” premise no longer held: Docker Desktop is actually installed and running on this machine (docker-desktop WSL distro, state Running), with a real vhdx confirmed at ~59.85GB. New scanner DockerVhdxBloat() recursively globs %LocalAppData%\Docker\wsl\**\*.vhdx (doesn’t hardcode a subfolder name — confirmed this machine uses main, not the disk/data guessed earlier), flags files ≥20GB as REVIEW with ActionKind.SuggestCommand (wsl --shutdown + diskpart compact — not Optimize-VHD, since that needs the Hyper-V module Docker Desktop’s WSL2 backend doesn’t require), plus a Reason explaining the disk grows but never auto-shrinks. Wired into both apps’ scan lists. No automated test — same precedent as Docker()/Wsl(). Unit 4 (--check/toast) deliberately held off instead — needs a Windows-only NuGet dependency, and that choice should wait until the cross-platform question is settled.

Built Unit 5 — StalePackages AppX cross-check

Cross-checks AppData\Local\Packages folder names (= PackageFamilyName) against Get-AppxPackage to distinguish “app uninstalled, orphaned data” from “app still installed, folder metadata is just old” (mtime-only was a weak signal — NTFS dir mtime only updates on direct child add/remove). Shells out to powershell -Command Get-AppxPackage (matches the existing Docker()/GetWslDistros() ProcessStartInfo pattern) rather than the native PackageManager WinRT API (would need a TFM retarget off plain net10.0-windows across all 3 projects for one lookup). Stays Risk=REVIEW either way — the orphaned-vs-installed distinction lives in the new Reason field, not a 4th risk tier. Added a static shared-runtime-package allowlist (Microsoft.VCLibs, Microsoft.NET.Native, Microsoft.UI.Xaml, Microsoft.WindowsAppRuntime, Microsoft.Services.Store.Engagement) so framework components never get flagged regardless of staleness. IsSharedRuntimePackage made public specifically for testing (no path-injection seam exists for StalePackages itself, matching Docker/Wsl/TempFolders precedent).

Built CheckItem.Reason + WPF details pane

Prompted by hands-on evaluation: grid labels were getting truncated with no way to read the rest, and REVIEW items (.claude\shell-snapshots, AppData\Local\Packages folders) gave no way to judge “will this affect an app I use” beyond the bare label. Added an optional Reason field to CheckItem carrying a plain-language explanation, populated for StalePackages and both AiFolders scanners. Labels were shortened back down to just the path — the long stuff moved into Reason. WPF gained a details pane below the grid (MainWindow.xaml row 2, wired via ItemsGrid_SelectionChanged) showing the selected row’s full label, full path, and Reason in a wrapped, scrollable text block. CLI untouched — Program.cs still only prints Label — Size — Risk; Reason isn’t surfaced there (scoped out, not forgotten).

Built AiFolders safety fix + staleness refinement (Units 7–8)

Spec’d next session originally — found a live safety bug; the fix shipped before the feature work.

Current AiFolders() used to offer the literal root .claude/.codex folder as one MoveFolderToRecycleBin item. Verified live on this machine: that root holds .credentials.json/auth.json, settings.json/config.toml, CLAUDE.md/MEMORY.md (persistent cross-session memory), and live SQLite runtime state right next to disposable caches — selecting that one row would recycle-bin all of it.

Fixed by replacing it with ScanClaudeFolder()/ScanCodexFolder(), an allowlist of verified-safe subpaths only (never the root) — chosen over a denylist specifically because a denylist would re-create this same bug for any new file/folder a future Claude Code or Codex update adds, until someone notices and excludes it.

Safe: .claude/shell-snapshots, paste-cache, debug, file-history (labeled “edit history,” not “cache”); .codex/cache, log, tmp, .tmp.
Excluded (verified why, on this machine): .claude/sessions + ide (live PID-keyed process state for running instances, not history); .claude/cache, backups, session-env (held back — cache/ turned out to hold an app-update changelog, not session junk); .codex/.sandbox*, .sandbox-bin, .sandbox-secrets (live executables + ACL state + sandbox_users.json — not safe).

Session-file judgment: no date filter at all — every .jsonl shows up regardless of age. Revised away from a day-threshold (90, then 60) after evaluation surfaced that a single one-and-done session is just as “done” the day it’s created as months later, so age alone can’t signal abandonment. The only exclusion is the session actually still open: for .claude, the transcript filename is literally the sessionId, cross-checked against the live PID files in sessions/*.json ({"pid":...,"sessionId":...}) — exact, not a guess. For .codex there’s no equivalent live-PID-to-session mapping, so anything modified in the last ~30 minutes is treated as possibly still active — a known asymmetry (Claude’s check is precise, Codex’s is a time-based heuristic), not an oversight.

Each shown file gets a label with age in days, a message count, and a cursory excerpt (first human-typed message, truncated, read straight from the .jsonl) — explicitly not an AI-generated summary (would add a first outbound API dependency, cost, and latency this fully-local tool doesn’t have today). For Codex, the excerpt logic skips tool-injected <environment_context>-style entries that show up under "role":"user" but aren’t anything the human actually typed.

Display: grouped list — per-file CheckItems printed under their project name, not a full tree-view/treemap (considered, rejected as more UI work than needed right now). Needed a new ActionKind.MoveFileToRecycleBin (single-file recycle-bin move) since a project folder mixes disposable .jsonl files with the untouchable adjacent memory/ subdir. Decided: per-file rows over per-project aggregation, since aggregation would need a new bulk-action that re-derives the relevant-file set at execute time, reopening a smaller version of the root-folder risk if written carelessly.

Verification: DiskCleanup.Tests\ScannersTests.cs covers the root/memory/sandbox exclusions, the active-session exclusion (Claude exact, Codex grace-window), and that old and brand-new session files both still appear. Confirmed manually via dotnet run --project DiskCleanup against real ~/.claude/~/.codex data.

Deferred (superseded) Unit 6 — DockerVhdxBloat, original note

New scanner idea — compare the physical Docker Desktop WSL2 .vhdx file size (never auto-shrinks) against logical usage, suggest a diskpart compaction command. Deferred at the time: Docker wasn’t installed on this machine and %LocalAppData%\Docker\wsl\ was empty, so it couldn’t be live-verified — revisit once on a machine with Docker Desktop installed. (Superseded by the “Fixed” entry above once Docker Desktop was installed.)

Review.md · external audit

Code review & evidence-tier audit

Review findings

Stale findings note

Findings #1 (Docker 0% rows) and #2 (FindBuildDirs following reparse points) reference code that has since changed — current Scanners.cs already filters 0B reclaimable rows (line 170) and already skips ReparsePoint inside FindBuildDirs (lines 813–818). Confirmed fixed after this review was written — see the Build Log tab.

1. Docker scanner likely shows zero-reclaim items as actionableResolved since

In Scanners.cs:126, this condition adds Docker rows when reclaimable.Contains('%') || reclaimable.Contains("0B"). Docker output usually includes percentages, so this can include rows with 0B (0%), which makes the UI suggest work where there is nothing to reclaim. A stronger version should parse the reclaimable size and only show meaningful values.

2. WSL project scan can follow symlinks/junctions while discovering build foldersResolved since

GetDirectorySize correctly skips FileAttributes.ReparsePoint, but FindBuildDirs does not check that before recursing: Scanners.cs:339. That weakens the promise that symlinks and junctions are never followed. Microsoft documents ReparsePoint as the marker for files/directories containing reparse-point data, so this is the right flag to use consistently.

3. Errors are swallowed during scanningDesign tradeoff, open

Many scanner methods use broad catch { }, e.g. Scanners.cs:27. For a personal tool this is tolerable, but as an engineer you should explain the tradeoff: it keeps scanning resilient, but hides permission, Docker, WSL, and registry failures from the user. A better version would collect warnings and show them in the CLI/WPF log.

4. WPF cleanup runs on the UI threadResolved since

The scan is correctly offloaded with Task.Run, but cleanup itself ran synchronously inside CleanButton_Click: MainWindow.xaml.cs:82. Large deletes or shell operations can freeze the window. A junior should recognize this as a UX/responsiveness issue and propose async execution plus disabled controls/progress state.

5. Use of SHFileOperation is defensible but legacyKnow your justification

The code correctly checks both the return code and fAnyOperationsAborted: ActionExecutor.cs:164. Microsoft’s docs say SHFileOperation was replaced by IFileOperation, and that FOF_ALLOWUNDO sends deletes to the Recycle Bin. So the current implementation is reasonable for a small app, but you should know it’s an older API and be able to justify why you used it.

What you’re expected to know

Architecture

  • DiskCleanup.Core owns scanning, selection parsing, and action execution
  • DiskCleanup is the CLI shell over Core
  • DiskCleanup.Wpf is the desktop UI over the same Core logic
  • Tests target Core because that’s where the important behavior lives

Safety model

  • Nothing is deleted until the user explicitly selects and confirms
  • Safe means regenerable, not automatically deleted
  • Review uses Recycle Bin where possible
  • Info rows, like installed apps, are non-actionable
  • Symlinks/junctions are meant to be treated as links, not traversed targets

Questions you should be able to answer

Why use MoveFolderToRecycleBin for REVIEW items but direct deletion for temp/cache contents?

REVIEW items carry real uncertainty about whether the content is wanted — Recycle Bin keeps the action reversible. Temp/cache contents are Tier 1/2 evidence (OS- or tool-documented as transient), so direct deletion matches the confidence level.

Why are installed apps informational only?

Uninstalling is a registry/MSI-level operation with side effects this tool doesn’t own — it surfaces visibility, and the user acts through Settings.

What can go wrong when deleting files?

Permissions, locked files, long paths, reparse points, partial success — each needs its own handling rather than a single try/catch swallowing all of them the same way.

Why is ProcessStartInfo used for Docker/WSL, and what risks exist with redirected output/timeouts?

Both are external CLIs with no .NET-native API — shelling out is the only path. Microsoft explicitly warns redirected streams can deadlock if handled poorly (e.g. reading stdout synchronously while the child blocks writing to a full stderr buffer), so knowing this docs area matters.

Why keep the core logic separate from WPF?

So the same scanning/selection/execution logic is testable headlessly and reusable across the CLI, WPF, and eventually Avalonia front ends without duplication.

What tests exist, and what gaps remain?

Core (Scanners, ActionExecutor) is tested; WPF UI binding behavior is not — the checkbox-commit bug in the Build Log was diagnosed by isolating Core via the CLI, not by a UI test.

Verification

Ran dotnet test. It passed: 19 tests passed, 0 failed. The first run needed package restore and failed under restricted network access; rerunning with approved NuGet access succeeded.

Docs worth knowing

Evidence-tier audit

Added 2026-07-07 after a discussion about AI-assisted rules risking unverified or hallucinated “knowledge” baked into the classifier. Every SAFE/REVIEW label in Scanners.cs rests on a claim about what a path means. This sorts each claim by how it’s actually checkable, not by who first said it.

TierWhat it means
Tier 1Tool/OS self-report — the tool’s own accounting (docker system df, Get-AppxPackage, the registry) or a documented OS mechanism. Highest trust, true regardless of who mentioned it.
Tier 2Documented spec/convention — a written external doc exists (XDG cache spec, npm/pnpm/cargo docs, Microsoft KB). Verifiable by reading the source.
Tier 2bTool-inspection knowledge, no formal spec. True as observed on this machine/version, but not backed by a citable doc. Weaker than Tier 2 — re-verify if the tool updates.
Tier 3Heuristic inference — age, size, or name pattern used as a proxy for “unwanted.” No document proves it, a judgment call. Should never sit in SAFE.
Tier 4Incident-informed — the rule exists because something already broke once.

Tier 1

  • Recycle Bin — SAFE, line 26. OS mechanism; items are already user-deleted.
  • Docker reclaimable rows — REVIEW, line 180. Docker’s own system df self-report. Still gated behind SuggestCommand (copy/paste, not auto-run) — extra friction kept even on high-trust data, the right call since these deletes are irreversible.
  • StalePackages installed/orphaned check — REVIEW, lines 312–315. The installed-or-not fact comes from Get-AppxPackage (Windows’ own registry); only the “stale = safe” layer on top is Tier 3.
  • InstalledAppsBySize — INFO only, line 397. Registry read, no action possible either way.

Tier 2

  • User Temp / SoftwareDistribution\Download — SAFE, lines 38 & 42. %TEMP% and Windows Update’s download cache are both Microsoft-documented as transient/safe to clear.
  • VS Code CachedExtensionVSIXs — SAFE, line 54. Documented in VS Code’s own GitHub issues as a re-downloadable install cache (community-level doc, closer to 2b in strength).
  • WSL ~/.cache — SAFE, line 74. XDG Base Directory spec explicitly defines this as non-essential, regenerable.
  • WSL ~/.npm — SAFE, line 79. npm’s own docs describe this as the package cache; npm cache clean is an official, supported operation.
  • WSL pnpm store — SAFE, line 88. pnpm’s own docs describe the store as reconstructable via re-fetch/re-link. Higher blast radius than the others (shared across every pnpm project on the distro) — worth an actual empirical check once before fully trusting this one.
  • node_modules/target WITH a project marker — SAFE, line 107. Standard tool convention, gated by a verifiable marker file rather than name-matching alone — the marker check is what keeps this out of Tier 3.
  • DockerVhdxBloat compaction — REVIEW, line 209. Documented Hyper-V/Docker Desktop behavior (vhdx grows, never auto-shrinks); compaction doesn’t delete data. Kept at REVIEW + manual command anyway.

Tier 2b

  • .claude cache dirs (shell-snapshots, paste-cache, debug, file-history) — REVIEW, line 474. .codex cache dirs (cache, log, tmp, .tmp) — REVIEW, line 586. Known from inspecting these tools directly, not from a published spec. Correctly kept at REVIEW rather than SAFE — re-verify if either CLI changes its on-disk layout.

Tier 3 — all currently REVIEW, correctly, none should ever become SAFE

  • node_modules/target WITHOUT a project marker — REVIEW, line 113. Appropriately downgraded once the verifiable marker check fails.
  • DownloadsTopFolders — REVIEW, line 242. “Big” is not “unwanted”; pure size heuristic, mitigated by Recycle Bin.
  • PersonalFolders (Documents/Desktop/Pictures/Music/Videos top-N) — REVIEW, line 424. Zero semantic knowledge about the file — closest thing in this project to what a manual disk-usage viewer shows and leaves for a human to judge. Correctly the least-trusted bucket.
  • StalePackages “stale = safe to delete” reasoning — REVIEW, lines 309–315. The orphan fact is Tier 1, but “hasn’t been touched in 6 months” as a proxy for “unwanted” is a guess layered on top.
  • AI session transcripts (.jsonl) — REVIEW, lines 512 & 611. Best-designed REVIEW row in the file: instead of asserting a size-only heuristic, it surfaces the underlying signal (age, message count, first-message excerpt) so the human does the actual judgment call.

Tier 4

  • WSL app-server dirs (.vscode-server, .cursor-server, etc.) — excluded entirely as INFO, line 120. Exact failure class as the project memory incident — a rule that looked reasonable and wasn’t, now hardcoded as never-touch rather than re-classified.
  • .claude / .codex root folders — never offered at all. Holds credentials/settings/live memory; precaution, not inferred.
  • FindBuildDirs walking the whole WSL home instead of ~/projects — same failure class as the .vscode-server incident; see the Build Log tab — fixed 2026-07-08.

What this leaves you with

  • Nothing is currently labeled SAFE on Tier-3-only evidence — the purely heuristic buckets (Downloads/Personal top-N, stale packages, AI session files) are already REVIEW and Recycle-Bin-backed
  • The weakest “trusted” items are Tier 2b (Claude/Codex internal folder names) — true today, but only because someone inspected the tool, not because a spec promises it won’t change
  • The pnpm store SAFE rule (Tier 2, but high blast radius) is the one item on this list worth a deliberate one-time empirical test before trusting it further