<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vortrix5</title>
    <description>The latest articles on DEV Community by Vortrix5 (@vortrix5).</description>
    <link>https://dev.to/vortrix5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981714%2Fc8c90ae1-742f-44bf-a967-d831b0a25de6.gif</url>
      <title>DEV Community: Vortrix5</title>
      <link>https://dev.to/vortrix5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vortrix5"/>
    <language>en</language>
    <item>
      <title>How I made deleting files hard to get wrong: building Sifty, a safety-first Windows cleaner</title>
      <dc:creator>Vortrix5</dc:creator>
      <pubDate>Fri, 12 Jun 2026 17:48:25 +0000</pubDate>
      <link>https://dev.to/vortrix5/how-i-made-deleting-files-hard-to-get-wrong-building-sifty-a-safety-first-windows-cleaner-544m</link>
      <guid>https://dev.to/vortrix5/how-i-made-deleting-files-hard-to-get-wrong-building-sifty-a-safety-first-windows-cleaner-544m</guid>
      <description>&lt;h2&gt;
  
  
  How I made deleting files hard to get wrong: building Sifty, a safety-first Windows cleaner
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A free, open-source Windows maintenance tool for the terminal — and the design decisions behind trusting a program to delete your files.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I don't trust cleanup tools. That's an awkward thing to admit right before telling you I built one, but it's also the entire reason I built it.&lt;/p&gt;

&lt;p&gt;The category has a reputation problem. The most famous Windows cleaner shipped a bundled cryptominer in one version and got compromised into a supply-chain attack in another. Most of them phone home, upsell you a "Pro" tier to fix problems they invented, and — the part that actually scares me — delete files &lt;em&gt;permanently&lt;/em&gt;. You click "Clean," a progress bar fills, and whatever it decided was junk is gone. No Recycle Bin. No undo. You're trusting a closed-source binary's judgment with no take-backs.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;strong&gt;Sifty&lt;/strong&gt;: a Windows 10/11 maintenance tool that runs in the terminal, is MIT-licensed, has zero telemetry, and is built from the ground up so that &lt;em&gt;deleting the wrong thing is hard to do even on purpose&lt;/em&gt;. This post is about that last part — the design, and the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Quickly, so the rest makes sense. Sifty is a scriptable CLI plus a full-screen TUI that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cleans junk and caches (temp files, browser caches, crash dumps, update leftovers — 11+ categories)&lt;/li&gt;
&lt;li&gt;finds duplicate files (SHA-256, and NTFS-aware so hardlinks aren't double-counted) and your biggest space hogs&lt;/li&gt;
&lt;li&gt;manages installed apps, startup items, services, and updates (via &lt;code&gt;winget&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;purges the clutter dev machines accumulate — &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;__pycache__&lt;/code&gt;, orphaned git worktrees, bloated WSL2 virtual disks&lt;/li&gt;
&lt;li&gt;has an optional AI assistant that runs &lt;strong&gt;locally&lt;/strong&gt; via Ollama&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j4hrqevp838wozqrxci.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j4hrqevp838wozqrxci.gif" alt="Sifty demo" width="720" height="528"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pipx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checkup&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="c"&gt;# one read-only scan of everything&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;junk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c"&gt;# preview what it would remove (dry-run)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;junk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--apply&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# actually do it (asks first)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the feature list isn't the interesting part. The interesting part is the constraint I put on myself: &lt;strong&gt;a tool that deletes files has no margin for "oops."&lt;/strong&gt; Here's how that constraint shaped the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 1: There is exactly one way to delete something
&lt;/h2&gt;

&lt;p&gt;The single most important design rule in the whole codebase is this: &lt;strong&gt;nothing in Sifty calls &lt;code&gt;os.remove&lt;/code&gt;, &lt;code&gt;os.unlink&lt;/code&gt;, &lt;code&gt;shutil.rmtree&lt;/code&gt;, or &lt;code&gt;Path.unlink&lt;/code&gt;. Ever.&lt;/strong&gt; Every deletion in the entire application funnels through one function, &lt;code&gt;safety.trash()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_subtrees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;extra_protected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send `path` to the Recycle Bin after a safety check.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;assert_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_subtrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra_protected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="nf"&gt;send_to_trash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# Send2Trash -&amp;gt; Recycle Bin, never permanent
&lt;/span&gt;    &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRASH &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are load-bearing here, and they're all in those few lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It routes to the Recycle Bin, not oblivion.&lt;/strong&gt; &lt;code&gt;send_to_trash&lt;/code&gt; is the project's one and only call to the &lt;a href="https://pypi.org/project/Send2Trash/" rel="noopener noreferrer"&gt;Send2Trash&lt;/a&gt; library. If Sifty makes a mistake, the file is sitting in your Recycle Bin where you can drag it back. Permanent deletion is not a code path that exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;dry_run=True&lt;/code&gt; is the &lt;em&gt;default value of the parameter&lt;/em&gt;.&lt;/strong&gt; Not a flag you remember to pass — the safe behavior is what you get if you do nothing. To actually delete, a caller has to explicitly opt out of safety. This inverts the usual danger: forgetting an argument makes Sifty &lt;em&gt;more&lt;/em&gt; cautious, not less.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every real deletion is audited.&lt;/strong&gt; &lt;code&gt;audit()&lt;/code&gt; appends a timestamped line to &lt;code&gt;%APPDATA%\sifty\audit.log&lt;/code&gt;. If you ever wonder "what did this thing touch," there's a paper trail.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because this is the &lt;em&gt;only&lt;/em&gt; delete path, I can make one airtight guarantee about the whole program by reasoning about one function. And I enforce it the dumb, reliable way — a test greps the source tree and fails CI if &lt;code&gt;os.remove&lt;/code&gt;/&lt;code&gt;rmtree&lt;/code&gt;/&lt;code&gt;unlink&lt;/code&gt; shows up anywhere outside this file. You can't accidentally reintroduce a raw delete in a feature PR; the build goes red.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 2: Some paths are refused no matter what
&lt;/h2&gt;

&lt;p&gt;The Recycle Bin saves you from &lt;em&gt;permanent&lt;/em&gt; loss, but sending &lt;code&gt;C:\Windows&lt;/code&gt; to the Recycle Bin still bricks your machine. So before anything gets trashed, it goes through &lt;code&gt;assert_safe()&lt;/code&gt;, which asks &lt;code&gt;is_protected()&lt;/code&gt;. This is where the actual judgment lives, and it's a two-tier model that took me a few iterations to get right.&lt;/p&gt;

&lt;p&gt;The naive approach — "refuse a hardcoded list of system folders" — has a subtle bug. If you protect &lt;code&gt;C:\Windows&lt;/code&gt; but the user aims a delete at &lt;code&gt;C:\&lt;/code&gt;, you've just authorized deleting the &lt;em&gt;parent&lt;/em&gt;, which takes Windows with it. And if you protect &lt;code&gt;C:\&lt;/code&gt; itself by refusing everything under it, you've made the user's entire disk undeletable, which makes the cleaner useless.&lt;/p&gt;

&lt;p&gt;The fix was to split protected roots into two kinds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_subtrees&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;extra_protected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;_norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allow_subtrees&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;contents_protected_roots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extra_protected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Deleting the root itself - OR AN ANCESTOR of it - is always refused.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_is_relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="c1"&gt;# Deleting something *inside* it is refused unless a caller vouched.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_is_relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_is_relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;self_protected_roots&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Only the root itself (or an ancestor) is off-limits; contents are OK.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_is_relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Contents-protected roots&lt;/strong&gt; — &lt;code&gt;C:\Windows&lt;/code&gt;, the &lt;code&gt;Program Files&lt;/code&gt; trees, &lt;code&gt;ProgramData&lt;/code&gt; — refuse the root &lt;em&gt;and everything inside it&lt;/em&gt;. You can't touch them or their contents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-protected roots&lt;/strong&gt; — the drive root &lt;code&gt;C:\&lt;/code&gt; and your user profile &lt;code&gt;C:\Users\you&lt;/code&gt; — refuse only the root &lt;em&gt;itself&lt;/em&gt;. Ordinary files inside stay deletable (otherwise the tool couldn't clean your Downloads), but the root can't be nuked wholesale.&lt;/p&gt;

&lt;p&gt;And notice the &lt;code&gt;_is_relative_to(root, target)&lt;/code&gt; check in &lt;em&gt;both&lt;/em&gt; loops: that's the ancestor guard. Aim at &lt;code&gt;C:\&lt;/code&gt; and the check sees that a protected root (&lt;code&gt;C:\Windows&lt;/code&gt;) lives underneath your target, so it refuses — closing the "delete the parent" hole.&lt;/p&gt;

&lt;p&gt;The key safety property: &lt;strong&gt;these checks fire even with &lt;code&gt;--apply --yes&lt;/code&gt;.&lt;/strong&gt; There is no override flag, no &lt;code&gt;--force&lt;/code&gt;, no "I really mean it." A protected path is simply not deletable by Sifty, full stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  But cleaners &lt;em&gt;need&lt;/em&gt; to touch system folders
&lt;/h3&gt;

&lt;p&gt;Here's the tension: the whole point of a cleaner is to clear &lt;code&gt;C:\Windows\Temp&lt;/code&gt;, which lives &lt;em&gt;inside&lt;/em&gt; a contents-protected root. A blanket refusal would block the feature.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;allow_subtrees&lt;/code&gt; is for. It's a per-call carve-out where a specific module &lt;strong&gt;vouches&lt;/strong&gt; for a specific subtree. The junk-cleaning module passes &lt;code&gt;allow_subtrees=[r"C:\Windows\Temp"]&lt;/code&gt;, and only then is that one folder permitted — while the rest of &lt;code&gt;C:\Windows&lt;/code&gt; stays locked. The permission is narrow, explicit, and lives at the call site where a human decided it was OK, not buried in a global allowlist. Default-deny, with auditable exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 3: The AI is advisory, local, and blind to your file contents
&lt;/h2&gt;

&lt;p&gt;Plenty of "AI-powered" tools mean "we ship your data to a cloud model." I wanted the opposite, on every axis.&lt;/p&gt;

&lt;p&gt;Sifty's optional assistant runs on &lt;strong&gt;&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/strong&gt; — a local model on your own machine. Nothing leaves your computer. But "local" wasn't enough on its own; I gave it three hard limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only ever sees metadata.&lt;/strong&gt; The advisor builds prompts from file &lt;em&gt;names, sizes, and paths&lt;/em&gt; — never file &lt;em&gt;contents&lt;/em&gt;. The model can reason that a 40 GB folder of &lt;code&gt;.mp4&lt;/code&gt; files in Downloads is probably reclaimable; it cannot read your documents, because they're never in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It cannot delete anything.&lt;/strong&gt; The AI has no access to &lt;code&gt;trash()&lt;/code&gt;. It's advisory: it explains and recommends, and that's the end of its authority.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every action it proposes needs your click.&lt;/strong&gt; It's agentic — it can &lt;em&gt;propose&lt;/em&gt; running a scan or a cleanup — but each proposed tool call shows up as &lt;strong&gt;Run / Skip buttons inline in the conversation&lt;/strong&gt;. The AI suggests; you decide. It never acts on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model I kept: the AI is a knowledgeable advisor sitting next to you, not a hand on the keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a library with a CLI on top
&lt;/h2&gt;

&lt;p&gt;One more decision that pays off for safety &lt;em&gt;and&lt;/em&gt; contributors: Sifty is layered, and the layers point one direction only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cli/  tui/        &amp;lt;- thin frontends (Typer, Textual)
        |
       core/      &amp;lt;- the engine: junk, disk, apps, safety, ...  (no UI code)
        |
   windows/  infra/   &amp;lt;- OS primitives (winget, Recycle Bin, UAC) + config/logging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontends call &lt;code&gt;core&lt;/code&gt;; &lt;code&gt;core&lt;/code&gt; calls &lt;code&gt;windows&lt;/code&gt;/&lt;code&gt;infra&lt;/code&gt;; nothing imports upward, and OS-specific calls are quarantined in &lt;code&gt;windows/&lt;/code&gt;. The CLI and TUI are deliberately dumb — they parse arguments and print results. All the logic that &lt;em&gt;matters&lt;/em&gt; lives in plain, testable functions like &lt;code&gt;junk.scan()&lt;/code&gt; and &lt;code&gt;disk.find_duplicates()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two payoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The safety guarantees are testable in isolation.&lt;/strong&gt; The tests for protected paths don't go anywhere near a terminal. They &lt;code&gt;monkeypatch&lt;/code&gt; the environment (&lt;code&gt;SystemRoot&lt;/code&gt;, &lt;code&gt;ProgramFiles&lt;/code&gt;, &lt;code&gt;Path.home&lt;/code&gt;) and point everything at a &lt;code&gt;tmp_path&lt;/code&gt; sandbox, so the protected-path logic runs deterministically — and the suite passes on CI's Linux runners even though Sifty is a Windows tool. The safety layer is the most heavily tested code in the repo by a wide margin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A future GUI is a frontend swap, not a rewrite.&lt;/strong&gt; It would call the same &lt;code&gt;core&lt;/code&gt; functions, inheriting the same single delete path and the same protections for free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building something that deletes
&lt;/h2&gt;

&lt;p&gt;If there's one transferable lesson, it's this: &lt;strong&gt;make the safe thing the default and the dangerous thing loud.&lt;/strong&gt; Concretely, the moves that worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One choke point.&lt;/strong&gt; Funnel every destructive action through a single function, then enforce it mechanically (a test that greps for the raw calls). One function to audit beats one hundred call sites to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversibility over correctness.&lt;/strong&gt; I will never write a perfect "is this junk?" classifier. So I didn't try — I made the &lt;em&gt;cost of being wrong&lt;/em&gt; small. Recycle Bin, audit log, &lt;code&gt;sifty undo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default to the cautious behavior.&lt;/strong&gt; Dry-run as the parameter default means a forgotten argument fails &lt;em&gt;safe&lt;/em&gt;. Safety you have to remember to turn on is safety you'll eventually forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No override for the truly dangerous stuff.&lt;/strong&gt; Protected paths have no &lt;code&gt;--force&lt;/code&gt;. A door that can always be forced open isn't a lock.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it / tear it apart
&lt;/h2&gt;

&lt;p&gt;Sifty is on PyPI and MIT-licensed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pipx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c"&gt;# or: pip install sifty&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;sifty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checkup&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c"&gt;# read-only - see what it'd find, deletes nothing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a scoop bucket and a standalone &lt;code&gt;sifty.exe&lt;/code&gt; on the &lt;a href="https://github.com/Vortrix5/sifty/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt; if you'd rather not install Python.&lt;/p&gt;

&lt;p&gt;The repo is &lt;strong&gt;&lt;a href="https://github.com/Vortrix5/sifty" rel="noopener noreferrer"&gt;github.com/Vortrix5/sifty&lt;/a&gt;&lt;/strong&gt;, and I'd genuinely love eyes on the safety model — especially anyone who can think of a way to make it delete something it shouldn't (there's a &lt;a href="https://github.com/Vortrix5/sifty/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; for that). It's open to contributors, the test suite is fast and cross-platform, and &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; will get you running in two commands.&lt;/p&gt;

&lt;p&gt;Break it before your users do. ⭐ if it's useful.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>microsoft</category>
      <category>cleaner</category>
    </item>
  </channel>
</rss>
