
Memo is designed with a local-first, privacy-respecting security architecture.
All provider API keys (OpenAI, Anthropic, Google, etc.) are encrypted at rest using AES-256-GCM. Keys are derived from a machine-specific secret stored in the OS keyring where available, falling back to a file-based secret with restricted permissions. Keys are never logged, exported, or transmitted in plaintext.
A token-bucket algorithm enforces per-IP rate limits on the API server:
| Endpoint | Rate |
|---|---|
/api/chat |
30 requests / minute |
/api/embeddings |
60 requests / minute |
| All other endpoints | 120 requests / minute |
Burst capacity is 2x the sustained rate. Rate limit headers (X-RateLimit-*) are included in API responses.
413 Request Entity Too Large response.config.yaml, memo.db) are created with 0600 permissions (owner read/write only).~/.memo/) is created with 0700 permissions.os.CreateTemp and are cleaned up on process exit.Sync data is end-to-end encrypted:
When an agent executes tools:
The WhatsApp bridge uses a mutex to prevent concurrent message processing from the same conversation, avoiding race conditions on session state. Messages are queued and processed sequentially per chat.
As of v3.3.3, every request against a Remote Access connection (LAN or ngrok tunnel) had to present an access token — before that, anyone who could reach that address at all could read provider API keys, wipe data, or run agent commands with zero credentials. As of v3.5.5, that single-shared-token scheme has been replaced with a full authentication system, built for self-hosting on a Pi/home server/VPS/Docker but equally in effect for LAN/ngrok/Tailscale Remote Access on desktop.
Selectable via Settings or memo remote set-mode:
| Mode | Credential required |
|---|---|
none |
None — only appropriate on a fully trusted private network |
token (default once bound off-loopback) |
A per-device token, X-Memo-Token header or Authorization: Bearer |
password |
Username + argon2id-hashed password, issues a signed session |
token_password |
Either a valid token or a valid password |
A request whose actual source IP is loopback (127.0.0.0/8, ::1) is trusted in every mode, with no credential — this is keyed on the request's real origin, not on what URL the client believes it's talking to, so it only ever covers software running on the same machine (the installed desktop app talking to 127.0.0.1). A request arriving from a LAN IP is still gated normally, even from the same physical box.
memo remote revoke-device <id>) without affecting any other device. memo remote rotate-token <id> revokes and re-issues a token under the same device name in one step, for a leaked token that needs to stop working immediately.--password/--key are never required as plain CLI arguments (visible via ps, left in shell history) — omit them and every CLI command that needs one prompts for it interactively, hidden.Self-hosted Memo supports multiple accounts with admin/user roles, managed from Settings → Accounts. Every screen in the app — chat, Settings, Developer Options — goes through the same first-run setup gate and login gate rather than each screen independently tracking auth state.
A CORS origin-validation bypass in the remote-access gate (CWE-346) was found in an adversarial pass over the whole auth surface and closed in v3.5.5.
The Developer API Gateway shares this same auth system when its own "Require API Key" toggle is enabled. See Self-Hosting → Known Limitations for what's explicitly not covered yet (built-in TLS, an independent adversarial penetration test).
Two govulncheck-flagged vulnerabilities in third-party dependencies were patched in v3.3.3: an infinite-loop bug in golang.org/x/text (reachable through the WhatsApp integration's profile-picture lookup) and a related fix in golang.org/x/net.