Uninstall an app on Windows and the uninstaller tells you it was "successfully removed". Then look in AppData.
Most uninstallers remove the program folder and leave behind everything the app created while you used it: data and caches in Roaming and Local, update downloads, dot-folders in your profile, Start-menu shortcuts, registry keys and start-up entries. Electron apps and anything with a local cache can easily leave gigabytes. In the demo below, the app's uninstaller removed 226 MB and left 1.7 GB behind.
So I built Untrail: you drag an app's icon into it, it runs the app's own uninstaller, then it finds what's left and removes what you approve.
Finding leftovers turned out to be the easy part. Most of the work went into not deleting the wrong thing. This post is about the decisions that made that possible, and I'd love your critique of them: my specific questions are at the end.
The naive approach is dangerous
The obvious algorithm is: search the usual places for folders named after the app, then delete them. It's also how you delete someone's files.
- Name collisions. An app called "Paint Pro" and a folder called
Paint Profrom a different vendor's product. - Common words. An app named "Notes" or "Sync" matches half your disk.
- Old folders. A
ZephyrPaintProfolder from a version you uninstalled three years ago, which you may have kept on purpose. - Shared publisher folders.
Program Files\SomeVendorcan hold several of that vendor's apps. - Your own files.
Documents\AppNameis often where you saved things. - Links. A junction inside an app folder can point anywhere, including your photos.
So the first rule I wrote down was: when in doubt, leave it. Missing a leftover is a mild annoyance. Deleting something that wasn't the app's is unforgivable.
A name match is only a hint
In Untrail, matching the name only makes a folder a candidate. To be marked Safe to remove and ticked by default, it also needs positive evidence that it belongs to the app. The evidence it accepts:
- It holds the app's own program files: an exe or DLL whose product or company name matches the app.
- Its text files (JSON, INI, XML, logs and so on) mention the app's install folder.
- The app's own registry settings point to it.
- It's the app's Electron data folder: the app ships an
app.asar, and the folder holdsLocal StateorPreferences. - It sits inside the publisher's own folder.
And some signals count against it, even with a matching name:
- It hasn't been touched since before the app was installed (with a two-day margin).
- Its name is a common word.
- It can't be read. A failed check is a doubt, never a pass.
Anything without evidence is still listed, with the reason, but marked Review and left unticked. Anything in Documents or Saved Games is marked Keep.
Here's the core of it. Note the catch: if anything goes wrong while checking, the folder isn't selected.
/// A matching name is only a hint. To be "Safe to remove", a folder also needs positive evidence that it is /// this app's, and must not look abandoned since before the install. Any failure to check is a doubt. static string OwnershipDoubt(Ctx c, string dir, bool inPublisherFolder, out string evidence) { evidence = null; try { if (Util.IsReparse(dir)) return "It's a link to another folder — check where it points"; if (c.InstalledAt.HasValue) { DateTime last = Directory.GetLastWriteTime(dir); foreach (FileSystemInfo f in new DirectoryInfo(dir).EnumerateFileSystemInfos().Take(300)) if (f.LastWriteTime > last) last = f.LastWriteTime; if (last < c.InstalledAt.Value.AddDays(-2)) return "Same name as the app, but untouched since before it was installed"; } evidence = inPublisherFolder ? "inside the publisher's folder" : Evidence(c, dir); return evidence == null ? "Same name as the app, but nothing in it links it to the app" : null; } catch { evidence = null; return "Couldn't check what's inside, so it isn't selected"; } } Every item in the list says why it was found, so you can make the call yourself:
Read-only scans of real apps on my own machine were reassuring in both directions. Large, legitimate data folders (11.9 GB for one code editor, 24.8 GB for Docker) were still marked Safe because the evidence was there, while same-named folders without evidence dropped to Review.
Uninstallers lie about being done
Untrail runs the app's own uninstaller first, because it knows things a scanner can't. But when has it finished?
My first version simply asked you to confirm when the uninstaller had finished. Automating that is harder than it looks, because waiting for the uninstaller's process to exit is wrong for a lot of apps. Many uninstallers (NSIS, Inno Setup, Squirrel) copy themselves to Temp, start the copy and exit immediately, so the process you launched exits within a second with code 0 while the real uninstall hasn't started. MSI runs inside msiexec. Steam hands the job to the already-running Steam client.
So Untrail:
- follows the uninstaller's whole process tree using toolhelp snapshots, not just the process it started;
- treats the exit code as meaningful only when no child processes were involved (and accepts MSI's 3010 and 1641, "success, restart needed");
- judges the result by the app itself: it's only Completed when the app's Installed-apps entry and its program are both gone.
There are four outcomes: Running, Completed, Partial (finished, but something is still installed) and NoChange (probably cancelled). Untrail only removes leftovers after Completed. If the uninstall was cancelled or failed, it deletes nothing, unless you explicitly choose Clean up anyway, with a warning.
Time of check vs. time of use
A scan can take a while, and you might review the list for longer. Anything can change between the scan and the delete. So every delete is checked again right before it happens:
- A fixed allowlist. Registry deletion is limited to the app's own
Software\...keys, its Installed-apps entry, its App Paths entry and itsRun/RunOncevalues. Files must pass a list of protected paths: never a drive root,Windows,Program Filesitself,AppDataitself,Documentsand so on. - The app's identity. The item must still belong to the app being removed. Folders that hold another installed app are refused outright, even when they share a publisher.
- The same path as the scan. At scan time, Untrail records each item's real path with
GetFinalPathNameByHandle. If the path now resolves somewhere else (a folder swapped for a junction, for example), the delete is refused.
Links are never followed. For junctions and symbolic links, only the link itself is removed, never its target. If a folder being deleted contains a link, the link is removed and the walk doesn't descend into it.
Two more rules:
- No backup, no delete. Registry entries are exported to
Documents\Untrail Backupsfirst. If the export fails, the entry stays. - Admin only when needed. Most leftovers live in your user profile and need no admin rights. For
Program Files,ProgramDataor machine-wide registry keys, Untrail starts an elevated helper and hands it the job over a named pipe. The helper checks it's talking to the Untrail window that started it (via the pipe's client process ID) and applies all the checks above again itself. It doesn't trust the window it's working for.
Measuring honestly
My first results screen showed the total from the scan, not what had actually happened. But some items can fail to delete, some go to the Recycle Bin (which still uses space), and some files are in use.
Now the results are measured, per drive:
- Uninstaller removed: measured before and after the app's own uninstaller ran.
- Untrail deleted: what Untrail permanently deleted afterwards, with nothing counted twice.
- Recycle Bin, At restart (files in use, which Windows removes on the next restart) and Not removed, each listed separately.
- Free space change: the drive's free space before and after. It's the real number, though other programs can affect it.
After cleaning, Untrail scans again. It only says "No additional leftovers detected in scanned locations" when that scan finished and found nothing. It never claims nothing is left.
Testing a program that deletes things
You can't unit-test your way to confidence here. The integration suite installs throwaway fake apps and runs real deletes against them, on a clean Windows machine in GitHub Actions on every push. The fake apps are designed to trick it:
- a decoy with a colliding name, an old same-named folder dated before the install, and a folder it isn't allowed to read;
- junctions and symbolic links, both inside a leftover and as the leftover itself;
- an NSIS-style uninstaller that hands off to a hidden copy of itself, one that gets cancelled, and one that only partly uninstalls;
- a registry backup that fails, and a file locked by a running process;
- a decoy app from the same publisher, sharing its folder.
One of the checks is accounting: what the uninstaller removed plus what Untrail deleted must equal everything that was found, to the byte. In total it's 86 checks, including the admin-only scenarios that need an elevated runner. The suite also refuses to start if any of its fixture paths already exist, so it can never run against something real by accident.
Built with nothing but Windows
A small detail that people seem to like: Untrail is plain C# for .NET Framework 4.8, which ships with Windows 10 and 11, and it's compiled with the csc.exe that's already in C:\Windows\Microsoft.NET\Framework64\v4.0.30319\. A PowerShell script builds the app and its installer; no SDK, no Visual Studio. The installer is per-user, so it needs no admin rights either.
The trade-off is C# 5 syntax (no string interpolation, no ?.), which honestly didn't hurt much.
Where I'd love your feedback
- What other evidence would you accept that a folder belongs to an app? Can you think of a case where the five signals above would give a false positive?
- Is "untouched since two days before the install" a sensible cut-off for old folders, or would you pick something else?
- Which apps have uninstallers that behave strangely: hand-offs, reboots, services left running? I'd like to add them to the test suite.
- Should it default to the Recycle Bin (as it does now) or delete permanently?
Answers in the comments are hugely appreciated, especially the ones that prove me wrong.
Try it, and tell me what it misses
Untrail is free and MIT licensed:
https://github.com/usmanmateen/untrail
It isn't code-signed yet, so Windows SmartScreen will warn the first time you run it. Each release includes SHA-256 checksums, and from the next release the builds also carry signed build provenance you can check with gh attestation verify.
The most useful thing you can send me is a leftover report: an app where Untrail missed something, or matched something that wasn't the app's. Run Untrail.exe --scan "<shortcut>" --hide-user --out report.txt (it replaces your user folder with %USERPROFILE%) and attach it to an issue. That's how the matcher gets better without ever getting braver than the evidence allows.




