- Shell 100%
|
Hugo O'Connor
c1a5b7a7dd
- quick start now clones withdone and runs install-himalaya explicitly, instead of pointing WITHDONE at a path that doesn't exist yet - add "Create your first profile" (cp -r an example + edit the 3 files) - describe what a successful pass logs and how to confirm in draft mode - fix stale Run-unattended anchor after the macOS heading rename Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| docs | docs: close install gaps for an agent following the README | |
| profiles | ||
| specs | docs: schedule via launchd on macOS (cron can't reach login keychain) | |
| users/operator | ||
| .gitignore | feat: X-DMARC-Status gate + courtesy reject notices | |
| install-himalaya.sh | chore: rename project email-agent -> email-reply-agent | |
| LICENSE | chore: add Apache 2.0 license | |
| README.md | docs: close install gaps for an agent following the README | |
| reply-bot.sh | feat: X-DMARC-Status gate + courtesy reject notices | |
email-reply-agent
Answer routine mail automatically: for each unread message from a known correspondent, a coding-agent CLI drafts a reply in the voice of the workflow that owns the address, and the script — never the agent — sends it and marks the message read. Strangers are never answered.
Specified in specs/SPEC-001-email-reply-agent.md under
the USDD Agent Protocol (PROTO-001). Status: implementing.
🛑 Read
docs/SECURITY.mdbefore deployingThis tool acts on instructions derived from email, and email
From:is forgeable. A spoofed sender that passes your allowlist runs the agent on the attacker's instructions. The allowlist is worth nothing unless your receiving domain enforces anti-spoofing DNS (DMARCp=reject). The authentication gate is the only thing protecting you — one mistake is host compromise. This is not optional reading.
How it works
A mailroom with named desks. Each desk is a profile (a directory under profiles/) with
its own clerk persona, its own allowlist of correspondents it will answer, and its own
send/draft policy. The router drops each unread letter on the first matching desk; the clerk
(the agent) drafts; the mailroom (the script) posts it.
himalaya IMAP ┌─────────── reply-bot.sh (effectful shell) ────────────┐
──unread──────────────────────▶│ Router ──▶ Approved? ──▶ Agent (claude|codex) │
envelope list -o json │ (profile gate under withdone, drafts a │
│ match) reply file + sentinel) │
│ │ drafts │
│ himalaya template send ◀────────┘ (header check) │
└──────────────────── flag add seen ────────────────────┘
trust boundary: every email body + every himalaya JSON is UNTRUSTED input
The single irreversible action (send) stays in deterministic shell, so no email body can induce a send — the agent has no send capability (ADR-001 / NFR-004).
Quick start
Prerequisites (resolved via PATH; cron-hardened in the script):
himalaya— IMAP/SMTP CLI; run./install-himalaya.shto install and configure an accountjq— JSON parsingwithdone— the headless agent launcher + completion sentinel (sibling repo)- A coding-agent CLI per profile:
claude(Claude Code) and/orcodex coreutilsfortimeout/gtimeout(brew install coreutils) — so a wedged agent can't hold the lock
# 1. Fetch the withdone launcher. The default WITHDONE path is ../withdone/withdone,
# so cloning it as a sibling means no env var is needed. (See withdone's own
# README for any install step; point elsewhere with `export WITHDONE=/path/to/withdone`.)
git clone https://codeberg.org/anuna/withdone ../withdone
# 2. Install himalaya and configure a mailbox (prompts for address/host/password).
./install-himalaya.sh # see docs/CONFIGURATION.md → Set up a mailbox
# 3. Stay on your Claude subscription, not the metered API (ADR-005).
unset ANTHROPIC_API_KEY
export CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)" # long-lived, TTY-free
# 4. Create at least one profile (next step), then run one pass.
./reply-bot.sh
Define a profile in docs/CONFIGURATION.md — the fastest start is
cp -r profiles/example-support profiles/<name> and edit the three required files. Each profile
must set SEND_MODE — start with draft (saves replies to your Drafts folder for review)
and switch to send once you trust it.
A successful pass logs Loaded N profile(s): ..., one line per message (drafting reply →
SEND_MODE=draft: saved to Drafts), and ends with Done. — all on stderr. In draft mode it
never sends; confirm the result in your Drafts folder before switching to send. With no
matching/approved mail it simply logs the skips and exits cleanly.
⚠️ Running unattended on macOS: use
launchd, notcron. Acrontabjob runs outside your GUI login session and fails on permissions — it can't reach the login keychain (himalaya's password lookup returnsexit 44) and is subject to TCC restrictions. Schedule with a LaunchAgent instead; seedocs/CONFIGURATION.md.
Documentation
| File | What's in it |
|---|---|
docs/SECURITY.md |
Threat model, required DNS hardening, per-message DMARC gate, sender-DNS audit. Read first. |
docs/CONFIGURATION.md |
Mailbox setup, profile layout & profile.conf keys, environment overrides, running unattended (cron / launchd). |
specs/SPEC-001-... |
The reply-mode spec: requirements, contracts, ADRs, verification. |
specs/SPEC-002-... |
The planned act-on-codebase mode (draft status) and its threat model. |
Layout
reply-bot.sh— the effectful shell: discovers unread mail per scan target, routes, gates on the allowlist, launches the agent underwithdone, verifies the agent preserved the reply headers (From/To/Subject/In-Reply-To) byte-for-byte before sending (REQ-005 / CON-004), sends or drafts, then marks read only on success (REQ-007).install-himalaya.sh— installs himalaya and writes one IMAP/SMTP account (keyring auth).profiles/— one directory per workflow, co-locating persona, allowlist, skills, knowledge.specs/— the zetl spec vault (requirements, contracts, ADRs).
Development
bash -n reply-bot.sh # syntax check
shellcheck reply-bot.sh # lint (if installed)
zetl check --dead-links # spec vault hygiene
The header-preservation and lock-liveness logic are pure helpers (header_value,
verify_headers, acquire_lock, touch_lock), unit-testable in isolation against fixtures —
see the spec's Verification section (TEST-001..012, TEST-injection). A committed tests/
harness and live-mailbox verification remain the open gates before status moves to
implemented.
License
Licensed under the Apache License 2.0. Copyright 2026 Hugo O'Connor. The warranty
disclaimer and liability limit (sections 7–8) are load-bearing given the security model — you
run this at your own risk. See docs/SECURITY.md.