This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
My Downloads folder is where files go to be forgotten. I wanted the lazy fix: point a tool at the folder, glance at a plan, say yes, done.
TidyMind is a small Python CLI that organizes a folder with a local open-weight model (qwen3:4b through Ollama). The model only proposes a category for each file (Documents, Images, Archives, Installers, Other). Ordinary code validates the proposal, shows the whole plan, and moves files only after you type y.
tidymind scan ~/Downloads
tidymind plan ~/Downloads --output plan.json
tidymind apply plan.json
Who gets to be lazier: anyone with a folder they keep promising to clean. The effort saved is the sorting itself. The safety rules exist so that being lazy can't cost you a file.
Demo
Real output from my machine (Windows 11, qwen3:4b, synthetic files):
Asking local model 'qwen3:4b' (metadata stays on this machine)...
1. [OK] backup.zip -> Archives/backup.zip
reason: Common archive extension .zip
2. [OK] holiday photo.jpg -> Images/holiday photo.jpg
reason: Common image extension .jpg
3. [OK] invoice_oct.pdf -> Documents/invoice_oct.pdf
reason: Common document extension .pdf
4. [OK] meeting notes.txt -> Other/meeting notes.txt
reason: Text file with unclear purpose
5. [OK] setup.exe -> Installers/setup.exe
reason: Common installer extension .exe
5 operation(s) would run; 0 would be skipped.
Move/rename 5 file(s) inside ...\Downloads? [y/N]:
Here is what happens when the folder changes between planning and applying. After the plan was made, I created a file at one destination and edited another source file:
1. [SKIP] backup.zip -- source changed since the plan was created (stale plan)
3. [SKIP] invoice_oct.pdf -- destination already exists (never overwritten)
Result: succeeded=3 skipped=2 failed=0
The existing file kept its content (PRECIOUS existing data), the other three files moved, and the exit code was 7 (partial completion).
Code
Bangkahdev
/
tidymind
A local, privacy-first file organizer CLI. A local open-weight model (via Ollama) proposes a category and an optional clearer file name for each file in a folder. Deterministic code validates the proposal, shows you the complete plan, and moves files only after you approve.
TidyMind
A local, privacy-first file organizer CLI. A local open-weight model (via Ollama) proposes a category and an optional clearer file name for each file in a folder. Deterministic code validates the proposal, shows you the complete plan, and moves files only after you approve.
The model is advisory. It cannot run commands, choose paths, delete files, or skip your approval.
Implements docs/SRS-TidyMind.md v1.1. Status: MVP code and tests written
real-model acceptance, offline test, model evaluation, lockfile and CI runs are still pending
(see Verification status). Nothing here claims they have passed.
Why open-weight / local
- File names, extensions and sizes never leave your machine: the client refuses any non-loopback endpoint.
- No API key, no per-request cost, no service dependency after the one-time model download.
- The model is swappable (
--model), and it is boxed in by a strict schema and an application-side allowlist.
Requirements
- Python…
How I Built It
Open-source AI used: qwen3:4b (Q4_K_M quantization, Apache 2.0 license as shown by ollama show) running locally in Ollama 0.40.11. The runtime has zero third-party Python dependencies; the HTTP call is plain urllib.
How the project is built around it. The design question was not "how do I get a model to sort files?" but "how do I make it impossible for a wrong or manipulated model to hurt anything?" The model holds no authority:
scan -> ask local model -> validate -> preview -> you approve -> move
- It sees almost nothing. Only an id, file name, extension and size. Never file contents, directory paths or modification times.
- Its output is untrusted data. Strict JSON schema, strict parsing, three bounded retries, then fail closed.
-
It never chooses a path. A plan holds a category and a file name; my code derives
root / allowlisted category / validated name. -
The validator rejects path separators,
.., drive prefixes, control characters, reserved Windows names, extension changes, duplicate destinations, symlinks, and stale or missing sources. -
Nothing is overwritten and there is no delete. Moves use a no-overwrite rename (
os.renameon Windows,renameat2withRENAME_NOREPLACEon Linux); where that can't be guaranteed, the move is refused. -
Approval can't be skipped. There is no
--yesflag. Onlyy/yesapproves; anything else exits with code 6 and changes nothing. - File names can't attack the terminal. Control and bidi characters are escaped in all output.
Testing. The tests never need Ollama. Black-box tests run the CLI in subprocesses against a fake Ollama server and compare before/after snapshots of the folder (paths, sizes, SHA-256). They cover declined approvals, conflicting targets, changed sources, symlinked category folders and hostile plan files (../escape.pdf, C:\x, CON.pdf, control characters). 261 tests pass on Windows 11, and 10 POSIX-only tests are skipped there. The GitHub Actions workflow runs the suite on Ubuntu and Windows with Python 3.11 and 3.12.
Results (synthetic data, one run)
| Model |
qwen3:4b, Q4_K_M, Ollama 0.40.11 |
| Hardware | i7-14650HX, RTX 4050 Laptop (6 GB), 16 GB RAM |
| Files | 24 synthetic names, expected categories fixed before inference |
| Correct categories | 23 / 24 (95.8%) |
| Invalid responses | 0 (retries disabled for this test) |
| Time | 24.5 s for one batch of 24 files |
The one miss: meeting notes.txt went to Other instead of Documents (the model's reason was "Text file with unclear purpose"). That is honest uncertainty from a classifier that only sees names, and not a safety problem.
What went wrong
With four files, the model returned a duplicate id three times in a row and the tool stopped with The model returned a duplicate id. (gave up after 3 attempts). Nothing changed on disk, because the pipeline fails closed.
The root cause is the model returning an invalid answer. My retry logic could not recover because it resent the identical request at temperature: 0, which makes the same wrong answer likely. The first attempt is still deterministic; retries now vary the sampling. I have not re-run the four-file case on the real model since the change, so I can't claim the fix works there. It is covered by a unit test that checks retries vary the sampling.
Limitations
- Classification uses names, extensions and sizes only. 95.8% on 24 mostly obvious synthetic names is not an accuracy estimate for a real folder.
- The model proposed no renames in my test set, so renaming is unproven.
- No batch rollback: a run can partially complete, and the result is reported per file.
- Windows and Linux only; macOS is refused because a no-overwrite rename isn't guaranteed there.
- My manual runs covered a dry run, declined approval, a conflicting target, a changed source and an approved apply of six files. I did not record a hash comparison for the manual approved run (the automated tests do compare SHA-256), and I did not run mutation testing or a network-disconnected test.
Why Does Open Innovation Matter?
File names leak more than you'd think (tax_return_2025.pdf, diagnosis_results.pdf). With a local open-weight model those names never leave the machine, and the code enforces it: any endpoint other than localhost, 127.0.0.1 or ::1 is refused. There is no API key and nothing is billed per request.
Running the model myself also let me iterate for free: the retry experiments above cost nothing and needed no quota. The model is a flag (--model), so it can be swapped; I only evaluated qwen3:4b.
The Lazy Payoff
Planning five files took about 5 to 7 seconds end to end on my laptop (including starting the tool), a batch of 24 files took 24.5 seconds of inference, and applying the plan is one keystroke. I did not time manual sorting for comparison, and I only used it on synthetic folders this week, so treat this as a speed measurement and not as a measured saving.
Top comments (0)