Quietly unsubscribe from bulk email.
A desktop app that finds everyone who mails you in bulk and unsubscribes from the ones you pick — without ever risking your receipts, order confirmations, or password resets.
No server. No telemetry. Your own Google credentials. Nothing is ever permanently deleted.
- Connects to your Gmail using credentials you create yourself — Hush ships with none.
- Reads the sender, subject and date of your messages. Never the contents.
- Groups them by sender, showing how much each one sends and how often.
- For the senders you tick, it can do any combination of three things:
| What it does | Guaranteed? | |
|---|---|---|
| Unsubscribe | Sends the sender's own one-click unsubscribe — the identical request Gmail's button makes | No. It's a request; they might ignore it or take a fortnight |
| Block | Creates a Gmail filter sending their future mail straight to Trash | Yes. It's a rule in your account and doesn't ask anyone |
| Bin the backlog | Moves their old newsletters to Trash | Yes, for everything Hush has scanned |
The recommended default is unsubscribe and block: the first takes you off their list properly at source, the second means it doesn't matter if they ignore you. That combination is the honest answer to "I unsubscribed and I'm still getting mail".
It's a one-shot tool. Run it, deal with your senders, close it. Nothing runs in the background and you can uninstall it straight afterwards — the unsubscribes, filters and deletions all stay done.
Nothing is ever permanently deleted, and Hush never asks for a permission that would let it. Binned mail and blocked mail both go to Trash, where Gmail keeps it for 30 days. Filters are visible and removable under Settings → Filters in Gmail.
Google asks you to approve three things on its own consent page, as separate tick-boxes you can decline individually:
- Read your mail — sender, subject and date only. Required.
- Manage your mail — to move old newsletters to Trash. Cannot delete permanently; that is a different permission and Hush never asks for it.
- Change your settings — to add the filter that blocks a sender for good.
Decline any of them and Hush still runs, with that feature switched off. There is also a read-only option on the connect screen if you would rather grant nothing until you have seen what the app found.
This is the part worth understanding before you trust it with your inbox.
Bulk and marketing mail carries a List-Unsubscribe header (RFC 2369).
Transactional mail — receipts, shipping notices, password resets, two-factor
codes, invoices — generally does not, because there is nothing to unsubscribe
from.
So: a sender is only ever offered to you if their mail carries that header. Senders without it are not shown, not selectable, and not reachable through any path in the app. That single rule prevents the worst thing this kind of tool can do.
That gate lives in one function — Group::finish in
src-tauri/src/store.rs — and everything downstream reads from it, so no part of
the interface can route around it.
On top of that:
- A never-touch list. Add anyone to it and they can't be selected, in this session or any future one.
- Warnings on likely-transactional senders. Banks, payment processors, airlines, delivery services, government, healthcare — plus subject-line signals like receipt, order, invoice, verification code, reset your password. These are not blocked, because plenty of shops send both marketing and receipts from the same address. They're flagged in a warning colour, with a plain-language reason, and need a second, separate confirmation.
- Nothing is ever pre-selected. Every checkbox starts empty. The bulk selection helpers deliberately skip flagged senders.
- A confirmation screen that says exactly who, how many, and what will be sent — with a panel showing the literal request.
- Dry run, on by default. The first launch sends nothing at all until you turn it off.
There are three kinds of unsubscribe in the wild, and Hush treats them differently on purpose.
| What Hush does | Why | |
|---|---|---|
| One-click (RFC 8058) | Sends a POST with body List-Unsubscribe=One-Click. Fully automatic. |
The sender has explicitly promised that this exact request means "unsubscribe" and nothing else. |
mailto: |
Either opens a ready-written message in your own mail app (default), or sends it through Gmail (only if you grant the send permission). | Unambiguous, but sending mail as you is a big permission — so it's opt-in. |
| A plain link | Nothing. It's listed under "you'll open these yourself", and you tick them off as you go. | A bare link might be a one-tap unsubscribe, a preference centre, a login wall, or a confirmation page. We can't tell, so a human decides. |
Hush will not follow redirects on a one-click endpoint (RFC 8058 forbids them), sends no cookies, and refuses any unsubscribe URL that resolves to your own network rather than the public internet.
Every mass-unsubscribe tool is an interface over three controls: unsubscribe, block, or filter. Tools that only do the first cannot answer the commonest complaint about them — "I unsubscribed and they're still emailing me."
Unsubscribing is a request. It depends on the sender honouring it, doing so promptly, and not having you on four other lists under a different address. Even a flawless implementation can't promise the mail stops, and any tool that says otherwise is overstating.
A Gmail filter isn't a request. It's a rule in your own account, and it works identically whether the sender is scrupulous, slow, or ignoring you outright.
So Hush does both, and is clear about which is which. There are also senders who
accept the one-click POST with a 200 and then still want you to press a
button on their page — that violates RFC 8058, it's undetectable in advance
because they respond identically to a compliant sender, and it's exactly why
blocking exists.
Unsubscribing stops the next newsletter. It does nothing about the 600 already sitting in your inbox, so Hush can bin those too — but only if you ask, twice: once when connecting (it needs a wider Google permission, and it's a tick-box that starts off), and again on the confirmation screen before each run.
Two things make it safe rather than alarming:
It only bins mail that carried an unsubscribe header. This is the same gate that decides who's unsubscribable, reused. A shop that sends you marketing and order confirmations from one address has the header on the marketing and not on the receipts — so the marketing goes and the receipts stay. The confirmation screen tells you the split before you commit: "Also bin their old emails (658) … 164 other emails from these senders — receipts, confirmations and the like — will be left alone."
It moves mail to Trash, never deletes it. Gmail keeps trashed mail for 30
days, so a mistake is yours to undo without needing us. Hush has no code path
that permanently deletes anything, and never requests a permission that would
let it — gmail.modify grants trashing but not permanent deletion, which is
exactly why it's the one used.
If you skip the permission, everything else works exactly the same. The tick-box is simply absent.
Leaves your computer: requests to googleapis.com and accounts.google.com,
and — only for senders you tick, only when dry run is off — one request to that
sender's own unsubscribe endpoint.
Never leaves your computer: everything else. There is no Hush server. There
is no analytics, no crash reporting, no update ping, no usage counter. You can
verify this: the app's web layer runs under a Content Security Policy that
forbids network requests entirely (src-tauri/tauri.conf.json), and the only
HTTP client in the Rust code is used for Google and for unsubscribe endpoints.
Stored on your computer, in a single SQLite file:
- Per message: sender address, sender name, subject, date, and the unsubscribe headers.
- Your never-touch list, your settings, and what happened when you unsubscribed.
Subjects are stored because the safety heuristics read them — a sender whose recent subjects are all "Your order has shipped" needs a warning, and that judgement can't be made without the words.
Stored in your operating system's keychain: the Google refresh token, and nothing else. If your machine has no working secret store, Hush keeps it in memory for the session and tells you so, rather than quietly writing it to a file.
Settings → Erase everything revokes access with Google, clears the keychain, and deletes the database. Your mail is untouched by it — that button only clears what's on this computer.
| Permission | When | What it allows |
|---|---|---|
gmail.readonly |
Always | Reading message metadata. Cannot change anything. |
gmail.modify |
Only if you tick "bin the old emails" | Moving mail to Trash. Not permanent deletion. |
gmail.send |
Only if you tick "send mail as me" | Sending the handful of unsubscribes that only work by email. |
Hush trusts what Google actually granted rather than what it asked for, so declining an extra on the consent screen leaves that feature switched off rather than failing later.
The app walks you through this one screen at a time, with buttons that open the exact pages. The short version:
- Create a project at console.cloud.google.com/projectcreate.
- Enable the Gmail API.
- Fill in app details — pick External as the audience.
- On the audience page, leave publishing status as Testing and add your own address as a test user.
- Create an OAuth client of type Desktop app on the clients page.
- Paste the Client ID and secret into Hush.
Keeping the project in Testing means Google never reviews it — because nobody except your listed test users can use it. No verification, no security assessment, no waiting.
The cost: Google expires the connection every seven days. Hush will notice and ask you to reconnect, which is one click. This is Google's rule and there is nothing the app can do about it short of asking you to publish the project and submit to review, which defeats the point.
Google issues desktop OAuth clients a secret, and then documents that it is not treated as confidential — it ships inside every copy of any app that uses it. Hush stores yours in the local database rather than the keychain for that reason, and relies on PKCE, which is what actually binds an authorisation response to the session that asked for it.
Download from Releases. Nothing else is required — no Node, no Rust, no Python.
| Platform | File |
|---|---|
| Windows | .msi |
| macOS | .dmg — universal, runs on both Apple silicon and Intel |
| Linux | .deb (Debian 12+, Ubuntu 23.04+), .rpm (Fedora, RHEL, openSUSE), or .AppImage |
On Linux the app needs WebKitGTK 4.1, which is what the system provides for
rendering. Ubuntu 22.04 and older ship 4.0 only, so the .deb will refuse to
install there with an unmet dependency on libwebkit2gtk-4.1-0 — that is the
distro being too old, not the package being broken. Debian 12, Ubuntu 23.04 and
anything newer are fine.
Unless the maintainer has an Apple Developer account, macOS builds are not signed or notarised, and macOS will refuse to open them by double-click.
To open it anyway:
- Drag Hush to Applications.
- Right-click (or Control-click) the app → Open → Open in the dialog.
If macOS says the app "is damaged and can't be opened", that's Gatekeeper's message for an unsigned download rather than actual damage. Clear the quarantine flag:
xattr -dr com.apple.quarantine /Applications/Hush.appOnly run that on a build whose checksum you've verified (see below).
Releases are built by GitHub Actions on GitHub's own runners — not on anyone's laptop. The workflow file is in this repository, and every run's log is public.
Each release publishes SHA256SUMS. To check what you downloaded:
# macOS / Linux
shasum -a 256 -c SHA256SUMS --ignore-missing
# Windows (PowerShell)
Get-FileHash .\Hush_0.1.0_x64_en-US.msi -Algorithm SHA256git clone https://github.com/justlinuxnoob/hush
cd hush
git checkout v0.1.0 # the tag you downloaded
npm ci # exact versions from package-lock.json
npm run tauri buildThen compare your SHA256 against the published one.
Honest caveat: the checksums will probably not match yet. Fully reproducible
builds need every input pinned — compiler, linker, system libraries, embedded
timestamps and paths — and this project is not there. What is pinned today: the
Rust toolchain (rust-toolchain.toml), the exact dependency versions
(Cargo.lock, package-lock.json), and CARGO_INCREMENTAL=0 with
codegen-units = 1. See NOTES.md for what remains.
What you can verify today, and what actually matters: that the published binary came from this source, built by a public CI run you can read, from a commit you can inspect.
Requires Rust and Node 20+. On Debian or Ubuntu you also need:
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev build-essential curl wget file libssl-devThen:
npm install
npm run tauri dev # run it
cargo test --manifest-path src-tauri/Cargo.toml # run the tests
VITE_HUSH_DEMO=1 npm run dev # the interface, with a fake backend and no GoogleSee NOTES.md for what's unfinished and what was guessed at.
Gmail bills in "quota units" rather than requests, and the published per-user
ceiling has changed more than once. Rather than hard-code a number that may
already be wrong, Hush uses an adaptive limiter: it starts conservatively,
speeds up while requests succeed, and halves its rate the moment Google pushes
back — additive increase, multiplicative decrease, the same control loop TCP
uses. Retries use exponential backoff with jitter and honour Retry-After.
Scans are cached in SQLite, resumable, cancellable at any point, and a second scan asks Gmail only what changed since the last one using its history marker.
See CONTRIBUTING.md. The short version: the safety gate is not negotiable, and changes near it need tests.
MIT.