← Try the mock

Butler: how it's built

An assistant whose authority stays on a Mac at home.

Every part below is built and tested against fakes. It has never been connected to a real account or run on the home Mac (readme, line 5; app readme, lines 3-6). Gmail is the only connector in the first release.

Architecture

Who can touch what

The phone never talks to Google. A redaction sidecar, push-based Gmail intake and a messaging bridge are planned, not drawn.

Who can touch whatPhoneHome MacWorkersOutsideiPhone appPoll schedulerGatewayRouter and redactorLocal storeToken vaultClaude workerCodex workerGmailGoogle sign-inJev reviewerApple push123456789101112
  1. iPhone app → Gateway: over the private network; approvals with Face Identifier — A label that picks out one thing, such as an account or a record. In Face ID, Apple's face recognition, the phone confirms it is you before it approves something. (request, sync)
  2. Poll scheduler → Gmail: read new mail every five minutes (timer, async)
  3. Gateway → Router and redactor: rank and redact, in code, no model (request, sync)
  4. Gateway → Local store: draft held here; Gmail untouched (request, sync)
  5. Gateway → Token vault: account token, key in the Keychain (request, sync)
  6. Gateway → Google sign-in: connect, then refresh a minute before expiry (request, sync)
  7. Gateway → Claude worker: task through a root-owned launcher (request, async)
  8. Gateway → Codex worker: a task, or a second opinion on the other's work (request, async)
  9. Gateway → Jev reviewer: first-pass review of redacted text (request, sync)
  10. Gateway → Gmail: send or label after approval or a promoted rule (request, sync)
  11. Poll scheduler → Apple push: at most one push a cycle (push, async)
  12. Apple push → iPhone app: notification (push, async)

A worker task, start to review

Worker output is checked and redacted before Jev sees it. Jev can only escalate, never clear a block.

A worker task, start to reviewGatewayWorker accountReviewRoute the taskStage the taskRoot-owned launcherWorker processRe-hash and validateRedact the outputJev first passSecond opinionReview screenHeld for you1234567891011
  1. Route the task → Stage the task: pick the worker and write the task files (request, sync)
  2. Stage the task → Root-owned launcher: sudo into a separate account (request, async)
  3. Root-owned launcher → Worker process: a short-lived process (request, async)
  4. Worker process → Re-hash and validate: streamed result (event, async)
  5. Re-hash and validate → Redact the output: inputs unchanged, result valid (request, sync)
  6. Redact the output → Jev first pass: redacted text only (request, sync)
  7. Jev first pass → Review screen: no concern: no review line (event, sync)
  8. Jev first pass → Second opinion: escalate (request, async)
  9. Jev first pass → Held for you: escalated to a hold, or no second opinion available (event, sync)
  10. Second opinion → Review screen: no objection: 'A second model reviewed this.' (event, sync)
  11. Second opinion → Held for you: objection or no answer (event, sync)

Wiremocks

A mock-up of a screen — Here, a static picture of a screen, drawn to show layout and flow, not the working app. — a static picture, not the real app

The app, screen by screen

The design A clickable, fully scripted mock of an app — Built before the app to pin its behaviour; not the HTTP-stubbing tool of the same name. with made-up mail. The sandboxed frame blocks storage, so the theme toggle does not apply and demos start over.

A mock-up of a screen — Here, a static picture of a screen, drawn to show layout and flow, not the working app. — a static picture, not the real app

Review and pairing

A drafted reply as the app shows it, reviewed and held, and the pairing screen, drawn with made-up mail.

Review

  • A second model reviewed this.

    Goal: reply to Recruiting team (hiring.example)

    I drafted a reply and kept it here. Nothing was sent.

    Open the draft it wrote

  • Review concern: this may be following instructions written in the email itself rather than yours.

    Held for you to look at first.

    Goal: reply to Careers team (jobs.example)

    Open the draft it wrote

Second opinion (no objection): the reply matches what you asked for and adds no new commitment.

Thursday afternoon call
To
Recruiting team (hiring.example)
Bcc
none
Subject
Thursday afternoon call

Approving binds this exact version. Any edit afterwards needs reviewing again.

Approve with Face Identifier — A label that picks out one thing, such as an account or a record. In Face ID, Apple's face recognition, the phone confirms it is you before it approves something.DeclineDiscard

Pair this device

Code from the Mac

4 1 7 2 9 3

Name this device

My phone

Pair with Face Identifier — A label that picks out one thing, such as an account or a record. In Face ID, Apple's face recognition, the phone confirms it is you before it approves something.

The Mac prints a six-digit code; the phone asks for it, then for Face Identifier — A label that picks out one thing, such as an account or a record. In Face ID, Apple's face recognition, the phone confirms it is you before it approves something..

Running cost

ResourceFigureStatusSource
Gmail reads while idle576 quota units per day per accountDerived
see inputs
Inputs: one poll every 300 seconds (configured, config module line 75) = 288 a day × 2 units a poll (implementation spec, line 587) = 576
Read on 2026-10-17
Gmail, each new message20 quota unitsPublished price
implementation spec, line 587
Read on 2026-10-17
Conflicts with Google's Gmail usage-limits page (60 quota units for a full-format read, read on 2026-10-17): The page lists messages.get at "20 (60 when format is full, raw, or html)", and the connector asks for the full format (gmail connector, line 157). The page is newer and governs the real cost; this row keeps the project's figure because the repo is read first.
Gmail, each send100 quota unitsPublished price
implementation spec, line 587
Read on 2026-10-17
Gmail per-user limit6,000 quota units per minutePublished price
implementation spec, line 587
Read on 2026-10-17
Gateway pacing5,000 quota units per minuteSet in the project's code
scheduler module, line 70
Read on 2026-10-17
Claude worker runs40 per daySet in the project's code
router module, line 42
Read on 2026-10-17
Codex worker runs20 per daySet in the project's code
router module, line 42
Read on 2026-10-17
Jev input tokens$0.042 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokensPublished price
implementation spec, line 590
Read on 2026-10-17
Jev output tokensfreePublished price
implementation spec, line 590
Read on 2026-10-17
Jev requests60 per daySet in the project's code
reviewer module, line 381
Read on 2026-10-17
Jev largest test request709 input tokensMeasured
implementation spec, line 590; session log 2026-09-23, line 112
Read on 2026-10-17
Jev at the cap, test-sized requests~$0.0018 per dayDerived
see inputs
Inputs: 60 a day (configured) × 709 tokens (measured) × $0.042 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. (published) = $0.00179
Read on 2026-10-17
Push notificationsat most 24 per daySet in the project's code
scheduler module, lines 47-49 (one per cycle, one-hour cooldown)
Read on 2026-10-17
Claude and ChatGPT subscriptions—Unknown
implementation spec, lines 588-589, name the plans but no price
Read on 2026-10-17

No cloud hosting: it runs on the owner's Mac over Tailscale's free personal plan (implementation spec, line 591).

Design decisions

  • One machine holds all authorityThe phone holds a session and one device credential, never an account token. A worker has no path to an account.
  • Routing and redaction are code, never a modelA model on a 2018 Intel Mac runs on the Central processing unit — The main chip that executes a computer's instructions. alone and is too slow, and no model output may gate a security decision. A redaction sidecar is planned, not built.
  • Approval is bound to the whole Everything an action would send — The account, sender, recipients, subject and body taken together.An approval is single-use, expiring and re-checked before dispatch, so it never means approving what a screen showed a moment ago.
  • A home-screen app over Tailscale Serve, never FunnelNo App Store or developer account is needed, and a deploy is a restart. Serve gives the secure address without a public name; Funnel would expose the app.
  • Workers live in separate Operating system — The core software of a computer, which also keeps one user account's files and logins apart from another's. accountsThe Operating system — The core software of a computer, which also keeps one user account's files and logins apart from another's. enforces the credential boundary. The other vendor gives a second opinion when Jev escalates or a rule is promoted. Jev can never clear a block.

Open questions

  • Claude and ChatGPT subscriptions: the spec names the plans but gives no price, so the monthly cost is unknown
  • Gmail: whether the Application Programming Interface — A defined way for one program to request data or actions from another. costs money at this volume is not recorded; the repo states quota units only
  • Jev tokens on real mail: the spec expects larger requests than the test, and none has been measured
  • Mail volume: messages a day are unknown, so the per-message quota cost has no daily total
  • Disk: store growth is marked verify in the spec
  • Power: the always-on laptop's electricity is not priced