I had 60,000+ emails in Gmail and no quick, safe way to clean up what I actually wanted to delete in bulk. So I built a local, free cleanup agent around Qwen 3, a custom MCP server, and an approval flow that keeps risky actions in my hands.
The full request path: you chat with a local model, the agent proposes, the MCP server holds all real authority, IBM Verify decides and approves on your phone, and Gmail only ever sees a trash-only credential. A kill switch revokes the grant and Verify sessions on a suspicious tap.
This gives readers the mental model immediately: Qwen 3 stays on the desktop, the MCP server narrows what the model can do, IBM Verify decides when the action is sensitive, and Gmail only ever sees the constrained credential.
Reasoning stays on the machine. The mailbox does not leave the desktop for a hosted model.
Broad intent becomes narrow tools: preview first, commit second, and no blind bulk action.
Credentials are sender constrained and sensitive operations can require explicit approval.
Bulk cleanup stays recoverable. Permanent delete is outside the credential's reach.
I had more than 60,000 emails in Gmail and no quick, safe way to get rid of the clutter I actually wanted to delete in bulk. Newsletters, security codes, promotions, old receipts, and one-off purchase confirmations just kept piling up. The obvious move is to hand the whole thing to a cloud assistant and let it rip.
The problem was not just volume. It was trust. I wanted a fast way to visualize what was about to happen, see how many messages matched, understand which senders were involved, and avoid waking up the next morning to find that an outside agent had confidently deleted something I needed.
So I built my own. It runs entirely on my laptop, uses Qwen 3 locally, and never sends mailbox content off to a hosted AI service. That matters for privacy, but it also matters for cost: once the machine is sitting on my desk, the intelligence is effectively free. Around that, I wrapped the same identity fabric this series has been building all along: a custom MCP server is the perimeter, IBM Verify decides, the credential is sender constrained, and my phone approves anything sensitive.
Do not ask the model to behave. Make misbehavior impossible.
That line became the design principle for the whole project. The local model was sometimes wrong, occasionally overconfident, and exactly the sort of thing you should never trust with permanent destructive power. That turned out to be fine, because the system was built so the model never needed to be perfect.
Before any model, before any identity layer, the first design choice was the most important one. The agent cannot permanently delete anything. Not because I told it not to. Because it literally cannot.
Gmail gives you two very different ways to remove a message. One moves it to Trash, where it sits for thirty days and can be pulled back. The other bypasses Trash and destroys the message immediately. That second path requires the broad https://mail.google.com/ scope. The narrower gmail.modify scope can still search, label, archive, and move messages to Trash, but the permanent delete call gets a hard refusal.
So I am using gmail.modify and nothing more. In this system, “delete” really means “move to Trash.” It is recoverable for thirty days, reversible with a single call, and enforced by the credential itself rather than by polite prompting.
The two guarantees live on the landing screen, not in a prompt: bulk cleanup only ever moves mail to Trash, and every run goes plan → confirm → execute.
There is a unit test in the repo whose only job is to fail the build if any source file ever references the permanent delete call or the broad scope. The safety is structural. The test just guards the structure.
This is the whole philosophy in one move. The rest of the build is just the same idea repeated at different layers.
A local model is not magic. Give it a list of hundreds of message IDs and ask it to decide what should go, and eventually it will lose the thread. That is a normal failure mode for a smaller local model, and pretending otherwise is how you end up bulk deleting the wrong thing.
So the model never touches raw message IDs. It works in two phases.
In the first phase it composes a Gmail search, something like “promotions older than ninety days from this sender.” It calls a preview_cleanup tool. The MCP server runs the search, counts the matches, groups them by sender, pulls a few representative samples, and mints a single-use token. Nothing is deleted. Nothing can be deleted yet.
The built-in explainer says it plainly: it previews what it would trash first count, senders, and sample messages and nothing moves until you confirm. Sensitive mail needs an extra step, and the session is protected by IBM Verify.
Only after that do I decide whether the preview looks right. If it does, the system trades that single-use token for the scoped action that moves matching messages to Trash. The model proposes. I commit. That separation is what makes the workflow usable instead of reckless.
The model gets to be creative about what to propose. It does not get to be the thing that pulls the trigger.
Trashing a promotion is one thing. Trashing a bank statement or a message from family is something else entirely. I did not want the model making that distinction on its own.
So the sensitivity decision is deterministic and lives in the MCP server, not in the prompt. Before anything gets trashed, the server inspects senders and subjects. If the sender is a bank, or the subject looks like a statement or payment notice, the message is flagged financial. If the sender is me or someone on my family list, it is flagged personal. Any flagged message in the set pushes the whole operation up a tier.
The model does not get a vote on that. It cannot talk its way into deciding that a financial email is harmless clutter. The rule looks at the raw data and decides.
The risk line should be deterministic. A model can be clever. The policy layer should be boring.
This app runs on localhost, on my own laptop, for exactly one user. It still got a real login. “It’s just local” is how personal tools become the softest target in the house.
So the app is gated behind IBM Verify with authorization code plus PKCE, an httpOnly session, and a server-side session guard. Signing out actually destroys the session, not just the cookie. The token behind that session is DPoP-bound, which means the token is tied to a private key held on the laptop.
If someone steals the token, it is not enough. Presenting it without the matching private key fails. A bearer token works for whoever holds it. A sender-constrained token works only in the hand it was issued for.
The local app still sits behind IBM Verify authorization code + PKCE, an httpOnly session, and a DPoP-bound token tied to a private key on the laptop.
Signing out or a revoke actually destroys the session, not just the cookie. The server-side guard refuses to act, even mid-task on sensitive senders like a bank.
Reading is quiet. The agent can search and summarize all day. Trashing ordinary clutter is a confirmation step. But the moment an operation touches something flagged financial or personal, the agent has to ask my phone.
This is the tiered part. Tier one is read, no friction. Tier two is a routine trash, with a confirmation card. Tier three is a trash operation that swept up something sensitive, and that triggers an approval through IBM Verify on my phone. I approve on my phone or nothing happens.
That approval gate is not a suggestion the model can route around. The server refuses to execute a tier-three operation until it has a real approval in hand.
When a cleanup sweeps up messages flagged financial or personal, the preview says so and confirming asks for approval on the phone before anything is trashed. Here, four flagged messages were phone-approved, then moved to Trash (recoverable for 30 days).
During testing, the local model occasionally acted exactly like a local model. It was overconfident. It invented outcomes. It sometimes described actions that had not happened. That is annoying, but it is only dangerous if the model has real authority.
In this system, it does not. The perimeter does not run on trust. The model cannot permanently delete. It cannot bypass preview. It cannot mint the approval state. And it cannot force the execute path without the exact chain of controls lining up behind it.
Qwen 3 is smart enough to make this useful on a desktop. The important part is that the walls were designed for the moments when any model is wrong.
UPDATE
Since I first wrote this, the agent has grown a lot. It archives and labels as well as trashes, I added automation/scheduling. It can clear a sender's backlog while keeping the newest message. Every action has one-click Undo for thirty days. Opening the app now lands on a dashboard: one card per built-in cleanup, each with a count of what is ready and a toggle to run it automatically, daily or weekly.
Here is the part I care about. Not one of those features touched the execute path. The dashboard's Preview button submits the exact same preview request I would type in chat. The routines only read counts; a human still confirms every cleanup. Adding a dozen features to this system meant adding zero new ways to move mail.
Each card shows what a cleanup would actually clear. Starred mail and protected senders are already excluded from the number, because an early version once said "2 ready" for two messages the perimeter then correctly refused to touch. The count and the action now share one definition of cleanable.
The newest wall is my favorite, because it started as a question rather than a feature request: what about the emails I star? I star mail to keep up front in my inbox and quick access to important email. Nothing in the system knew that.
Now it does, the same way it knows everything else that matters: structurally. A starred message is never bound into a cleanup token at preview, and at execute the server re-checks Gmail's live starred list and drops anything I starred while the confirmation was sitting on screen. If that check fails, the cleanup refuses to run rather than run without it. There is no setting for this and no way to talk the model out of it. A star means keep.
One confirmation card carrying the whole model: the proposed set, the mail withheld because senders are protected, the mail withheld because I starred it, the financial flags that push the operation to Tier 3, and the phone approval that follows.
The direction of travel is always the same: move authority further from the model. Next on the list is fronting sensitive actions with an IBM Verify token exchange, so the step-up decision becomes an auditable access policy the agent cannot see or influence at all.
I built this to clean my inbox. What I actually built was a template for how to let an agent touch something real without pretending the model deserves unconditional trust.
Strip out the Gmail specifics and what is left is a shape: a local model reasons, an MCP server holds the perimeter, identity decides the action, credentials are sender-constrained, and sensitive moves go to a human’s phone. That shape works because it assumes the model will eventually be wrong.
The agent that cleans my inbox is small. The idea behind it is not.