← Back to list

A Local-First Inbox Cleaner: No OAuth, No Cloud, No LLM Calls on Your Mail

Last week I pointed an inbox cleanup tool at my own mail and it asked me a question no other cleanup tool I’ve used had ever asked:

Vikas Sah · 2026-05-06 15:04 · 0 claps · 7.1 min read
#inbox #unsubscribe #claude-code #github-repositories
Open on Medium ↗
Wiki topics: LLM · Large Language Models 🔓 · Open Source

A Local-First Inbox Cleaner: No OAuth, No Cloud, No LLM Calls on Your Mail

Last week I pointed an inbox cleanup tool at my own mail and it asked me a question no other cleanup tool I’ve used had ever asked:

“Heads up: unsubscribing this Substack bundle takes Lenny with it (and vice versa to keep). Want to keep them grouped?”

That single line is the entire reason I built [enuff-is-enuff-unsubscribe](https://codecoincognition.github.io/enuff-is-enuff-unsubscribe/getting-started.html). The popular cleanup tools don't ask it because they can't. They bucket your senders by who's on the From-line. Six writers I read for six different reasons all happen to route through Substack, and the tools were quietly making one decision for me as if it were six.

This piece is about what the tool does, how it works differently from Unroll.me / SaneBox / Clean Email, and why I built it to never see my live inbox in the first place.

What it does

[enuff-is-enuff-unsubscribe](https://codecoincognition.github.io/enuff-is-enuff-unsubscribe/getting-started.html) is a Claude Code plugin. You feed it a mailbox export — a Gmail Takeout .mbox file, an Apple Mail .mbox, or a folder of .eml files — and it does three things:

  1. Scans. Reads the headers, ranks every recurring sender by volume and noise, tags whatever shouldn’t be touched (banking, security alerts, receipts, two-factor codes), and writes the result to a local report directory.
  2. Reviews. Walks you through the senders brand-by-brand, in chat, with sample subject lines and a recommendation per row. You triage the list by replying in plain language: “keep 1, 3, 6,” “unsub all retail and social, keep all newsletters,” “kill everything from a sender I haven’t replied to in a year.”
  3. Acts. Once you’ve approved the queue, it works through the unsubscribes one by one. Finds the unsubscribe link from the List-Unsubscribe header, drafts an opt-out email when the link is broken, writes a Gmail filter rule when there’s no path out at all.

There’s nothing magical underneath. It’s about 600 lines of Node.js standard library plus a small set of Claude Code skills and slash commands. The unsubscribe action runs through your local browser. Nothing is hosted, nothing is proxied, nothing is logged.

How it’s different from the popular tools

Three differences from Unroll.me, SaneBox, Clean Email, and the rest of the category.

It never sees your live inbox. Most cleanup tools begin by asking for OAuth access to your Gmail, an IMAP password, or both. They have to. They run in the cloud, and they need read-and-write access to your mail account to do anything useful. That access doesn’t go away after the cleanup; it sits there as a connected app until you remember to revoke it. enuff-is-enuff-unsubscribe runs against a file you exported yourself. The tool never logs into anything. There is no token to revoke because there was no token to issue.

It groups senders by what unsubscribing actually does, not by who sent the email. This is the boring engineering decision that produces the Substack moment. Most tools bucket by sender domain. If [lennysnewsletter.com](http://lennysnewsletter.com/) shows up in the From-line, that's one row. If [pragmaticengineer.com](http://pragmaticengineer.com/) shows up, that's another. Plumbing-wise, both of those writers' unsubscribe URLs route through *.[substack.com](http://substack.com/). The tool that buckets by sender hides that coupling. You can't make a real decision about Lenny without knowing whether you're also making one about Stratechery. So I bucket by the unsubscribe URL host instead. More on this below.

Every irreversible action requires explicit approval. The popular tools tend to bias toward speed: one click, hundreds of unsubscribes. That’s fine until they’re wrong about something. They unsubscribe you from your bank, or your kid’s school, or a list you actually wanted to stay on. The plugin makes you authorize the queue before any unsubscribe runs, and refuses to touch anything it classified as protected. The trade is: you give up the one-click experience. You get back the ability to read what you’re about to do.

Why it’s local-first

There are no servers in the loop, and the reason is a story most people in this category would rather you forgot.

In April 2017, The New York Times (Mike Isaac, “Uber’s C.E.O. Plays With Fire”) reported that Slice Intelligence, Unroll.me’s parent company, had been selling aggregated, anonymized Lyft transaction data scraped from its users’ inboxes. The buyer was Uber, which used the data as market research on its largest competitor. Unroll.me CEO Jojo Hedaya posted an apology on the company blog the next day. The product still exists. Slice rebranded. The disclosure language got longer.

The receipts had been collected through Unroll.me’s normal product loop: the user signed up, granted inbox access, the cleanup ran, and the data flowed downstream into the parent’s analytics business. The user got a cleaner inbox. The user didn’t know they were paying for it with their ride history.

Unroll.me apologized, the parent company adjusted their disclosures, the product still exists. But the category never recovered the trust it lost. People who paid attention have been suspicious of any cleanup tool that begins with “connect your email” ever since. People who weren’t paying attention now use these tools by default, on the assumption that nothing has changed since the apology.

The structural fix isn’t a different terms-of-service. It’s not having the data in the first place. If the tool runs locally, against a file you control, on hardware you own, the tool has no path to resell your data. Every other choice followed that one. No OAuth flow, no IMAP password, no accounts, and nothing phones home.

The trade is real. You have to export your mail before you can use the tool. Gmail Takeout takes thirty minutes for most accounts, longer for old ones, and the file you get is a few hundred megabytes. That’s friction. I think it’s the right friction. The category got too cheap, and getting cheaper meant getting worse.

The Substack moment, walked through

Here’s what the plugin showed me when I pointed it at my own inbox last week.

The classifier filtered out the protected senders first — banking, two-factor, calendar invites, receipts I keep for taxes — and produced a triage list of twenty-six recurring brands across thirty-seven streams. About six rows down, the plugin printed a cluster I hadn’t expected to see:

[7] Substack bundle (10 writers, 28 emails)
    - Lenny's Newsletter
    - Money Stuff (Matt Levine)
    - Pragmatic Engineer (Gergely Orosz)
    - One Useful Thing (Ethan Mollick)
    - Platformer (Casey Newton)
    - Stratechery (Ben Thompson)
    - + 4 others
    NOTE: unsubscribing this Substack bundle takes Lenny with it
    (and vice versa to keep). Want to keep them grouped?

I actually said “huh” out loud. I’d subscribed to those writers on six different days, in six different years, for six different reasons, and the plugin was telling me they were all on a single string.

In my head, those are six independent writers. Lenny is a product newsletter. Matt Levine is finance. Gergely is engineering management. Ethan is AI-and-work. Casey is platforms. Ben is strategy. They live in different mental folders. I subscribed to each on a different day, in a different year, for a different reason.

The plugin doesn’t see any of that. The plugin reads the List-Unsubscribe header. And in the headers, all six writers point at hosts on *.[substack.com](http://substack.com/). Substack runs the unsubscribe handler. The action that happens when you click "unsubscribe" on any of those six rows is the same action: a request to Substack's platform layer, scoped to whatever Substack thinks it's scoped to.

A cleanup tool that buckets by sender produces a clean six-row list and lets you triage them independently. It looks correct, and it feels correct, until a writer you wanted to keep stops showing up. But you can act on the per-writer rows confident you’ve made six decisions when, in the underlying infrastructure, you’ve made one. You don’t notice until much later — and even then you probably blame yourself, because it’s hard to reason backward from “Lenny disappeared from my inbox” to “Substack treated my unsub request as bundle-scope.”

Bucketing by unsubscribe URL host instead of sender domain makes the coupling visible at the moment you have to decide. You see the bundle, you see what it contains, and you can either make a single bundle decision or drill into a specific writer and find the direct path that bypasses the platform handler. Money Stuff, for instance, has a Bloomberg-direct list that doesn’t share Substack’s plumbing. The decision the user thought they were making becomes one they can actually make.

The Substack case is the most legible because Substack hosts a lot of the writers anyone with my reading habits cares about. The same pattern shows up for Beehiiv hosts, ConvertKit-hosted creators, Mailchimp landlords, and any other platform that runs a centralized unsubscribe handler. The plugin warns about all of them. Most tools warn about none of them.

What it doesn’t do

Three honest limits.

The plugin works on .mbox exports and .eml folders. It doesn't talk to Gmail's live API, doesn't parse Outlook's .pst, and doesn't currently handle Proton Mail's encrypted format directly — you have to use Proton's official export tool first.

It doesn’t have a one-click experience. The export-then-scan workflow takes longer than logging into Unroll.me. That’s the price of the privacy stance. If you want one click, the popular tools will give you one click, on the terms I described above.

It also doesn’t have a UI in the conventional sense. The “interface” is Claude Code’s chat surface plus a local HTML report. That works well if you already use Claude Code; it works less well if you don’t.

These are deliberate scope choices, and I think they’re the right ones for v1. They’re not permanent, and none of them are about to change in ways that would compromise the design choices.

Try it on your own inbox

The repo is at github.com/codecoincognition/enuff-is-enuff-unsubscribe. MIT license. The README has a two-command install for Claude Code users and a download-the-zip-and-run path for everyone else.

Step by step walkthrough here: https://codecoincognition.github.io/enuff-is-enuff-unsubscribe/getting-started.html

If you’ve got a Gmail Takeout sitting around from the last time you tried to clean your inbox and gave up, this is what to point it at. The first thing you’ll see is the same Substack prompt I saw, and the second thing you’ll see is whatever bundle is hiding in your own inbox that you’d never noticed before.

The tool shouldn’t be the interesting part. What’s interesting is what it shows you about the inbox you thought you understood.


메타데이터
post_id
a9cf7fe4ef65
slug
most-inbox-cleanup-tools-read-your-email-mine-refuses-to-a9cf7fe4ef65
url
https://medium.com/@engineeratheart/most-inbox-cleanup-tools-read-your-email-mine-refuses-to-a9cf7fe4ef65
canonical_url
https://medium.com/@engineeratheart/most-inbox-cleanup-tools-read-your-email-mine-refuses-to-a9cf7fe4ef65
author_url
https://medium.com/@engineeratheart
status
ok
fetched_at
2026-06-24 11:06:28