Guard rails

One process reads untrusted text and can act as you. Here is what stands in between, and where it stops.

Before a password exists

  • Until you choose a password, the API answers nothing but the sign-in pages.
  • The first password needs a one-time setup code the backend writes to backend/.setup_code, readable only by your macOS user and deleted once the password is set. The request must also come from a page at localhost on the host Mac, but a device on your network can forge those headers: the code is what keeps someone else from claiming your Edward first.
  • Wrong codes are limited (10 per client in 15 minutes); sign-in allows 5 failed tries per client in 15 minutes, and 100 across all clients.
  • Sessions last 30 days and renew past half their life. The cookie is Secure: serve Edward over HTTPS.

Money

  • A daily spend cap ($5 over your local day by default). Background work stops first (at 60 %), then Edward’s own turns (at the cap); your own turns may run a $3 reserve past it. When the cap is reached he says so in a sentence, not an error.
  • Per-turn caps: $3 for a turn you asked for, $1 for anything else, and 60 tool rounds at most.
  • Every model call is priced and logged, and shown in the app.

What leaves

  • The egress secret scan is always on. Every message and file he sends, every shell command, every push, every URL he fetches or looks at, every search, everything typed in a browser and every argument to a custom MCP tool is scanned for keys, tokens, JWTs and private keys. A hit is refused, with only the kind logged. The exception is Anthropic’s server-side search and fetch (the default on Claude): their queries are checked only after the fact, and logged.
  • Redaction on the way in: shell output, files he views and every local tool’s result pass through the same scanner before the model sees them (the server-side search and fetch results can’t be).
  • One send. send_message is the only way he messages anyone: one recipient, a 10-minute dedupe, and a recipient rule. He may write to someone only if you asked, it is the chat the turn was asked in, they addressed him in the last 24 hours, or it is a live conversation; anyone else is your call. The app says “Sent” only when the message was delivered.
  • Your Chrome (if you turned the hands on): typing outside a search box or clicking send, post, delete and the like needs you to have asked in that turn; payments are refused in every turn.

Taint and approvals

A turn is marked tainted when it starts from untrusted content (a message, a mail, a wake’s pack) or reads some (a web page, your messages, a calendar invitation). The approval gate that can hold such a turn’s outward calls is off by default; the recipient and Chrome rules above do its job in code. Set EDWARD_TAINT_APPROVALS=shadow to log what it would have held, or on to hold each one for your Approve / Deny on Today. An approval runs the stored call once, and never past the secret scan.

His computer

His shell runs under a macOS Seatbelt sandbox: it can’t read his own credentials (the session key, the setup code, .env), write the code he runs from or what production depends on, reach his own API or databases, or launch and script other apps. Details in His computer.

It is not a containment boundary. The shell runs as your macOS user and can write your shell startup files, your LaunchAgents and Homebrew, from which programs later run as you, outside the sandbox. A prompt injection that reaches the shell (a web page, a message, a file he was pointed at) therefore has a way to run code as you. The rails make that deliberate work rather than one mistaken command. For strong isolation, run Edward as a separate macOS user (prompt G in Adapting to your Mac).

Other people

  • Friends’ pings stay off your machine: no shell, editor, Chrome or screens unless EDWARD_FRIENDS_FULL_TOOLSET=on. They can still read what Edward reads (your chats, memories, calendar) and answer from it; his prompt asks for discretion, which is a prompt, not a wall. See Listening & pings.
  • Twilio webhooks are refused without a valid Twilio signature, and then only your own numbers get a turn. Anyone else’s text is held, unanswered, with no push.
  • The household hub’s voice runs as someone in your home, not as you: it follows the recipient rule and never texts you.
  • A wake never messages you with send_message; it reaches you only through tell_owner. And a wake can’t change or delete what you said.

Known gaps

  • Alexa: inbound requests are not signature-verified. Only the skill id is checked, and an Alexa turn runs as you, so anyone who learns the id and your URL can run a turn as you. Leave ALEXA_SKILL_ID unset unless you use the skill (the route then answers 503). Adapting to your Mac has a prompt (E) that adds Amazon’s verification.
  • The widget token is owner-equivalent: it can chat as you. Keep it on your own devices and regenerate it if it leaks.
  • Postgres trusts local connections (Homebrew’s default), so any program running as you outside the sandbox can reach it. The shell itself is refused.
  • Custom MCP servers’ environment values are stored in plain text.

Emergency switches

EDWARD_EGRESS_SECRET_SCAN=off and EDWARD_COMPUTER_SANDBOX=off exist for emergencies only; the second runs his shell unsandboxed and logs every command. To stop Edward reaching anyone: turn Messages off in Listening, unset TWILIO_AUTH_TOKEN, and turn the iMessage and Push Notifications skills off.


The long form: trust-boundary.md, auth-and-pwa.md and computer.md.