cd /news/ai-tools/my-screenshots-folder-was-a-to-do-li… · home › topics › ai-tools › article
[ARTICLE · art-157350] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

🦥 My Screenshots Folder Was a To-Do List That Never Reminded Me. So I Hired a Sloth.

A developer built slothlens, an open-source tool that runs Gemma 3 4B locally through Ollama to read and categorize screenshots overnight, sorting them into searchable folders while redacting sensitive text before storage. The tool is designed to run only during a configurable night window, pause when the machine is unplugged, the keyboard or mouse is in use, or CPU load exceeds 35%, and commit each result to SQLite immediately so failed images are retried on the next run. On a deliberately small 2-vCPU, 4 GB test machine with no GPU, a 16-screenshot demo set took roughly 32 minutes to process, with individual images taking three to seven minutes each.

by read18 min views1 publishedOct 25, 2026

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

Open your Desktop. Go on, I'll wait.

Chances are it's a graveyard of files called Screenshot 2026-10-21 at 01.58.44.png. Hundreds of them. Most have never been opened twice.

Look closer and a pattern appears. We almost never take a screenshot to keep something. We take it to do something later:

A screenshot is an intention with no reminder attached. The Desktop isn't a mess. It's a to-do list that has never once reminded anyone of anything.

And sitting right next to those intentions are some of the most sensitive files on a developer's machine: a bank OTP, a transfer receipt with a full account number, a café wifi password, and (admit it) a screenshot of a .env file with a live API key in it.

slothlens is a sloth that eats your screenshots while you sleep.

After midnight, on AC power, when nobody has touched the keyboard for a while, it wakes up and reads your screenshots one at a time with Gemma 3 4B running locally through Ollama. Then it goes back to sleep. In the morning you get:

slothlens find cors brings back that error screenshot, even though the filename means nothing.2026-10/error/2026-10-19_vite-cors-error.png, fully reversible with slothlens undo. Who gets to be lazier? Anyone with a screenshot graveyard. You stop scrolling through Screenshot (847).png looking for that one error, you stop forgetting the interview invite you screenshotted on Tuesday, and you never have to sort a single file by hand.

No cloud, no account, no API key. Screenshots only ever go to the Ollama server on localhost, and extracted text passes through three layers of redaction before it is stored.

A vision model on a small CPU is slow. Painfully slow. So slothlens doesn't fight it: it's slow on purpose, and it's polite about it.

Sloth rule How
Only works at night Configurable window, default 00:30-07:00
Never drains your battery Stops when you unplug ( psutil )
Yields to humans Stops if the keyboard or mouse was used in the last 5 minutes (macOS, Windows, Linux via xprintidle )
Yields to other work Stops if the CPU is busier than 35%
Lowest priority on the machine os.nice(19)
One bite at a time One image per request, 20 seconds of rest between bites
Never loses its place Every result is committed to SQLite immediately; failed images are retried on the next nap

The gate is checked before every screenshot. Come back to your laptop mid-nap, and the sloth finishes the bite it's chewing, then goes back to sleep.

The test machine is deliberately tiny: 2 vCPUs, 4 GB of RAM, no GPU. The demo set is a "graveyard" of 16 screenshots with predefined labels: chats, receipts, errors, bookings, a .env file, an OTP, a wifi password, and one screenshot that tries to hijack the model.

An excerpt from the first full nap:

$ slothlens nap --now
🦥 found 16 uneaten screenshots. at sloth speed that's about 32m00s.
🌿 [2/16] chewing Screenshot 2026-10-24 at 13.07.15.png ...
    3m36s  [credentials] The Workshop WiFi login details screenshot. 🔒
🌿 [3/16] chewing Screenshot 2026-10-24 at 09.20.44.png ...
    3m45s  [chat] MLH Hacktoberfest Dev Team reminder about submissions deadline.  ⏰ Reply to the Dev Team about the #devchallenge by Oct 25.
🌿 [4/16] chewing Screenshot 2026-10-23 at 18.02.09.png ...
    5m59s  [map] Workshop Coffee meetup details in Google Maps  ⏰ Check the Hacktoberfest meetup details on Sat 25 Oct.
🌿 [5/16] chewing Screenshot 2026-10-23 at 11.36.18.png ...
   ⚠️  couldn't digest it: <HTTPError 500: 'Internal Server Error'>
   ...
🌿 [9/16] chewing Screenshot 2026-10-22 at 08.05.50.png ...
    6m37s  [booking] Google Calendar invites technical interview for frontend intern React/TypeScript project  ⏰ Prepare React/TypeScript project walkthrough for interview on Oct 27th, 10:00 AM (GMT+7)
   ...
🌿 [11/16] chewing Ảnh màn hình 2026-10-21 lúc 09.14.33.png ...
    3m59s  [booking] VietJet Air booking confirmation for Ho Chi Minh City to Hanoi flight.  ⏰ Review VietJet Air booking details before departure.
🌿 [12/16] chewing Screenshot 2026-10-21 at 01.58.44.png ...
    4m40s  [credentials] Magento CMS environment variables reveal Stripe API key and public URL. 🔒
🌿 [13/16] chewing Screenshot 2026-10-20 at 21.13.09.png ...
    3m50s  [chat] Vietcombank sent a one-time code for account [REDACTED]. 🔒
   ...
🌿 [16/16] chewing Screenshot 2026-10-19 at 23.41.07.png ...
    3m57s  [error] Vite error fetching posts from local API server, 8080

The 32-minute estimate was optimistic: with no history yet, the sloth guesses two minutes per image. Reality was closer to four.

Those 500 s are the Linux out-of-memory killer (more on that below). The sloth shrugs, writes it down, and moves on. The next nap tries those screenshots again:

$ slothlens nap --now          # next nap
🦥 found 3 uneaten screenshots. at sloth speed that's about 12m35s.
    4m15s  [receipt] Vietcombank transfer receipt for 2.5M VND to Nguyen Van An. 🔒  ⏰ Review the transfer details for accuracy.
    5m03s  [chat] Senior dev explains Docker's origin in a Stack Overflow chat.  ⏰ Review Docker's origin story.
   ⚠️  couldn't digest it: <HTTPError 500: 'Internal Server Error'>

$ slothlens nap --now          # and the one after
🦥 found 1 uneaten screenshots. at sloth speed that's about 4m15s.
    4m21s  [receipt] GrabFood order total is 66000 VND with a -20000 VND promo.

Yes, that's about four minutes per screenshot. In a chat app that would be unusable. For a job that runs while you sleep, it's roughly 14 screenshots an hour, and nobody is waiting. (On a laptop with a GPU or Apple Silicon, it's seconds.)

In the morning:

slothlens digest --html ~/digest.html

The same report comes out as Markdown in the terminal. The note at the top is written by Gemma, from the night's captions:


While you slept, slothlens read **16 screenshots** in 1h08m of slow, steady chewing
(about 4m16s each). Cloud bill: $0.00.

> Slowly waking, let's just focus on that React/TypeScript interview tomorrow morning –
> a calm mind is the best start to the day.

## ⏰ You screenshotted these to remember
- [ ] **Prepare React/TypeScript project walkthrough for interview on Oct 27th, 10:00 AM (GMT+7)**
- [ ] **Reply to Minh Tran about the slides by Friday.**
- [ ] **Test the memoization before Monday's release.**
- [ ] **Check the Hacktoberfest meetup details on Sat 25 Oct.**
  ...

## 🔒 Probably shouldn't still be lying around
- The Workshop WiFi login details screenshot. · _password_ (`slothlens forget 2`)
- Vietcombank transfer receipt for 2.5M VND to Nguyen Van An. · _card or bank account number_
- Magento CMS environment variables reveal Stripe API key and public URL. · _api key or token_
- Vietcombank sent a one-time code for account [REDACTED]. · _one-time code_

Weeks later, when the filename means nothing:

$ slothlens find cors
#16   Oct 19  [error] Vite error fetching posts from local API server, 8080
      demo/graveyard/Screenshot 2026-10-19 at 23.41.07.png

$ slothlens find "otp vietcombank"
#13   Oct 20  [chat] Vietcombank sent a one-time code for account [REDACTED]. 🔒
      demo/graveyard/Screenshot 2026-10-20 at 21.13.09.png

$ slothlens find interview
#9    Oct 22  [booking] Google Calendar invites technical interview for frontend intern React/TypeScript project
      demo/graveyard/Screenshot 2026-10-22 at 08.05.50.png

The CORS caption doesn't even say "CORS". Search also covers the text the model copied out of the image, so the error message itself is searchable.

And the tidy-up, which is a dry run until you say --apply, and fully undoable afterwards:

$ slothlens tidy --apply
moved 16 files. changed your mind? `slothlens undo` (batch 0621bf83)

2026-10/
├── booking/      2026-10-21_vietjet-air-booking-details.png
│                 2026-10-22_frontend-interview-react-typescript.png
├── chat/         2026-10-20_minh-tran-slides-request.png
│                 2026-10-20_vietcombank-otp-message.png
│                 2026-10-21_docker-origin-chat.png
│                 2026-10-24_dev-team-hacktoberfest-reminder.png
├── code/         2026-10-22_json-structured-clone.png
│                 2026-10-23_code-memoization-co-editing.png
├── credentials/  2026-10-21_magentic-cms-env.png
│                 2026-10-24_the-workshop-wifi-login.png
├── design/       2026-10-22_fintech-dashboard-concept.png
├── error/        2026-10-19_vite-cors-error.png
│                 2026-10-24_sqlite-db-error.png
├── map/          2026-10-23_workshop-coffee-meetup.png
└── receipt/      2026-10-20_grabfood-order-details.png
                  2026-10-23_vietcombank-transfer-receipt.png

$ slothlens undo
put 16 files back where they were.

A sloth that eats your screenshots while you sleep.

You have 2,000 files called Screenshot 2026-10-21 at 01.58.44.png. Some are errors you meant to google. Some are messages you meant to answer. One is your bank OTP. One is your .env file with a live Stripe key.

slothlens is a local, deliberately slow night-shift agent. After midnight, on AC power when nobody is touching the keyboard, it reads your screenshots one at a time with Gemma 3 4B running on your own machine through Ollama, and in the morning it leaves you:

slothlens find "cors 5173" 2026-10/error/2026-10-19_cors-error-localhost.png, with undo No cloud. No account…

About 1,300 lines of plain Python, MIT licensed. The only required dependency is Pillow (psutil is optional, for precise battery and CPU checks). It talks to Ollama over HTTP, keeps everything in a single SQLite file, and ships with a test suite that runs without a model.

ollama pull gemma3:4b
git clone https://github.com/longphanquangminh/slothlens && cd slothlens
pip install -e ".[gates]"

slothlens init                  # writes ~/.slothlens/config.json
slothlens nap --now --limit 3   # try it now instead of waiting for midnight
slothlens digest --html ~/digest.html
slothlens schedule              # prints the cron / launchd / Task Scheduler line for your OS

The repo also includes the 16-screenshot demo set, the scoring script (demo/evaluate.py), and the raw logs from every run described below.

watch dirs
    │
    ▼
find uneaten screenshots (SHA-1 dedupe)
    │
    ▼
gate: night? plugged in? human idle? CPU idle?
    │ yes                               │ no
    ▼                                   └── sleep, resume on the next nap
Gemma 3 4B via Ollama
(JSON-schema-constrained output, one image)
    │
    ▼
normalize + secret scrubbing + regex redaction
    │
    ▼
SQLite + FTS5 ──► digest · find · remember · tidy/undo

I needed a model that could read screenshots, run on a CPU with 4 GB of RAM, and read Vietnamese, because plenty of screenshots in Vietnam are Vietnamese (a Vietnamese Mac even names them Ảnh màn hình…, so slothlens recognises screenshot filenames in a dozen languages).

Gemma 3 4B checks all three boxes: it's natively multimodal, the Q4_K_M quantisation is about 2.5 GB plus an 850 MB vision projector, and it's trained on 140+ languages. It read an unaccented Vietcombank SMS (Ma OTP cua Quy khach la…) without any special handling.

Every image costs a fixed 256 tokens for Gemma's vision encoder, plus about 450 tokens of instructions. On 2 vCPUs, prompt processing runs at roughly 3 to 5 tokens per second and JSON generation at about 2. That's where the four minutes go.

The first nap died on screenshot #1 with an HTTP 500. dmesg explained it:

Out of memory: Killed process 1637 (llama-server) anon-rss:3957420kB

When Ollama runs on CPU, it turns mmap off and loads the whole model into anonymous memory. Weights, vision projector and KV cache came to 3.9 GB on a 4 GB machine. The kernel did what kernels do.

The fix was two request options:

options = {"temperature": 0.1, "num_ctx": 2048, "use_mmap": True}

With mmap on, the weights live in the page cache, which the kernel can reclaim instead of killing the process. That made things much better, but 4 GB is still 4 GB: some requests still hit the OOM killer. So the real fix was making failure boring. A failed screenshot is logged, skipped, and retried on the next nap.

While debugging, I started a second nap without noticing the first one was still running. The second sloth woke up, looked around, and went straight back to sleep:

🦥 found 16 uneaten screenshots. at sloth speed that's about 32m00s.
💤 stopping: CPU is busy (50%). 16 left for tomorrow night.

It saw another process using the CPU (its own twin) and yielded, exactly as designed. That's when I knew the gate worked.

Ollama accepts a JSON Schema in the format field and compiles it into a grammar, so the model can only produce valid JSON, and the category can only be one of eleven values:

payload = {
    "model": "gemma3:4b",
    "format": SCHEMA,              # grammar-constrained decoding
    "messages": [{"role": "user", "content": PROMPT, "images": [b64]}],
    "options": {"temperature": 0.1, "num_ctx": 2048, "use_mmap": True},
}

The most important field in the schema is remember:

People screenshot things so they can act on them later. Calendar invites, interviews, tickets and bookings, events with a place or a time, deadlines, requests in a chat and bills to pay are almost always reminders. Write the action as a short imperative with the date or time you can see.

That one paragraph is what turns a pile of images into a to-do list.

My first prompt asked a yes/no question: sensitive: true if the image shows a password, one-time code, API key…. Gemma flagged 13 of 14 screenshots. The digest's "shouldn't still be lying around" column was basically the entire week:

My favourite justifications, verbatim:

"Contains a timestamp and hashtags."

"Contains code snippet."

"API endpoint and URL present."

"contains personal communication."

Small models love saying yes to yes/no questions. So instead of rewording the question, I changed its shape:

sensitive: boolean became secret_seen, an enum: none, password, one-time code, api key or token, card or bank account number, identity document, medical record. "A timestamp" doesn't fit in any of those boxes. key_text. The model has to copy out the important text (and redact it) before it gets to decide whether there's a secret in it. The paranoia was gone. Then, two screenshots into the next nap, this showed up in my terminal:

3m20s  [credentials] Connect to The Workshop guest WiFi using Workshop_Guest_5G and latte-art-2026. 🔒

It correctly flagged the wifi password as a secret, correctly redacted it in key_text, and then wrote it into the caption anyway. My regex safety net missed it, because the caption had no password: in front of it.

The fix I ended up liking: ask the model to point at the secret, then throw its answer away. A new field, secret_values, has Gemma copy each secret exactly as it appears. Before anything is stored, slothlens scrubs those strings out of every other field and deletes the list. It lives in RAM for a few milliseconds and is never written anywhere.

def normalize(raw):
    secrets = raw.pop("secret_values", None) or []          # the model tells us what to censor...
    secrets = sorted({s.strip() for s in secrets if len(s.strip()) >= 4}, key=len, reverse=True)
    for k in ("caption", "key_text", "remember", "slug"):
        raw[k] = scrub(str(raw.get(k, "")), secrets)       # ...we censor it everywhere...
    ...                                                       # ...and the list is never stored

So there are now three layers between a secret and the database: the model's own redaction, the model-pointed scrub, and a regex pass for OTP codes, sk-…/ ghp_…/AWS/Slack keys, JWTs, password: … and long card or account numbers.

Sensitive screenshots are also blurred in the HTML digest. My first version used a CSS filter: blur(), until I realised the unblurred thumbnail was still sitting in the page as base64 for anyone who opened view-source. Now the pixels themselves are blurred before they're embedded.

The next run looked fantastic. Then I read this line closely:

⏰ Check in for flight VJ124 before Sat 31 Oct 06:15

That was word for word the example I had written in the prompt. While tuning the remember instructions, I had "helpfully" used screenshots from the test set as examples: the flight, and "Send the slides to Minh by Friday 5pm." The model wasn't reading the flight confirmation. It was copying my homework.

So I swapped the examples for things that appear nowhere in the test set (an electricity bill, a dentist appointment, a message to someone called Lan), added one rule (never write a task about the screenshot itself, after it suggested "Archive this screenshot for future reference" for a meme), and re-ran everything from scratch. All the numbers below come from that clean run.

If you build evals for your own prompts: grep your prompt for your test data. I didn't, at first.

Yes/no prompt Final prompt
Screenshots flagged as sensitive 13 of 14 4 of 16
Real secrets caught 4 / 4 4 / 4
False alarms 9 0
Secrets that reached the database 0 0

A screenshot is text written by strangers. One test screenshot is a terminal that starts with:

The prompt tells the model that everything in the image is data, the category is constrained by the grammar, and normalize() clamps every field afterwards. Gemma filed it as an error, summarised the SQLite traceback underneath, didn't mark it safe, didn't call it a cat, and didn't invent a to-do. It also has no tools to misuse: its only output is metadata.

The evaluation script then greps the entire database for every secret planted in the test images: the OTP, both API keys, the Postgres password, the wifi password and the bank account number. Secrets found: 0 out of 6. The snitch run would have failed this check, and that's exactly why the check exists.

Search is SQLite FTS5 with tokenize='unicode61 remove_diacritics 2', so you can search Vietnamese without typing accents: slothlens find "chuyen tien" finds the Chuyển tiền thành công transfer receipt.

Better still, find "com tam" finds the GrabFood receipt even though Gemma misread it as Cơm Tắm instead of Cơm Tấm. Accent-insensitive search quietly forgives the model's accent mistakes too.

Scored by demo/evaluate.py against the 16 labelled screenshots:

Metric Result
Category accuracy 14 / 16
Sensitive screenshots caught 4 / 4, 0 false alarms
"Remember" to-dos caught 6 / 6, 3 false alarms
Secrets that reached the database 0 / 6
Prompt injections obeyed 0 / 1
Time per screenshot 4m16s on average (3m25s to 6m37s)
Cloud bill $0.00

The evaluation uses 16 synthetic screenshots with predefined labels; treat it as a development-set smoke test, not a held-out benchmark.

What it still gets wrong:

slothlens done 18 dismisses one in a second.chat instead of credentials (it was still flagged as a one-time code, so it's in the 🔒 list), and the tweet was filed as magentic-cms as "Magento CMS" every single time. Because of what's in the pictures. A screenshots folder holds OTPs, bank transfers with full account numbers, wifi passwords and, sooner or later, a live API key. A cloud vision API would be the most convenient way to build this, and also the fastest way to leak all of it. With an open-weight model on my own machine, the threat model is "is my laptop secure?", a question I already have to answer anyway.

Because slow is cheap when you own the model. With an API, every screenshot is another paid call, forever, and the bill scales with your bad habits. With Gemma running locally, the cost is a bit of electricity at 3 a.m. That changes the design entirely: when there's no per-request bill and nobody is waiting, latency stops mattering, and a model that takes four minutes per image is perfectly fine.

Because I can change the brain. The model is one string in a config file. On a machine with more RAM, gemma3:12b. If something better comes out next month, it's ollama pull and one edit. If I want to fine-tune it on my own screenshot habits, the weights are right there. There's also an OpenAI-compatible backend, so it works with llama.cpp's server, LM Studio or vLLM.

Because it works offline. Once the model is downloaded, it runs on a plane, in a lab with no internet, or in a country where some APIs aren't available.

Because I could open the hood. When the model leaked a password into a caption, I didn't file a ticket with a provider and wait. I changed the schema, added a scrub step, and re-ran the eval the same night. Every prompt, schema and redaction rule is in the repo.

Where a closed model would win. A frontier vision model would caption faster, more accurately, and wouldn't put a café in the wrong city. But the job is "is this a receipt or a to-do?", done while you sleep, on some of the most private files you own. For that job, a 4B open model on your own CPU is the better tool, not the compromise.

The useful part isn't the folder tree. It's turning

Screenshot 2026-10-22 at 08.05.50.png

into

Prepare React/TypeScript project walkthrough for interview on Oct 27th, 10:00 AM.

and turning

Screenshot 2026-10-20 at 21.13.09.png

into a polite note that there's still a bank OTP lying around.

The effort it replaces is the effort nobody actually does: opening hundreds of screenshots one by one, deciding which ones still matter, writing down the to-dos, deleting the risky ones, and renaming the rest. slothlens did all of that for 16 screenshots in about an hour of machine time and zero minutes of mine. The only thing left for a human is to read one page in the morning, tick a few boxes, and run slothlens forget on the screenshots that should never have survived.

You don't get more organised. You get a sloth.

Best Use of Gemma. Gemma 3 4B is the entire brain of slothlens: it runs locally through Ollama, reads every screenshot with its vision encoder, writes the captions and to-dos, names the secrets it sees (after four rounds of prompt engineering), reads Vietnamese out of the box, and writes the morning note in the digest. I picked it because it fit all three constraints at once: vision, a 4 GB CPU-only machine, and multilingual text.

── more in #ai-tools 4 stories · sorted by recency
── more on @slothlens 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/my-screenshots-folde…] indexed:0 read:18min 2026-10-25 · —