LazyTidy: a local Gemma folder tidier that can't delete or overwrite anything A developer released LazyTidy, a stdlib-only Python command-line tool that uses a local Gemma model via Ollama to suggest category folders and cleaned-up filenames for messy directories. Safety is enforced in plain Python rather than by the model: dry runs are the default, folders are whitelisted, names are sanitized, files are never overwritten or deleted, and an undo log allows reversal. The author notes the tool has not been run against a real Gemma instance, so the quality of suggested names remains untested. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady https://dev.to/challenges/hacktoberfest-week3-2026-10-19 LazyTidy is a command-line tool for the clutter you keep promising to sort "later": a Downloads folder full of IMG 001.JPG , scan 3 .pdf and notes.txt . Point it at a folder and a local open-weight model suggests a category folder and a clean name for every file. You look at the plan, add --apply , and walk away. It is lazy-friendly by design: one command, no per-file decisions. The twist is that the model is never trusted with the disk. A model that can rename and move your files can also lose them, so the rules are enforced by plain Python around it: dry run by default, a fixed list of allowed folders, sanitized names, never overwrite, never delete , and an undo command. Honest note: I have not run this on my own messy folder with a real model, so I have no "I saved N hours" number and won't invent one. What I can say is what the tests prove about safety, below. ollama pull gemma3 python3 lazytidy.py ~/Downloads dry run: prints the plan only python3 lazytidy.py ~/Downloads --apply moves the files python3 lazytidy.py ~/Downloads --undo puts everything back The dry run prints one line per file, in the shape scan 3 .pdf - invoices/acme-invoice-42.pdf . In my tests a scripted stand-in plays the model. I have not run it against a real Gemma instance, so how good the suggested names are is untested. Stdlib-only Python, one file plus tests. lazytidy.py : bash /usr/bin/env python3 """LazyTidy: sort and rename a messy folder while you do nothing. Fully local. A local open-weight model Gemma via Ollama suggests a category folder and a clean filename for each file. Plain Python makes it safe: dry-run by default, whitelisted folders, sanitized names, never overwrites, never deletes, and writes an undo log. Stdlib only. """ import json, re, sys, urllib.request from pathlib import Path OLLAMA, MODEL = "http://localhost:11434/api/generate", "gemma3" CATEGORIES = "documents", "images", "invoices", "notes", "code", "other" UNDO LOG = ".lazytidy-undo.json" def ask model name, snippet : prompt = f"Organize this file. Name: {name}\nStart of content: {snippet r}\n" f'Reply ONLY as JSON: {{"folder": one of {CATEGORIES}, ' '"new name": short descriptive filename WITHOUT extension, lowercase words separated by dashes}' body = json.dumps {"model": MODEL, "prompt": prompt, "stream": False, "format": "json"} .encode req = urllib.request.Request OLLAMA, body, {"Content-Type": "application/json"} with urllib.request.urlopen req, timeout=300 as r: return json.loads json.loads r.read "response" def clean stem s, fallback : if not isinstance s, str : return fallback s = re.sub r" ^a-z0-9 +", "-", str s .lower .strip "-" :60 return s or fallback def snippet of p, n=300 : try: return p.read text errors="ignore" :n except Exception: return "" def plan folder, ask=ask model : """Return list of src, dst . Never touches the disk.""" root, moves, taken = Path folder , , set for p in sorted root.iterdir : if not p.is file or p.name.startswith "." : continue try: s = ask p.name, snippet of p except Exception: s = {} cat = s.get "folder" if s.get "folder" in CATEGORIES else "other" stem = clean stem s.get "new name" , clean stem p.stem, "file" dst, i = root / cat / stem + p.suffix.lower , 1 while dst.exists or dst in taken: never overwrite i += 1; dst = root / cat / f"{stem}-{i}{p.suffix.lower }" taken.add dst ; moves.append p, dst return moves def apply moves, folder : done = for src, dst in moves: dst.parent.mkdir exist ok=True src.rename dst ; done.append str src , str dst move only, never delete Path folder / UNDO LOG .write text json.dumps done return done def undo folder : log = Path folder / UNDO LOG for src, dst in reversed json.loads log.read text : Path dst .rename src log.unlink def main : a = sys.argv 1: if not a: sys.exit "usage: lazytidy.py FOLDER --apply | --undo " folder = a 0 if "--undo" in a: undo folder ; print "undone" ; return moves = plan folder for s, d in moves: print f"{s.name} - {d.relative to folder }" if "--apply" in a: apply moves, folder ; print f"moved {len moves } files. undo: --undo" else: print "dry run only. add --apply to move files." if name == " main ": main test lazytidy.py passes, using a fake model : python import tempfile from pathlib import Path import lazytidy as t def make d : d/"IMG 001.JPG" .write text "jpgdata" ; d/"scan 3 .pdf" .write text "Invoice 42 total 10" d/"notes.txt" .write text "buy milk" ; d/"dupe.txt" .write text "another note" d/".hidden" .write text "skip me" ; d/"sub" .mkdir def fake name, snip : return {"IMG 001.JPG": {"folder":"images","new name":"Beach Day "}, "scan 3 .pdf": {"folder":"invoices","new name":"acme-invoice-42"}, "notes.txt": {"folder":"notes","new name":"shopping"}, "dupe.txt": {"folder":"notes","new name":"shopping"}} name with tempfile.TemporaryDirectory as tmp: d = Path tmp ; make d moves = t.plan d, ask=fake assert not d/"images" .exists plan is a dry run names = {s.name: str dst.relative to d for s, dst in moves} assert names "IMG 001.JPG" == "images/beach-day.jpg" sanitized, ext lowercased same target name for two files: both kept, one gets a -2 suffix dupe.txt sorts first assert {names "notes.txt" , names "dupe.txt" } == {"notes/shopping.txt", "notes/shopping-2.txt"} assert ".hidden" not in names and "sub" not in names t.apply moves, d assert d/"invoices"/"acme-invoice-42.pdf" .read text == "Invoice 42 total 10" assert not d/"notes.txt" .exists t.undo d assert d/"notes.txt" .read text == "buy milk" and d/"IMG 001.JPG" .exists assert not d/t.UNDO LOG .exists with tempfile.TemporaryDirectory as tmp: d = Path tmp ; make d evil = lambda n, s: {"folder": "../../etc", "new name": "../../../passwd"} for s, dst in t.plan d, ask=evil : assert dst.parent == d/"other", dst bad folder - 'other' assert ".." not in dst.name and "/" not in dst.name def boom n, s : raise RuntimeError "model down" assert all dst.parent == d/"other" for s, dst in t.plan d, ask=boom model failure is safe model down: original names are kept sanitized , not replaced by junk like "none" assert sorted dst.name for s, dst in t.plan d, ask=boom == "dupe.txt", "img-001.jpg", "notes.txt", "scan-3.pdf" pre-existing destination is not overwritten d/"other" .mkdir ; d/"other"/"notes.txt" .write text "KEEP" t.apply t.plan d, ask=boom , d assert d/"other"/"notes.txt" .read text == "KEEP" print "all tests passed" format: "json" . It sees only the file name and the first 300 characters of text, and it only ../../etc , becomes other ; names are reduced to lowercase letters, digits and dashes; collisions get a -2 suffix; hidden files and sub-folders are skipped; moves are renames only, with an undo log written. other/ with their original sanitized names. While testing the "model down" case I found my first version renamed everything to none.txt , because a missing name was turned into the text "none". I fixed it and added a test for it. Known limits: it reads text files only images and PDFs are classified by name, not content , the undo log is overwritten by a second --apply , and undo does not remove the category folders it created. Those are the first things I would fix. A file tidier is exactly the kind of tool that sees everything: invoices, contracts, private notes. I would not hand the contents of my Downloads folder to a server I don't control just to rename files. With an open-weight model running locally, the first 300 characters of each file never leave the machine, the tool works offline, and it costs nothing per run, which matters for something you would run weekly. The model being swappable also means a small one is good enough here, because the safety comes from the code around it, not from trusting the model. Not included. I can't claim a measured time saving, since I have not used it on a real folder yet. What I can claim is the part I tested: it will not overwrite a file, will not delete one, and can be undone. Best Use of Gemma : Gemma, served locally via Ollama, makes the naming and sorting suggestions at the core of the project.