Damien
A Gmail triage CLI that asks before it acts. Rules decide what they can, a model judges the residue, and code performs every write — in bulk, journalled, and reversible.
It was built to answer one question honestly: can a mailbox of a hundred thousand messages be put in order by software you would actually leave alone with your mail?
Why it is different
- It never acts on mail you have not read. A plan is a document. Applying one takes an
approval token — printed once by
plan approve, stored only as a hash — and--only <action>applies exactly the slice you looked at. - Every write is reversible. Each batch is journalled as
pendingbefore it reaches Gmail and markeddoneafter, sorun undoputs back the state each message had when the plan was built (inbox, unread, label).trashis a label and Gmail holds it 30 days; there is no permanent-delete path in the code or the scope. - Rules run first, and they are free. A rule that maps onto Gmail search syntax is executed by Gmail — no listings, no headers, no model. On a settled mailbox a pass costs $0.
- The model only ever sees what your rules could not settle — headers and a snippet —
and every answer is cached, so a message is never paid for twice. It reads the text only
if you ask it to (
DAMIEN_READ_BODIES=on_escalation), only for the messages the snippet could not settle, and never an attachment. - Your keys, your credential, nothing in the middle. No server, no account here, no
telemetry. Run it with no model at all if you like (
DAMIEN_JUDGE=rules). - It does not send. There is no send path: no code in Damien sends, drafts-and-sends,
or forwards (the
gmail.modifyscope technically permits sending; Damien never calls it).
What it did on a real mailbox
One Gmail account, used daily, 107,710 messages in the inbox when this started. Every figure below is measured by the tool itself, not estimated:
| 107,710 → under 7,000 messages in the inbox | 94,202 of them were 2024 or older |
| 101,048 messages acted on, across 29 applied runs | each one bulk, journalled, reversible |
| 44,985 judgements by the model · 27,072 decisions by rules | rules are mined from your own corrections — and they are free |
| 200 corrections → 68 rules, 67 of them evaluated by Gmail itself | so 36% of all decisions now cost nothing and never drift |
| $0.0000449 per judgement | ≈$0.045 per 1,000 messages; a 7,054-message inbox took $0.17 and about three minutes |
| $0.000000 once the rules cover it | a settled pass re-decided 233 messages in 3 seconds, 0 model calls |
| 44,985 judgements ≈ $2.02, best case | and many were served from cache at no cost at all |
| a 200-message plan cold: ~10 s, $0.0075 · warm: ~1.0 s, $0.0000 | the cache key is the request, so unchanged mail is never bought twice |
That table is the whole argument for the architecture: the expensive part shrinks as you use it, because every class of mail you decide once becomes a rule that costs nothing forever. The model is the on-ramp, not the engine.
What it looks like
From a real run over the author's mailbox (7,054 messages still in the inbox; 68 rules; about three minutes):
$ damien plan build -q 'in:inbox' --show 12
plan 8f6ed799980e (7054 item(s))
archive 49
keep 6825
label 66
trash 114
judgment: 3709 call(s), 575 from cache, $0.1713
archive jev 121d 'Clean up your report, Ivan.' <notifications@mail.creditkarma.com>
keep rule 117d 'AI is making your next Apple device cost more' <theneuron@newsletter.theneurondaily.com>
label Receipts jev 13d 'You accepted an AWS Marketplace offer' <no-reply@marketplace.aws>
trash jev 13d '💀 You're Not Ready for This Friday…' <support@theduabrand.com>
$ damien plan approve 8f6ed799980e
token: 1f0c…
$ damien plan apply 8f6ed799980e --token 1f0c… --yes --only trash
run 2ba1162fdb34 ok: 114 message(s) in 1 batch(es) (trash slice)
the rest of the plan is still approved
to take it back: damien run undo 2ba1162fdb34 --yes
Nothing in Gmail changed until that last command, and run undo 2ba1162fdb34 reverses it.
Install
uvx --from git+https://github.com/ivan-rivera-projects/damien damien --help # try it without installing
pipx install git+https://github.com/ivan-rivera-projects/damien # or install it
From a checkout, if you want to hack on it:
git clone https://github.com/ivan-rivera-projects/damien && cd damien
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
State lives in ~/.damien (one SQLite file by default; set DAMIEN_DB_URL for Postgres).
Set it up
Two things are yours, and neither is shared with anyone else:
- A Gmail credential of your own — a Google OAuth desktop client from your own
Cloud project, dropped at
~/.damien/credentials.json, thendamien login. One scope (gmail.modify), and you can revoke it whenever you like. - A judge, or none — any decision model with the Jev contract:
openrouter(DAMIEN_MODEL+OPENROUTER_API_KEY),typesafe(TYPESAFE_API_KEY),local(DAMIEN_JUDGE_URL; nothing leaves the machine), orDAMIEN_JUDGE=rulesto use no model at all and pay nothing.damien judge evalthen calibrates the threshold for your model.
Then damien init writes your profile and a rules file, and damien doctor tells you
whether anything is missing (it changes nothing).
→ docs/SETUP.md has the click-by-click, including the OAuth
testing-mode trap that expires tokens weekly (it caught this project's own first
credential). → docs/FIRST-RUN.md is the first hour, in order.
The pipeline
scan ──► rules ──► (residue) ──► the judge ──► guardrail ──► confidence ──► plan ──► apply ──► journal
free one request a person's threshold review bulk undoable
server- per message signals win groups
side
- Rules first, and for free. A rule that maps onto Gmail search syntax is executed by
Gmail.
to_query()returnsNonewhen it cannot be certain, and the rule then runs in Python over listed mail — a wrong query acts on the wrong mail, soNoneis the safe direction. - Gmail's own work is trusted before ours. Spam is never fetched.
CATEGORY_PERSONALis kept by a rule with no model call. Gmail's category reaches the model as context. - The person's work is trusted above both. Labels you name in
DAMIEN_PROTECT_LABELSare a floor the model cannot cross, your labels reach the model asyour_labels, and your own corrections outrank a rule and the model on every later plan. - The judge sees only the residue, as a small state (headers + snippet, never the body, unless escalation is on) and atomic questions: which bucket, was it sent by a machine, does it want a reply, how important. One request per message. The answers are kept as data, so thresholds can be re-tuned without paying again.
- A guardrail may only raise an action to
keep— never overrule you or a rule. It fires on money and deadline subjects, replies from real people, mail you already read, and Gmail's ownIMPORTANT. - A confidence gate. Below
DAMIEN_CONFIDENCE_ASKa judgement keeps the message with the reason attached: recorded, but not acted on. - Plans are read, then applied — by group or by action, with every batch journalled
pendingbefore it lands anddoneafter, so a run is resumable and undoable.
Use
damien doctor # is everything wired up?
damien scan -q 'in:inbox older_than:2y' # how much is there? reads only
damien plan build -q 'in:inbox' -n 200 # decide; writes nothing to Gmail
damien plan digest <plan-id> --min-size 5 # collapse it into review groups (N:fp)
damien plan approve <plan-id> # prints a token, once; only its hash is kept
damien plan apply <plan-id> --token <t> --only archive --yes
damien run undo <run-id> --yes # take a run back
damien correct <message-id> --action keep --note 'a brand we follow'
damien rules from-senders <plan-id> --yes # make your decisions permanent
damien rules plan --scope 'in:inbox older_than:2y' # a rule pass over history, bounded
damien judge eval # calibrate the model's threshold
damien pass weekly # the scheduled pass: build, approve, apply the certain
damien skill install --target openclaw # give an agent host the Damien skill
--json on status, doctor, scan, plan build|digest|show|approve|apply|apply-group,
run list|undo, rules list, correct, restore, judge eval, pass.
Safe to run twice: plan build never writes to Gmail, an applied plan refuses to re-apply,
a partial run resumes only when its filter matches and only the batches its journal does not
hold, and an undone run cannot be undone twice.
Documentation
| document | read it for |
|---|---|
docs/SETUP.md |
your own Gmail credential and judge key, step by step |
docs/FIRST-RUN.md |
the first hour: look, apply, learn rules, schedule |
docs/COMMANDS.md |
every command — when to use it, what it writes, what it costs |
docs/HOW-IT-WORKS.md |
the pipeline, the store, and what it refuses to do |
docs/POLICY.md |
the layers of authority, the seven buckets, the rule vocabulary |
docs/RUNBOOK.md |
the weekly and monthly passes, failure modes, how to undo |
docs/PRIVACY.md |
exactly what leaves your machine, and when |
Two more are for machines or reviewers rather than users: docs/SKILL-PROMPT.md is a prompt
that has an AI agent (Hermes, OpenClaw, anything that runs commands) create its own skill for
driving this, including the scheduled passes below, and docs/AUDIT-BRIEF.md is a brief for an
independent reviewer — the invariants this tool promises, where each is enforced, and the
surfaces where a defect would hide.
Cleaning a mailbox of a hundred thousand messages
Brute force does not work, and the numbers say why: a Gmail header costs quota, and a first attempt to fetch a whole inbox throttled after 64 seconds. A paced pass over 107,710 messages would take ~90 minutes of wall time before any judgement happened.
107,710 in:inbox
94,202 2024 and older <- 87% of it, historical records
2,256 Jan – Oct 2025
1,717 Oct 21 – Dec 2025
9,536 2026
So the order is: rule passes first, judgement second, and never both over the same messages.
| step | how | messages | cost |
|---|---|---|---|
| archive what a rule can describe | rules plan --scope … — Gmail evaluates it |
tens of thousands | ~1 min, $0, no headers |
| judge what is left and recent | plan build -q '<a date range>' |
a few thousand | ~$0.000046 each |
| keep it clean | rules from-senders on the recurring senders |
— | free |
The rule query is always wrapped in in:inbox unless --scope says otherwise: a bare
older_than:365d matched 152,881 messages in this mailbox because it had escaped the inbox
and was reaching Sent and archived mail too.
Where state lives
| SQLite (default) | Postgres (DAMIEN_DB_URL) |
|
|---|---|---|
| setup | none — one file | one env line |
| vectors | text column, clustered in Python | message_embeddings + pgvector |
| tested | ✅ | ✅ in CI: Linux runners start a postgres:17 service; macOS runs SQLite only |
Tables: messages, decisions, verdicts (the judgement cache), message_embeddings,
plans, plan_items, runs, run_batches, corrections. A column added after release is
applied by ADDED_COLUMNS in store.py — never by dropping the table.
The judgement cache
The cache key is the request: the model, the questions as written, and the state the model
was shown. Reword a question, change the model, or add a label yourself — invalidated.
Nothing else. That is why a settled mailbox costs nothing to re-check, and why editing the
buckets in decide.py is the one change that re-buys every answer.
Review groups (the digest)
Senders do not collapse an inbox: 200 messages came from 140 senders, 102 with a single
message. Similarity does. Local embeddings (fastembed, no torch), greedy
nearest-centroid, and a threshold chosen from the data — 0.65 on this mailbox, where 0.70
left 75 groups and 0.60 dissolved the useful "4 finance offers" group into the marketing
blob. Result: 200 messages → 36 review groups.
What this deliberately does not do
- No bodies unless you ask. Judgement uses headers and a snippet. With
DAMIEN_READ_BODIES=on_escalationit also reads the newest text of the messages the snippet could not settle — quoted replies stripped, capped, attachments never opened — and keeps what it read on your machine so a re-plan costs nothing. Off by default. - No scheduler. Have the host's own scheduler run
damien pass weeklyevery Monday anddamien pass monthlyon the 1st (see the RUNBOOK); the tool exits either way. - No permanent delete.
trashis recoverable for 30 days, and the code cannot do more. - No sending. No send path: nothing sends, drafts-and-sends or forwards (the scope technically permits sending; Damien never calls it).
- No telemetry. Nothing is reported anywhere.
- No MCP door. A small tool wrapper over these functions is the obvious next step for a host that cannot shell out; the CLI costs zero tool schemas in a context.
Model facts this is built on (all from the vendor docs)
- One request per message with every question about it: questions in a call are
independent and parallel ("adding questions barely changes the response time"), while
many emails in one
statecosts accuracy ("Jev suffers from context rot"). confidence(0–1) describes how peaked the answer is, not whether it is right — hence conservative bands and a person as the last resort.- $0.042/Mtok input, output free; the vendor's published measurement is ≈$0.0256 per 1,000 items, and this tool measured ≈$0.000046 per message on a real mailbox.
- A Choice always picks an option even when none fit, so a companion Noul and an
unclearbucket guard it. - Never ask it to count, order or compute — that is code's job.
How the model actually behaved, measured
Across 44,985 judgements on one real mailbox:
| 10,735 | 24% | cleared the 0.8 bar and were acted on unattended |
| 11,611 | 26% | between the bars — reported, not acted on |
| 22,639 | 50% | kept, because it was not sure enough to touch anything |
| 36,775 (82%) | of what it chose was keep; trash was only 5.5% of its answers |
Two things follow, and they are the reason this design is safe to leave running:
- Half the model's answers never moved a message. The confidence gate is not decoration:
a judgement below
DAMIEN_CONFIDENCE_ASKis recorded and the message stays where it is. - The model is conservative, and the rules do the deleting. Its instinct is
keep(82%); the 27,072 decisions made by rules — mined from your own corrections, and evaluated by Gmail — are where the archive-and-trash volume actually lives. You decide the classes; the model only handles what is left, and only when it is sure.
200 corrections were enough to produce 68 rules, which is 0.44% of judgements touched by hand. That is the whole loop: correct a class once, and it never costs a model call again.
Status
Beta, and in daily use on a 100,000-message mailbox. The test suite runs on Linux and macOS; Postgres is exercised on the Linux runners. Interfaces may still move; the safety properties above will not.
MIT licensed. The predecessor project — a four-service platform that this replaced — was torn down in October 2026; its repositories remain on GitHub for history and nothing of it runs.
Metadata
Release files for damien 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| damien-0.4.1.tar.gz | 139.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| damien-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 232.3 kB
Release files / damien-0.4.1.tar.gz
| Download URL | damien-0.4.1.tar.gz |
|---|---|
| Size | 139.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
16f2987e0a9aa3e8e3caba42a66782798d7b9191e9ff626d3322502b9536f75d
|
|
BLAKE2b-256 checksum How to use checksums |
85fd382f8ad01c9ec4efd328c42a9e39aec205a324e2372033c090e99626618b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / damien-0.4.1-py3-none-any.whl
| Download URL | damien-0.4.1-py3-none-any.whl |
|---|---|
| Size | 92.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cba1f7c6e20014c7769e83fbd681668a34598062676ff7a9da18e92c81b6c249
|
|
BLAKE2b-256 checksum How to use checksums |
dea652f1daf2cf690977b4a32b303cedd42ec52b75552a3dd0298d2396fd5dc0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for damien 0.4.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for damien-0.4.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.