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-Unsubscribe header (RFC 8058), never links parsed out of an email body, and never for malicious_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.

What is and is not measured

From the repository's README and its CI.

ItemStatus
Unit tests14 passing in the latest CI run (Python 3.9 and 3.12)
What the tests coverList-Unsubscribe parsing and trusted-domain matching
Classification precision and recallnot measured: no evaluation set and no eval/ harness
Classifier, fail-safe fallback, Gmail client, --auto flowuntested

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.

  1. Gmailgmail_client.py fetches by date window
  2. Trusted-domain checkplain code, independent of the model
  3. Classifierclassifier.py: Claude, forced tool call, one category per email
  4. Scan reportterminal and a markdown file
  5. Filearchive plus label into real Gmail folders (--apply or --auto)
  6. Unsubscribemarketing_spam only, --apply plus per-sender confirmation, List-Unsubscribe header
  7. 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 in classifier.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-Unsubscribe parsing and trusted-domain matching. The classifier, its fail-safe fallback, the Gmail client, and the --auto flow 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