Skip to content

Safety Model

Alex Coulombe edited this page Aug 9, 2026 · 3 revisions

Safety Model

The rules here are asserted in code at the point of deletion (custodian/policy.py), not just documented — an intention that isn't checked is not a safety property. tests/test_policy.py, tests/test_engines.py, and tests/test_safedelete.py exist specifically to hold that line; 43 tests pass on both macOS and Windows.

Disk pressure triggers; age ranks

A plain "14 days untouched, delete it" rule eventually eats the project you were about to reopen, and the cost of being wrong is asymmetric — a C++ project is a 10–20 minute rebuild plus a shader recompile, not a 10-second one.

By default, clean does nothing until the volume drops below 100 GB free (min_free_gb). Once it's under pressure, the largest eligible projects are reclaimed first, until the volume is back above the line. Unreal cache sizes are strongly barbell-distributed — on the macOS test machine, two projects were 26% of the total reclaimable space — so largest-first usually means touching very few projects.

Set min_free_gb to 0, or pass --ignore-pressure, to make age the sole trigger.

Freshness is measured twice, because mtimes lie

Copying a project, restoring it from backup, or syncing it through cloud storage rewrites every file's mtime, making a long-dead project look like it was touched this morning. So alongside the filesystem mtime of Content/Source/Config, the tool reads Unreal's rotated editor log filenames, which encode their own timestamp and therefore survive a copy intact. When the two disagree by more than a month, the report flags it.

What is never deleted

Content, Source, Config, the Plugins directory itself, and Saved/SaveGames are on a hard-coded NEVER_DELETE set, checked on every resolved path regardless of what a .ueclean.json says.

  • Saved/Autosaves is removable, but gated at 90 days regardless of other settings — after an editor crash it's sometimes the only copy of an hour's work.
  • Saved/Config is opt-in — losing it costs you your editor layout, not your work.
  • Binaries is kept by default on C++ projects (real rebuild cost); Blueprint-only Binaries regenerates on open and is reclaimed.

Symlinked plugins are skipped, not followed

A Plugins/Foo directory that's a symlink into a different checkout is common (shared plugin repos). Deleting through it would reclaim space from a repository the user never named — this was caught in testing (Plugins/ECABridge symlinked to a separate repo). It's skipped with a note; the rest of the project is still processed. One bad target never aborts the whole plan.

Engine installs: the rule that protects 55+ GB installs

A launcher/binary install's Engine/Binaries is the engine — shipped precompiled, nothing on your machine can rebuild it. Deleting it turns a 55 GB install into a multi-gigabyte re-download.

A source-built engine's Engine/Binaries and Engine/Intermediate are genuine build output — and can be enormous (90 GB Intermediate + 57 GB Binaries measured on one machine).

Told apart by the marker UnrealBuildTool itself leaves: Engine/Build/InstalledBuild.txt vs Engine/Build/SourceDistribution.txt. An engine with neither marker is treated as installed — guessing wrong in the safe direction costs nothing; guessing wrong the other way destroys an install.

The expensive engine targets are off by default even on source builds, and are two independent opt-ins because they carry different risk:

  • Engine/Intermediate (--engine-intermediate) — purely the incremental-compile cache. Reclaiming tens of GB of it has no effect on whether the editor launches; only the next engine compile becomes a full rebuild instead of incremental.
  • Engine/Binaries (--engine-binaries) — the compiled editor executable itself. Reclaim it on a source build and the editor will not launch again until the engine is fully rebuilt.

Writability is checked up front, not discovered mid-plan

Projects copied between machines or accounts are routinely owned by another user. os.access() is unreliable for this on Windows (it ignores ACLs), so writability is verified by actually creating and removing a probe file. Unwritable directories are reported with the reason before the plan runs, not failed one move at a time after it starts.

Trash by default; --permanent on request

--apply moves reclaimed directories to the Trash / Recycle Bin, so a policy you disagree with costs a drag-and-drop, not a rebuild. The catch: on the same volume, this frees no disk space until the bin is emptied — which defeats the point if you're cleaning because you're out of room. --permanent deletes outright. The tool states which one it did, every time.

Naming in the Trash is collision-proof, not timestamp-based. Every Unreal project reuses the same generic folder names — DerivedDataCache, Intermediate, Binaries — so a real run cleaning several projects moves several identically-named directories into the Trash. An earlier version disambiguated with a second-resolution timestamp; cleaning two projects' DerivedDataCache within the same wall-clock second produced the same stamped name for both, and the second move failed outright with "already exists" (found live, 2026-08-09, on a real cleanup run). It's now <project>-<name> first — which also makes the Trash readable afterward, since you can tell whose DerivedDataCache it was — with a counter as the guaranteed-unique fallback for anything that still collides.

The live-editor guard

Before deleting, each project is checked against the running process list for an actual Unreal editor binary (not its long-lived helpers — UnrealEditorServices, UnrealTraceServer, zenserver all run continuously and hold no project open; matching on the bare substring "unreal" produced false positives against the tool's own shell in testing). If the process list can't be read at all, the guard fails closed and blocks.