Filesystem Analysis & Disk Cleanup
Filesystem Analysis & Disk Cleanup shows what is consuming disk space on a device and gives you safe, auditable ways to reclaim it. Three things work together: a scan that builds a per-volume snapshot, a file cleanup that previews candidates and then deletes exactly the paths you approved, and a set of OS-native cleaners that reclaim the space a file scanner structurally cannot see – the Windows component store, Windows.old, APFS local snapshots, the systemd journal.
Overview
Section titled “Overview”Disk exhaustion is one of the most common and disruptive problems in a managed fleet: application crashes, failed updates, corrupted databases. Breeze answers it in three layers, each feeding the next:
- Analysis – a parallel, resumable scan of one volume produces a snapshot: largest files and directories, temp accumulation, old downloads, unrotated logs, trash usage, duplicate candidates and the list of cleanup candidates.
- File cleanup – a preview turns the snapshot’s candidates into a pinned plan; execute deletes only paths from that plan, permanently, and reports what actually happened to each one.
- OS-native cleaners – a closed catalog of vetted maintenance tools (Windows Disk Cleanup handlers, DISM,
tmutil,brew,apt-get/dnf,journalctl) that is listed with estimates, then run with explicit confirmation.
One concept, one home. All three live in the device’s Disk Cleanup tab (/devices/:id#filesystem). The File Manager no longer carries its own inline cleanup section; its Disk Cleanup button navigates to this tab. Everything is also available through the REST API and the AI assistant.
Volumes
Section titled “Volumes”A scan targets one volume at a time. GET /devices/:id/filesystem/volumes lists the volumes a device can scan and clean, derived from the disk inventory the agent already reports:
{ "success": true, "data": [ { "mountPoint": "C:\\", "scanPath": "C:\\", "fsType": "NTFS", "totalGb": 476.9, "usedGb": 401.2, "freeGb": 75.7, "usedPercent": 84.1, "isOsRoot": true, "scanState": { "lastRunMode": "incremental", "lastBaselineCompletedAt": "2026-10-18T02:14:00.000Z", "hasCheckpoint": false }, "latestSnapshot": { "id": "8f2b...", "capturedAt": "2026-10-20T09:31:00.000Z", "partial": false, "cleanupEstimateBytes": 12884901888 } } ]}| Field | Description |
|---|---|
mountPoint |
Exactly what the agent reported, so the UI can show the operator’s own string. |
scanPath |
The normalised key (see below). This is what scan_path stores and what POST /scan sends. |
fsType, totalGb, usedGb, freeGb, usedPercent |
From the disk inventory. null (never 0) when no disk row backs the volume. |
isOsRoot |
true for the OS volume (C:\ on Windows, / elsewhere). |
scanState |
The volume’s own last run mode, last baseline time and whether a resumable checkpoint exists; null if it has never been scanned. |
latestSnapshot |
The newest snapshot for this volume, with its cleanup estimate; null if none. |
Only fixed local volumes are offered. The following filesystem types are never listed (NON_SCANNABLE_FS_TYPES): read-only media cdfs, udf, iso9660, squashfs; memory filesystems tmpfs, devtmpfs; kernel filesystems proc, sysfs; container and network mounts overlay, nfs, nfs4, cifs, smbfs, 9p, autofs and every fuse* variant. Windows UNC paths (\\server\share) are excluded as well. Deleting through a network mount frees space on a machine that is not the one being cleaned, which is the cross-volume mistake this design exists to stop. A volume whose fsType is blank is still offered, because the agent reported a real mount point. At most 24 volumes are listed per device.
The device’s OS volume is always listed, even on a device that has not yet reported a disk inventory, so a fresh endpoint can still be scanned.
In the tab, the volumes appear as chips at the top (used/free bar, last-scan age, an OS badge). Selecting a chip re-keys every panel below it – scan controls, snapshot panels and the file-cleanup panel all show that volume’s state. The chip selection is component state, not part of the URL.
Scan paths are normalised
Section titled “Scan paths are normalised”The volume key is a normalised path (normalizeScanPath in @breeze/shared). It is what scan_path stores, what ?path= expects, and what the agent receives in the filesystem_analysis command:
- Windows – separators become
\, repeats collapse, the drive letter is upper-cased, and a trailing\survives only on a volume root.c:/Users//Todd/andC:\Users\Toddare the same path;c:\,C:/andC:are allC:\. A UNC path keeps its two leading separators so the volumes filter can recognise and refuse it. - macOS and Linux – repeats collapse,
.and..resolve, and a trailing/survives only on/itself. Case below the root is preserved:/Usersand/usersare different directories on a case-sensitive volume, and folding them would silently merge two scans.
Requests may send any of these forms; the API normalises before reading or writing. This is why c:\ and C:\ are the same volume and resume the same checkpoint, and why every response that concerns one volume echoes the normalised scanPath it used.
Deep Filesystem Scan
Section titled “Deep Filesystem Scan”A scan is started with a POST to the scan endpoint naming a volume (or a directory inside one). The API queues a filesystem_analysis command to the agent over WebSocket (preferred) or the command queue. The agent walks the tree with a worker pool – several goroutines each process a subtree and report to a coordinator – so a volume with millions of entries completes in minutes rather than hours.
Scan strategies
Section titled “Scan strategies”| Strategy | Behavior | When to use |
|---|---|---|
baseline |
Full recursive scan from the path, up to maxDepth. |
First scan of a volume, or after a large change in usage. |
incremental |
Scans only the “hot directories” that changed significantly since the last baseline. | Routine follow-ups to detect new accumulation. |
auto |
The API picks baseline or incremental from the volume’s scan state. |
Default. |
With auto, the API decides in this order:
-
If the normalised path is not one of the device’s volume scan paths (a subdirectory), a
baselinescan is always used. -
If a previous scan of this volume was interrupted and checkpoint data exists, a
baselinescan resumes from the checkpoint. -
If no baseline has ever completed for this volume, a
baselinescan is started. -
If the volume’s disk usage has changed by more than 3% since its last baseline, a
baselinescan is triggered. -
Otherwise, if hot directories exist from the previous scan, an
incrementalscan targets only those.
Scan state is keyed by (device, scan path). Scanning D:\ no longer resets the C:\ baseline, pollutes its hot-directory list, or becomes the snapshot a C:\ preview would delete from.
Resumable state
Section titled “Resumable state”Long baseline scans checkpoint into device_filesystem_scan_state: which directories are done and which are pending. If the scan is interrupted (agent restart, network drop, timeout), the next auto or baseline scan of the same volume resumes from the checkpoint rather than starting over.
| Field | Type | Description |
|---|---|---|
deviceId |
UUID | First half of the primary key. |
scanPath |
text | Normalised volume path. Second half of the primary key – one state record per volume. |
scanGeneration |
UUID | The command that owns the current scan; results from a superseded command are rejected. |
lastRunMode |
string | baseline or incremental. |
lastBaselineCompletedAt |
timestamp | When the last full baseline finished. |
lastDiskUsedPercent |
real | Usage at the last baseline, used for the 3% rescan rule. |
checkpoint |
JSONB | Pending directories and depth information for resume. |
aggregate |
JSONB | Accumulated results from completed subtrees. |
hotDirectories |
JSONB | High-churn directories for incremental scans (up to 24). |
Scan configuration
Section titled “Scan configuration”POST /api/v1/devices/:deviceId/filesystem/scanContent-Type: application/json
{ "path": "D:\\", "strategy": "auto", "maxDepth": 32, "topFiles": 50, "topDirs": 30, "maxEntries": 5000000, "workers": 8, "timeoutSeconds": 300, "followSymlinks": false}| Field | Type | Default | Description |
|---|---|---|---|
path |
string | OS volume | Volume or directory to scan. Normalised server-side. Max 2048 characters. |
strategy |
enum | auto |
auto, baseline, or incremental. |
maxDepth |
number | – | Maximum directory depth (1-64). |
topFiles |
number | – | Largest files to keep (1-500). |
topDirs |
number | – | Largest directories to keep (1-200). |
maxEntries |
number | – | Hard traversal cap (1,000 to 25,000,000). |
workers |
number | – | Parallel scan workers (1-32). |
timeoutSeconds |
number | 300 (baseline) / 120 (incremental) | Scan timeout (5-900). |
followSymlinks |
boolean | – | Follow symbolic links during traversal. |
“Root-scoped” means the normalised path equals one of the device’s volume scan paths – not a string compare against C:\. A scan of D:\ on a two-drive machine is root-scoped for D:\.
The response is 202 Accepted with the command id and the chosen mode:
{ "success": true, "data": { "commandId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "queued", "createdAt": "2026-02-20T14:30:00.000Z", "scanMode": "baseline", "strategy": "auto", "scanPath": "D:\\" }}Filesystem Snapshots
Section titled “Filesystem Snapshots”When a scan completes, the result is saved as an immutable snapshot in device_filesystem_snapshots.
| Field | Type | Description |
|---|---|---|
id |
UUID | Snapshot identifier. |
deviceId |
UUID | The device that was scanned. |
scanPath |
text | The normalised volume or path this snapshot covers. |
capturedAt |
timestamp | When the snapshot was created. |
trigger |
enum | on_demand (user-initiated) or threshold (automatic). |
partial |
boolean | true if the scan was interrupted or incomplete. |
summary |
JSONB | Files, directories and bytes scanned, max depth, permission-denied count. |
largestFiles |
JSONB | Largest files by size (up to 50). |
largestDirs |
JSONB | Largest directories (up to 30). |
tempAccumulation |
JSONB | Temp accumulation by category (bytes per category). |
oldDownloads |
JSONB | Download-directory files not accessed recently (up to 200). |
unrotatedLogs |
JSONB | Large log files that have not rotated (up to 200). |
trashUsage |
JSONB | Trash / recycle bin contents (up to 16). |
duplicateCandidates |
JSONB | Likely duplicates by size and name (up to 200). |
cleanupCandidates |
JSONB | Everything eligible for safe cleanup (up to 1,000). |
errors |
JSONB | Scan errors, permission denied and the like (up to 200). |
“Latest snapshot” is always per scan path. GET /devices/:id/filesystem?path= returns the newest snapshot for that volume (default: the OS volume) and echoes the scanPath it resolved; a scan of another volume never changes the answer.
GET /api/v1/devices/:deviceId/filesystem?path=D:\{ "success": true, "data": { "id": "snapshot-uuid", "deviceId": "device-uuid", "scanPath": "D:\\", "capturedAt": "2026-02-20T14:35:00.000Z", "trigger": "on_demand", "partial": false, "scanMode": "baseline", "path": "D:\\", "summary": { "filesScanned": 1250000, "dirsScanned": 85000, "bytesScanned": 450000000000, "maxDepthReached": 24, "permissionDeniedCount": 12 }, "topLargestFiles": [], "topLargestDirectories": [], "tempAccumulation": [ { "category": "browser_cache", "bytes": 2500000000 } ], "oldDownloads": [], "unrotatedLogs": [], "trashUsage": [], "duplicateCandidates": [], "cleanupCandidates": [], "errors": [] }}Snapshot merging
Section titled “Snapshot merging”An incremental scan is merged into the volume’s existing snapshot: summary counts accumulate, max depth takes the maximum, largest files and directories deduplicate by path keeping the larger size, temp accumulation sums per category, cleanup candidates, old downloads, unrotated logs and trash entries deduplicate by path, and error lists concatenate up to 200.
Disk Cleanup
Section titled “Disk Cleanup”File cleanup is select → confirm → execute → result, inside the tab:
- The Cleanup panel shows category cards with byte totals and a candidate table sorted by size, with per-row checkboxes and “select all in category”. The panel reads the selected volume’s latest snapshot.
- Execute opens a destructive confirmation naming the volume, the count and bytes, and the first paths. Recycle-bin and trash rows carry a note that their contents are deleted, not the folder.
- The request is pinned to the preview’s
cleanupRunId. The result panel reports reclaimed bytes and thecompleted / skipped_locked / rejected / skipped_budgetcounts; failures are listed in an amber list, never a green box.
What gets cleaned
Section titled “What gets cleaned”Candidates are classified by a rooted rule table shared byte-for-byte by the agent, the API and the web app (packages/shared/src/utils/cleanupRules.json, embedded into the agent binary). Rules match path components from the volume root, never substrings: a directory that merely happens to be named tmp or Caches – /opt/myapp/tmp, /System/Library/Caches – is not in scope. Patterns are lower-cased, /-separated, and start at <vol> (the scanned volume root) on Windows or / on POSIX; * matches exactly one component and ** the subtree.
| Category | OS | Matches | Excludes | Min age |
|---|---|---|---|---|
temp_files |
Windows | <vol>/windows/temp/**, <vol>/users/*/appdata/local/temp/** |
– | 24 h |
temp_files |
macOS | /tmp/**, /private/tmp/**, /private/var/tmp/**, /private/var/folders/*/*/t/** |
– | 24 h |
temp_files |
Linux | /tmp/**, /var/tmp/** |
/tmp/.X11-unix/**, /tmp/.ICE-unix/**, /tmp/systemd-private-*/** |
24 h |
browser_cache |
Windows | Chrome, Edge, Brave and Chromium User Data/*/{Cache, Code Cache, GPUCache, DawnCache, GraphiteDawnCache, ShaderCache} and Service Worker/{CacheStorage, ScriptCache}; Firefox profiles/*/{cache2, startupCache, shader-cache} |
– | – |
browser_cache |
macOS | /users/*/library/caches/**, /library/caches/** |
Homebrew (goes to package_cache), com.apple.bird, CloudKit, com.apple.icloud* |
– |
browser_cache |
Linux | /home/*/.cache/**, /root/.cache/** |
pip (goes to package_cache) |
– |
package_cache |
Linux | apt archives, dnf, yum, pip and npm caches |
apt/archives/lock, apt/archives/partial/** |
– |
package_cache |
macOS | Homebrew cache, npm _cacache, pip, NuGet *.nupkg |
– | – |
package_cache |
Windows | pip, npm-cache/_cacache, Chocolatey cache, NuGet *.nupkg, UWP AC/INetCache, AC/Temp, TempState |
– | – |
trash |
Windows | <vol>/$recycle.bin/* (each SID directory; only when the scan root is the volume root) |
– | – |
trash |
macOS | /users/*/.trash |
– | – |
trash |
Linux | /home/*/.local/share/Trash, /root/.local/share/Trash |
– | – |
Temp files younger than 24 hours are never candidates. Trash rules have contents granularity: the candidate is the bin directory, and execute deletes its children while the directory itself (and desktop.ini, which Explorer needs) survives.
Never candidates, whatever the snapshot says: Chrome, Edge and Firefox Bookmarks, History, Cookies, Login Data and places.sqlite (they are not under a cache pattern), and anything under a cleanup-denied root, which is rejected even when a rule matched: <vol>/windows/system32, <vol>/windows/winsxs, <vol>/program files, <vol>/program files (x86) on Windows; /system, /usr, /bin, /sbin, /etc, /private/var/db, /library/apple on macOS; /usr, /bin, /sbin, /etc on Linux.
Rules are enforced twice: at execute the API re-filters the pinned candidates through the shared table (so a snapshot captured by an older agent cannot be executed against), and the agent re-checks each path under cleanupGuard before deleting.
The scanner also reports old downloads, unrotated logs, duplicate candidates and the largest files. These are informational and are never auto-cleaned.
Preview
Section titled “Preview”The preview builds a plan from the volume’s latest snapshot without touching the device.
POST /api/v1/devices/:deviceId/filesystem/cleanup-previewContent-Type: application/json
{ "path": "D:\\", "categories": ["temp_files", "browser_cache"] }| Field | Type | Required | Description |
|---|---|---|---|
path |
string | No | Volume to preview. Defaults to the OS volume. Normalised server-side. |
categories |
array | No | Restrict to specific categories. Default: all four. Max 10 entries. |
The response creates a cleanup run with status previewed and pins both the snapshotId and the scanPath into the run’s plan; the cleanupRunId is what execute must present:
{ "success": true, "data": { "cleanupRunId": "run-uuid", "scanPath": "D:\\", "snapshotId": "snapshot-uuid", "estimatedBytes": 4300000000, "candidateCount": 156, "categories": [ { "category": "temp_files", "count": 89, "estimatedBytes": 1800000000 } ], "candidates": [ { "path": "D:\\Temp\\old_installer.exe", "category": "temp_files", "sizeBytes": 450000000, "safe": true, "reason": "Temporary file older than 24 hours", "modifiedAt": "2026-01-15T10:00:00.000Z" } ] }}Execute
Section titled “Execute”POST /api/v1/devices/:deviceId/filesystem/cleanup-executeContent-Type: application/json
{ "cleanupRunId": "run-uuid", "paths": ["D:\\Temp\\old_installer.exe", "D:\\$Recycle.Bin\\S-1-5-21-..."]}| Field | Type | Required | Description |
|---|---|---|---|
cleanupRunId |
UUID | Yes | The previewed run to execute. There is no “latest snapshot” fallback. |
paths |
array | Yes | Paths to delete. Min 1, max 200, max 4096 characters each. |
What execute guarantees:
- Nothing is deleted that was not previewed. A path outside the pinned plan is reported in
rejectedPaths(and as an action withstatus: "rejected") and is never dispatched. If every path is screened out, the request fails with400 No valid cleanup paths selected from latest previewable candidatesand the body still carriesrejectedPathsso you can see why. - Deletion is permanent. The agent is sent
permanent: trueandcleanupGuard: true. There is no trash move – that is what makes the bytes actually free (a trash move on the same volume frees nothing, and across volumes it copied files onto the OS drive). - Symlinks and reparse points are refused, at the target and among the children of a contents-granularity delete. The agent uses
Lstatthroughout and never follows a link. - Locked files are never forced. A Windows sharing violation reports
skipped_locked. - Bins are deleted by contents. A recycle-bin or trash candidate deletes its current children; the directory and
desktop.inisurvive. - A 4-minute budget bounds the request. Deletes are sequential with a 30-second command timeout each; paths not reached are
skipped_budget, the run is recordedexecutedwithpartial: true, and you re-run for the rest. - Run state is claimed. The run flips
previewed → runningfor the duration and lands asexecutedorfailed; a second execute against the same run answers409 run_not_previewedwith its current status.
Per-path status is one of completed, partial (something was removed but the target is not clear – some children failed, or were held back as links or as changed since the preview), failed, skipped_locked, rejected, skipped_budget. “Emptied most of the bin, three children failed” is partial, never completed. The response:
{ "success": true, "data": { "cleanupRunId": "run-uuid", "scanPath": "D:\\", "status": "executed", "bytesReclaimed": 4100000000, "selectedCount": 2, "failedCount": 0, "counts": { "completed": 2, "partial": 0, "failed": 0, "skipped_locked": 0, "rejected": 1, "skipped_budget": 0 }, "rejectedPaths": ["D:\\Projects\\notes.txt"], "partial": false, "budgetMs": 240000, "actions": [ { "path": "D:\\Temp\\old_installer.exe", "category": "temp_files", "sizeBytes": 450000000, "status": "completed", "bytesFreed": 450000000 } ] }}bytesFreed on an action is the agent’s sum of Lstat sizes actually removed – logical bytes. If every dispatched path fails the run is failed (HTTP 500 with the same body); if at least one completes it is executed.
Run history
Section titled “Run history”GET /devices/:id/filesystem/cleanup-runs?limit=&cursor=– paginated (default 20, max 100), newest first, both kinds (filesandsystem). An invalid cursor is400 invalid_cursorrather than silently page 1.GET /devices/:id/filesystem/cleanup-runs/:runId– one run with its plan, executed actions and error.
The tab’s Run history panel reads these. Retention is a background job: previewed runs are deleted after 7 days (an abandoned preview has no value), and the pinned plan.candidates list is trimmed from finished runs after 90 days while the summary, status, byte count and executed actions stay. Both windows are configurable (FILESYSTEM_CLEANUP_PREVIEW_RETENTION_DAYS, FILESYSTEM_CLEANUP_PLAN_RETENTION_DAYS).
OS-Native Cleaners
Section titled “OS-Native Cleaners”The file scanner can only reclaim what it can enumerate. A component store, Windows.old, an APFS local snapshot and a systemd journal are invisible to it – the operating system manages them through its own tools. Breeze runs those tools as a second cleanup engine on the same tab, in the System cleanup panel below the file cleanup.
The native engine has no itemised preview to check a selection against, so instead it is constrained by a closed catalog: a fixed list of vetted actions, every command line built from constants inside the agent, binaries by absolute path, no shell, timeouts and output caps on every process. The only client input is a list of action ids and one bounded integer.
The flow is two steps:
- List –
POST /devices/:id/filesystem/system-cleanup/listqueues asystem_cleanup_listcommand (202with acommandId); pollGET .../system-cleanup/list/:commandIduntilstatus: "completed"returns the device’scatalog: each action with its label, description, availability (andunavailableReason), an “up to” estimate or size unknown, its risk flags and the volumes it affects, plusvolumesBeforefree-space readings. Estimation runs with a 3-minute cap; an estimate that times out reportsestimateKnown: false. - Run –
POST /devices/:id/filesystem/system-cleanup/runwith{ actionIds, params? }records akind: "system"cleanup run and queuessystem_cleanup_run(202withcleanupRunIdandcommandId); pollGET .../system-cleanup/run/:cleanupRunId. Actions run sequentially in catalog order (cleanmgr and DISM must not overlap); one failing does not stop the next. Each action reportscompleted | failed | timed_out | unavailable | busy | not_started, its exit code, duration and output tail.
On an agent older than the minimum version both POSTs answer 409 agent_update_required with minAgentVersion; the panel shows an update banner and disables Run. A device runs one OS cleanup at a time – a second run while one is in flight is 409 run_in_progress with the running cleanupRunId, because Windows Disk Cleanup is driven by a shared on-disk selection profile and two overlapping runs would each execute whatever the other wrote last. Each run’s deadline is the sum of its selected actions’ timeouts plus 10 minutes of slack, capped at 3 hours; a run past its deadline is failed as run_expired and its command cancelled.
freedBytes on the result is measured: the agent samples free space per volume before and after the whole run and reports the delta (volumes[].freeBefore / freeAfter). That figure, not the estimate, is what the run achieved. Note that component-store cleanup (win_dism_component_cleanup) releases some of its space only after the next reboot, so those bytes are not in freedBytes.
Catalog
Section titled “Catalog”| Action id | Platform | What it runs | Timeout |
|---|---|---|---|
win_cleanmgr (or win_cleanmgr:<handler>) |
Windows | Sets StateFlags5555 on the selected built-in Disk Cleanup handlers and runs cleanmgr.exe /sagerun:5555. The bare id means “every allowlisted handler present on this device”; a :<slug> sub-id selects one. Allowlisted handlers: delivery_optimization_files, device_driver_packages, previous_installations, upgrade_discarded_files, windows_upgrade_log_files, setup_log_files, temporary_setup_files, service_pack_cleanup, system_error_memory_dump_files, system_error_minidump_files, windows_error_reporting_files, windows_error_reporting_system_archive_files, windows_error_reporting_system_queue_files, temporary_files, windows_defender, old_chkdsk_files, diagnostic_data_viewer_database_files, branchcache, content_indexer_cleaner. Handlers absent on the device’s Windows build are simply not offered. update_cleanup is no longer offered: cleanmgr’s Windows Update cleanup handler deadlocks under the SYSTEM service before it frees anything – use win_dism_component_cleanup, which performs the same component-store cleanup. Disk Cleanup itself never exits in session 0 (it waits on a progress window that cannot be shown), so the agent ends the run once the process tree has used no CPU for 5 minutes and reports it as completed; the 60-minute cap remains as a backstop. |
60 min |
win_dism_component_cleanup |
Windows | dism.exe /Online /Cleanup-Image /StartComponentCleanup – removes superseded components from WinSxS. Never /ResetBase, so installed updates stay removable. |
90 min |
mac_tm_local_snapshots |
macOS | tmutil listlocalsnapshots /, then tmutil deletelocalsnapshots <date> for each listed snapshot. Backups on the Time Machine destination are untouched. |
10 min |
mac_brew_cleanup |
macOS | brew cleanup --prune=all. Installed software stays at its current version. |
10 min |
linux_pkg_cache_clean |
Linux | apt-get clean / dnf clean all / yum clean all (first present). Removed archives are re-downloadable. |
5 min |
linux_pkg_autoremove |
Linux | apt-get -y autoremove / dnf -y autoremove. Uninstalls packages. |
15 min |
linux_journal_vacuum |
Linux | journalctl --vacuum-size=<bytes>. params.journalVacuumBytes sets the target: 64 MiB - 4 GiB, default 256 MiB. Archived journals only; the active journal is preserved. |
5 min |
Risk flags
Section titled “Risk flags”Every action declares zero or more risk flags. The panel renders them as badges, and the confirmation dialog names the specific loss rather than asking a generic “are you sure”:
| Flag | Carried by | What it means for the customer |
|---|---|---|
long_running |
win_cleanmgr, win_dism_component_cleanup |
Can hold the device for up to 60 or 90 minutes. |
may_require_reboot_free_state |
win_dism_component_cleanup |
Some of the space is released after the next restart. |
removes_driver_rollback |
win_cleanmgr:device_driver_packages |
Older driver packages are deleted; you can no longer roll a driver back to a previous version. |
removes_packages |
linux_pkg_autoremove |
Software is uninstalled (whatever the package manager considers orphaned). |
removes_os_rollback |
win_cleanmgr:previous_installations, win_cleanmgr:upgrade_discarded_files |
Deletes Windows.old / $WINDOWS.~BT / $WINDOWS.~WS – the path back to the previous Windows build. Irreversible. |
removes_recovery_points |
mac_tm_local_snapshots |
Deletes the only on-disk restore points a Mac has when its Time Machine destination is not attached. No in-place rollback remains. |
Selecting any action that carries removes_packages, removes_driver_rollback, removes_os_rollback or removes_recovery_points arms the destructive dialog’s second checkbox; the run button stays disabled until it is ticked.
What is deliberately excluded
Section titled “What is deliberately excluded”These are excluded in code, not configuration – the catalog is an as const list pinned by tests on both sides of the Go/TypeScript boundary, and an id outside it is rejected before anything is queued:
- DISM
/ResetBase(makes every installed update permanent). - The
DownloadsFolderhandler (user data). Windows ESD installation files(breaks Reset this PC).Language Pack(uninstalls installed languages).- Every per-user cleanmgr handler (
Recycle Bin,Thumbnail Cache,Temporary Internet Filesand the rest): under the SYSTEM service account they operate on the SYSTEM profile, and the file engine already covers user bins. - Docker prune, Storage Sense and
snaprevision pruning.
AI Integration
Section titled “AI Integration”The AI assistant has three tools. Tiers and rate limits are the ones enforced by the guardrails (see AI Assistant).
| Tool | Tier | What changed |
|---|---|---|
analyze_disk_usage |
1 / 2 with refresh: true |
Gains path (normalised server-side; default OS root). Without refresh it reads the latest stored snapshot for that path (devices.read) and never scans; with no stored snapshot it says so. refresh: true scans that path on the device, which needs devices.execute, like POST /devices/:id/filesystem/scan. |
disk_cleanup |
1 for preview / 3 for execute |
Gains path. preview returns a cleanupRunId, which the tool remembers; execute runs against that run, or against an explicit cleanupRunId – without either it answers cleanup_run_required. Same pinning, expiry, rejectedPaths and permanent-delete semantics as the route. |
system_cleanup |
2 for list / 1 for status / 3 for run |
New. Three actions, used in order. list asks the device for its catalog, so it needs devices.execute like the route, and is a read-only Tier 2 action (no prompt, audit-logged). It queues a catalog request and waits up to 60 seconds for the device; if the device has not answered by then it returns status: "pending" with a commandId, and calling list again with that commandId re-checks the same request instead of starting a new one. run requires actionIds from the shared allowlist (plus optional params.journalVacuumBytes), starts the run and returns immediately with a cleanupRunId, the commandId and the run’s stored deadlineAt – it never waits, because a run can take hours (DISM alone is capped at 90 minutes). status takes that cleanupRunId and returns the persisted state of the run – running, executed or failed – with freedBytes, per-action status and volume deltas once the agent has reported; it is the same code path as the web panel’s poll, including the lazy deadline timeout that finalises a run the device never answered. A run whose every action failed reports failed. run is supervised Tier 3: it always takes human approval. The per-tool rate limit is 30 calls per hour across all three actions; the single-run-per-device claim, not the rate limit, is what stops a second run starting while one is in flight. |
All three refuse an agent below the minimum version with agent_update_required and the required version.
Over MCP, a Tier 3 action is refused outright – there is no interactive approval surface on that transport – so an MCP client can diagnose with analyze_disk_usage, disk_cleanup preview, system_cleanup list and system_cleanup status, and a technician executes in the Breeze web app. A key with only ai:read reads stored results; a fresh scan (refresh: true) and system_cleanup list reach the device and need ai:write. See MCP Server.
Database Schema
Section titled “Database Schema”Filesystem Snapshots
Section titled “Filesystem Snapshots”Table: device_filesystem_snapshots
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key. |
device_id |
UUID | Foreign key to devices. |
org_id |
UUID | Foreign key to organizations. |
scan_path |
text, NOT NULL | The normalised volume or path this snapshot covers. One snapshot stream per volume. |
captured_at |
timestamp | When the snapshot was created. |
trigger |
enum | on_demand or threshold. |
partial |
boolean | Whether the scan was incomplete. |
summary, largest_files, largest_dirs, temp_accumulation, old_downloads, unrotated_logs, trash_usage, duplicate_candidates, cleanup_candidates, errors |
JSONB | The snapshot sections described above. |
raw_payload |
JSONB | Complete raw agent response. |
Indexed on (device_id, scan_path, captured_at DESC) for latest-snapshot-per-volume reads.
Cleanup Runs
Section titled “Cleanup Runs”Table: device_filesystem_cleanup_runs
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key. |
device_id |
UUID | Foreign key to devices. |
org_id |
UUID | Foreign key to organizations. |
scan_path |
text, nullable | The volume a files run covers. NULL for a system run, which cleans the machine rather than a path. |
kind |
text | files (the itemised file engine) or system (OS-native cleaners). Default files. |
command_id |
UUID, nullable | The queued system_cleanup_run command for kind = 'system' runs. |
requested_by |
UUID | Foreign key to users. |
requested_at |
timestamp | When the preview (or system run) was requested. |
approved_at |
timestamp | When execution was approved; null for preview-only runs. |
plan |
JSONB | Files: snapshotId, scanPath, categories, the preview. System: actionIds, params, deadlineAt. |
executed_actions |
JSONB | Per-path or per-action results. |
bytes_reclaimed |
bigint | Bytes reclaimed. |
status |
enum | previewed, running, executed, or failed. |
error |
text | Error if the run failed. |
Indexed on (device_id, requested_at) for history.
Scan State
Section titled “Scan State”Table: device_filesystem_scan_state
| Column | Type | Description |
|---|---|---|
device_id |
UUID | Foreign key to devices. First half of the primary key. |
scan_path |
text, NOT NULL | The normalised volume. Second half of the primary key – one checkpoint and baseline per volume. |
org_id |
UUID | Foreign key to organizations. |
scan_generation |
UUID | Command id owning the current scan; superseded or duplicate results are rejected. |
last_applied_command_id |
UUID | The last command whose results were applied. |
last_run_mode |
text | baseline or incremental. |
last_baseline_completed_at |
timestamp | When the last full baseline finished. |
last_disk_used_percent |
real | Usage at the last baseline. |
checkpoint |
JSONB | Pending directories for resume. |
aggregate |
JSONB | Accumulated partial results. |
hot_directories |
JSONB | High-churn directories for incremental scans. |
Preview expiry and the pinned run
Section titled “Preview expiry and the pinned run”Two properties of a pinned run a technician has to know before approving:
- A preview expires after 24 hours. An execute against an older run is refused with
409 preview_expired(the body carriesrequestedAtandttlHours), and the technician previews again. A day-old plan is a guess about a machine nobody has looked at since. - A bin is live. Pinning a recycle-bin or trash path pins the path, not its contents: execute deletes whatever is in the bin at the moment it runs, not the files the preview enumerated. The same is true of a temp file replaced between preview and execute. The confirmation dialog says so on those rows.
A run is single-use: once it has left previewed it cannot be executed again (409 run_not_previewed).
Agent version requirements
Section titled “Agent version requirements”- Permanent file cleanup requires an agent at
0.115.0(MIN_AGENT_VERSION_CLEANUP_GUARD) or newer. Execute sendspermanent: truewithcleanupGuard: true; an older agent has no guard and would perform an unguarded recursive permanent delete, so both the route and the AI tool refuse an older agent with409 agent_update_requiredbefore anything is dispatched. The body carriesminAgentVersionand the device’s currentagentVersion; the panel renders the required version in its error. - OS-native cleaners require
0.116.0(MIN_AGENT_VERSION_SYSTEM_CLEANUP) or newer and answer409 agent_update_requiredbelow it. They first shipped in0.115.0, but that agent’s Disk Cleanup (cleanmgr) run hangs until the 60-minute cap kills it, so0.115.xagents are refused until they update. The gate compares the core version only, so a release candidate of the gate release counts. As a defence in depth, an agent that reports a new-enough version but answersunknown command type:resolves to the same 409 on poll.
Both gates fail closed: an empty or unparseable agent_version is treated as too old.
How to read an estimate
Section titled “How to read an estimate”Every reclaim figure shown before a run is heuristic and an upper bound:
- The file engine’s candidate totals are the snapshot’s sizes at scan time; a locked file, a link, a changed bin or the 4-minute budget can all leave part of it in place.
- Native-cleaner estimates are labelled “up to X”, heuristic, or size unknown. “Up to” is a genuine ceiling such as a cache directory’s current size. Heuristic figures are reliable only to an order of magnitude: DISM’s
/AnalyzeComponentStorefields are component-store overhead, andStartComponentCleanupfrees an amount related to but not equal to them;journalctl --vacuum-sizeonly removes archived journals, so the estimate is usage minus target, floored at zero;autoremove‘s figure is the packages’ installed size, which over-counts shared files. Time Machine local snapshots report a count only – their byte size is unknown. Unknown is never rendered as 0, because a confident zero reads as “nothing to reclaim”, which is a different and possibly wrong claim.
Contrast freedBytes, which is measured from the free-space delta either side of the run. When the two disagree, the measured figure is the truth.
API Reference
Section titled “API Reference”All endpoints are prefixed with /api/v1/devices/:deviceId.
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/devices/:id/filesystem/volumes |
List the volumes this device can scan and clean | devices.read |
GET |
/devices/:id/filesystem?path= |
Latest snapshot for a volume (default: OS volume) | devices.read |
POST |
/devices/:id/filesystem/scan |
Trigger a scan of a volume or directory | devices.execute + MFA |
POST |
/devices/:id/filesystem/cleanup-preview |
Preview safe candidates for a volume; returns cleanupRunId |
devices.execute + MFA |
POST |
/devices/:id/filesystem/cleanup-execute |
Execute the pinned run on the selected paths | devices.execute + MFA |
GET |
/devices/:id/filesystem/cleanup-runs |
Paginated run history, both kinds | devices.read |
GET |
/devices/:id/filesystem/cleanup-runs/:runId |
One cleanup run | devices.read |
POST |
/devices/:id/filesystem/system-cleanup/list |
Queue a catalog request; returns a commandId |
devices.execute + MFA |
GET |
/devices/:id/filesystem/system-cleanup/list/:commandId |
Poll the catalog request | devices.read |
POST |
/devices/:id/filesystem/system-cleanup/run |
Run selected actions; returns a cleanupRunId, or 409 run_in_progress |
devices.execute + MFA |
GET |
/devices/:id/filesystem/system-cleanup/run/:cleanupRunId |
Poll the run | devices.read |
Audit actions: device.filesystem.cleanup.preview, device.filesystem.cleanup.execute, device.filesystem.system_cleanup.list, device.filesystem.system_cleanup.queue, device.filesystem.system_cleanup.run.
Troubleshooting
Section titled “Troubleshooting”“No filesystem analysis available yet” (404)
Section titled ““No filesystem analysis available yet” (404)”No snapshot exists for this volume. Run POST /devices/:id/filesystem/scan with the volume’s path and wait for it to complete. Scanning another volume does not help – snapshots are per scan path.
“No filesystem snapshot available. Run a scan first.” (404 on cleanup-preview)
Section titled ““No filesystem snapshot available. Run a scan first.” (404 on cleanup-preview)”The preview needs a snapshot of the requested volume to build candidates from. Scan that volume first.
Scan command returns 502 or times out
Section titled “Scan command returns 502 or times out”Large scans can exceed the command timeout. Increase timeoutSeconds (up to 900), reduce maxEntries or maxDepth, or scan a subdirectory. A timed-out baseline resumes from its checkpoint on the next attempt of the same volume.
Cleanup preview returns zero candidates
Section titled “Cleanup preview returns zero candidates”Only candidates that matched a rule in the shared table (safe: true) in one of the four categories are previewed. If the snapshot lists candidates but the preview is empty, they are informational items (old downloads, unrotated logs, duplicates), temp files younger than 24 hours, or under a cleanup-denied root. Also check that your categories filter matches what the snapshot contains.
“No valid cleanup paths selected from latest previewable candidates” (400)
Section titled ““No valid cleanup paths selected from latest previewable candidates” (400)”Every path in the request was screened out before dispatch. The body’s rejectedPaths and actions say which and why: not_in_plan (the path is not in the pinned run – copied from a different preview, hand-typed, or the wrong volume), rule_rejected (it no longer matches a cleanup rule) or denied_root. Preview again and submit paths from that response.
Execute returns run_not_previewed, preview_expired or “Cleanup run not found”
Section titled “Execute returns run_not_previewed, preview_expired or “Cleanup run not found””The pinned run was already consumed (it is single-use), is older than 24 hours, or belongs to another device. Preview again – the plan is cheap, the mistake is not.
Execute returns agent_update_required (409)
Section titled “Execute returns agent_update_required (409)”The device’s agent is older than 0.115.0 and lacks cleanupGuard; cleanup is refused rather than degraded, because an unguarded permanent delete is worse than no delete. The body carries minAgentVersion and the device’s agentVersion. Update the agent and retry.
Cleanup reported success but no space was freed
Section titled “Cleanup reported success but no space was freed”Check the run’s per-path results: skipped_locked and partial entries kept their bytes, and bytesReclaimed counts only what was removed. On a device whose agent predates the cleanupGuard gate, earlier releases moved files to the agent’s own trash directory instead of deleting them; current releases refuse such agents with 409 agent_update_required, so a “success” that freed nothing on a current API means the bytes are still held by locked or skipped paths. For OS-native cleaners, Windows Update cleanup and DISM release their space after a reboot, and freedBytes is measured at the moment the run ends.
A native cleaner reports unavailable
Section titled “A native cleaner reports unavailable”The catalog row’s unavailableReason says why: the binary is absent (cleanmgr.exe is not present on Server Core), or the agent’s sandbox denies writes to the cache path (/var/cache/apt, /var/log/journal) on a hardened systemd unit. busy means another maintenance operation (a concurrent run, a patch job’s Homebrew cleanup, DISM) held the agent’s process-wide lock – nothing was attempted and a retry is right; not_started means the run’s overall budget expired before that action’s turn, as opposed to timed_out, where the action itself ran and overran its own cap.
System cleanup returns run_in_progress (409)
Section titled “System cleanup returns run_in_progress (409)”One OS cleanup runs per device at a time. Wait for the running cleanupRunId to finish (poll it), or for it to pass its deadline, then retry.
Cleanup actions partially failed
Section titled “Cleanup actions partially failed”Per-path status explains it: the file was already gone, it is locked by a running process (skipped_locked), it is a link (rejected), or the 4-minute budget ran out (skipped_budget). The run is executed if at least one path completed and failed if every dispatched path failed; bytesReclaimed reflects only removed paths.
Incremental scan returns minimal results
Section titled “Incremental scan returns minimal results”Incremental scans traverse only the hot directories the previous baseline identified for that volume. If the disk is stable there is little to scan. Run a baseline scan for a full view.
Scan shows “partial: true” in the snapshot
Section titled “Scan shows “partial: true” in the snapshot”The scan was interrupted (agent restart, disconnect, timeout). The partial snapshot is valid for what was scanned; the next scan of the same volume resumes from the checkpoint and merges.
Permission denied errors in scan results
Section titled “Permission denied errors in scan results”Directories the agent cannot read are recorded in the snapshot’s errors array with the path and error type. Running the agent as root/SYSTEM reduces these but is not always necessary.