Portfolio project: human-governed automation on a personal inbox
Inbox Marshal
Inbox hygiene with reversible automation by default
Gmail spam cleanup and receipt/subscription organization, built to run on your own inbox with your own Google Cloud project and API key. Claude classifies each email, and the category selects a fixed, bounded action: label, or archive plus label. It never permanently deletes, and unsubscribe needs explicit per-sender confirmation. Classification quality is not measured.
See what is measuredView the repoWhat is not built
- 14 unit tests passing (latest CI run)
- Never deletes
- Classification quality not measured
- Auto mode has no human approval
- Classifier and Gmail client untested
01
The problem
Inboxes accumulate marketing spam faster than anyone manually unsubscribes from it, and receipts and bills get buried in the same pile as everything else.
Most email cleanup tools either delete things without asking or require trusting a third-party SaaS with full inbox access. This runs entirely on your own machine, with your own Google Cloud project and your own API key.
Deleting without asking
The strongest action here is archive plus label, into real Gmail folders such as
Marshal/Flagged-Spam. You browse and delete from Gmail yourself.Trusting a spoofable link
Unsubscribe uses only the standardized
List-Unsubscribeheader (RFC 8058), never links parsed out of an email body, and never formalicious_spam.
02
How it works
Three modes with different permissions share one pipeline: read mail, classify, file. The model chooses the category; the action for each category is fixed in code.
Stages, one at a time
Stage 1 Fetch
gmail_client.py scans the mailbox by date over a configurable window (lookback_days), excluding Spam and Trash.
Stage 2 Trusted-domain floor
trusted_domains in config.yaml is a hard floor independent of the model: matching domains are never touched.
It ships empty, so the floor protects nothing until it is filled in.
Stage 3 Classify
One forced-tool Claude call per email (classifier.classify_email) picks a category: malicious spam, marketing spam, payment receipt, subscription/bill, security alert, needs-your-attention, or leave-alone. A malformed result falls back to leave_alone.
The fallback is real code but is not covered by automated tests.
Stage 4 File
The category selects a fixed action: spam is archived out of the inbox and labeled; receipts, bills and security alerts are labeled but stay visible. --scan changes nothing, --apply confirms before filing, --auto files without a prompt.
Stage 5 Unsubscribe (optional)
Only for marketing_spam, only in --apply, only after per-sender confirmation, via the List-Unsubscribe header. Never in --auto.
03
What is measured
Nothing is benchmarked here, and the page says so. The table carries its caveat directly underneath.
From the repository's README and its CI.
| Item | Status |
|---|---|
| Unit tests | 14 passing in the latest CI run (Python 3.9 and 3.12) |
| What the tests cover | List-Unsubscribe parsing and trusted-domain matching |
| Classification precision and recall | not measured: no evaluation set and no eval/ harness |
Classifier, fail-safe fallback, Gmail client, --auto flow | untested |
Caveat How often Claude mislabels a real email as spam, or the reverse, has not been quantified. Use --scan first and review what it would do.
Source: tests/, README Known limitations
04
Architecture pattern
Pattern Deterministic pipeline with an LLM classifier (not an agent loop), with a human-gated apply step.
run_agent.py fetches mail, skips trusted domains with plain code, and sends each remaining email to one forced-tool Claude call. The returned category selects a fixed, bounded action in code. The action table, the trusted-domain floor, the never-delete rule and the unsubscribe mechanism are deterministic; each email is judged independently with no memory or planning.
- Gmailgmail_client.py fetches by date window
- Trusted-domain checkplain code, independent of the model
- Classifierclassifier.py: Claude, forced tool call, one category per email
- Scan reportterminal and a markdown file
- Filearchive plus label into real Gmail folders (--apply or --auto)
- Unsubscribemarketing_spam only, --apply plus per-sender confirmation, List-Unsubscribe header
- Notifynotify.py macOS notification in --auto; scheduled by launchd
05
Competencies demonstrated
Human-centered automation
Reversible filing and confirmation requirements match action risk.
Privacy engineering
User-owned OAuth project, local execution, trusted-domain floor, and minimal retention.
Failure-safe design
Malformed model output defaults to
leave_alone(implemented inclassifier.py; not yet covered by automated tests).Operational design
Interactive and unattended modes cannot accidentally share blocking behavior.
Product thinking
Backlog cleanup and ongoing hygiene are treated as distinct user workflows.
06
What is not built or not verified yet
Taken from the repository's own Known limitations. This is part of the pitch: the gaps are known, written down and not rounded away.
- not verified
Classification quality
There is no evaluation set, no precision/recall numbers, and no
eval/harness yet. - limit
--auto has no human approval
It archives and labels on the model's single classification per email. Archiving is reversible, but a misclassified email can sit hidden in a
Marshal/folder until you look; it only notifies you afterward. - limit
trusted_domains is empty by default
Until you add your bank, employer, etc., nothing is protected by the hard floor.
- limit
Test coverage is thin
Tests cover
List-Unsubscribeparsing and trusted-domain matching. The classifier, its fail-safe fallback, the Gmail client, and the--autoflow are untested. - limit
Trusted-domain matching trusts the From header
Matching is on the sender's From domain only, which is not authenticated by this tool; a spoofed From header can match a trusted domain.
- not built
Audit log
This repo writes no audit log, only the Gmail labels themselves.
07
Roadmap
- DoneScan, apply and auto modes with a never-delete rule
- DonePer-sender confirmed unsubscribe via
List-Unsubscribe - DoneScan by date across the mailbox, not only the literal inbox label
- Not doneOpen gap, no plan stated in the README: an evaluation set and precision/recall numbers
- Not doneOpen gap, no plan stated in the README: tests for the classifier, fallback, Gmail client and auto flow