install
Two ways onto your Mac: a one-line install script, or the .dmg by hand. Either way you end up with Twin.app in Applications.
quick install
curl -fsSL https://raw.githubusercontent.com/woustachemax/twin/main/scripts/install.sh | bash
Downloads the latest release's .dmg, mounts it, copies Twin.app into /Applications, and clears the quarantine flag so Gatekeeper doesn't block the first launch. Safe to re-run: an existing install gets replaced, not skipped.
manual download
Not keen on piping a script into bash? Grab the .dmg from the download section on the landing page instead, and drag Twin.app into Applications yourself.
the gatekeeper warning
Twin ships ad-hoc signed, not notarized by Apple. On a Mac other than the one that built it, the first launch after a manual download gets blocked: "Twin can't be opened because Apple cannot check it for malicious software." Click Done, then:
- Open System Settings → Privacy & Security.
- Scroll to the Security section. You'll see "Twin was blocked to protect your Mac" with an Open Anyway button next to it.
- Click Open Anyway, then confirm Open (Touch ID or your password may be asked for).
Or skip all of it: xattr -cr /Applications/Twin.app in Terminal after dragging Twin in. The quick install script does this step for you automatically.
building from source
git clone https://github.com/woustachemax/twin.git cd twin python3 -m venv .venv source .venv/bin/activate pip install anthropic duckdb python-dotenv pynput pyobjc-framework-Cocoa pyobjc-framework-Quartz pyobjc-framework-Vision pyobjc-framework-EventKit pyobjc-framework-Speech pyobjc-framework-AVFoundation python3 buddy.py
That's the core install. You'll need Python 3 with Tk (the python.org installer includes it, or brew install python-tk). Add pip install pypdfium2 torch transformers sentencepiece protobuf Pillow if you also want /ingest. It pulls in torch, so it's a much bigger download, and it's why the downloadable app leaves it out entirely.
first run
The first time Twin starts, or any time ~/.twin/config.json is missing or setup wasn't finished, it opens a setup window before the chat widget. Four steps and a summary:
- Provider. Pick Anthropic, OpenAI, Google Gemini, or xAI, and paste your API key. Twin checks it with one small test request, then saves it to your macOS Keychain. Wrong key, out of credits, or the wrong provider: it says so and lets you try again.
- Name. What Twin should call you. Filled in with the first name from your Mac account.
- Buddy. Pick one of the five personas. Twin is selected by default.
- Permissions. Buttons for Calendar, the hotkey, and Messages. macOS only prompts when you click one. This page can be skipped.
- Done. A summary of your choices. Twin creates your database at
~/.twin/twin.duckdbhere.
Your API key is not saved in a file. It goes straight into the macOS Keychain. Close the setup window before finishing and Twin quits; it picks up at step 1 next time, with your provider and key filled back in.
permissions
Grant these from the setup window's permissions page, or later in System Settings. macOS applies a permission change on relaunch, not immediately. Quit and reopen Twin after granting one for it to take effect.
| permission | where | without it |
|---|---|---|
| Full Disk Access | System Settings → Privacy & Security → Full Disk Access | Twin reads no Messages and silently ingests nothing, so spending questions come back empty. |
| Screen & System Audio Recording | System Settings → Privacy & Security → Screen & System Audio Recording | screencapture still succeeds but returns only the wallpaper, so Twin answers vaguely instead of erroring. |
| Microphone, Speech Recognition | Allow when prompted, or System Settings → Privacy & Security → Microphone / Speech Recognition | Voice input (hold ⌘ ⇧ V) doesn't work. |
| Accessibility | System Settings → Privacy & Security → Accessibility | The global hotkey (⌘ ⇧ space) does nothing. |
| Calendars | Allow when prompted, or System Settings → Privacy & Security → Calendars | Twin can't mention what's coming up on your calendar. |
Twin keeps working without any of these. It says in the widget which feature is off and where to turn it on.
commands & hotkeys
Everything Twin responds to, typed or held.
| action | how |
|---|---|
| Show or hide Twin | ⌘ ⇧ space from anywhere |
| Ask something | Type in the box and press Return |
| Move the widget | Drag it |
| List personas | /persona, then click one to switch |
| Switch persona | /persona <key>, for example /persona gengar |
| Change provider or key | /setup |
| Catch up on today's Messages | "what did I miss?", "catch me up" |
| Ask about your screen | "what am I looking at?", "what's on my screen?" |
| Look up an SEC filing | "Tesla's last 10-K", "summarize Apple's most recent 8-K" |
| Read a document | /ingest <path>, for example /ingest ~/Downloads/receipt.jpg |
| Stop referencing that document | /forget |
| Check what would be redacted before sending | /redact-test <text> |
| Talk instead of typing | hold ⌘ ⇧ V, or hold the dot by the input field |
| Turn spoken replies on or off | /voice on, /voice off (off by default) |
personas
Pick one during setup, or switch anytime with /persona <key>. Commands take the key, not the display name.
| key | name | character |
|---|---|---|
twin | Twin | Warm, cheerful, easygoing. The default. |
gengar | Shade | Mischievous shadow-ghost, sly and teasing. |
ember | Ember | Bright, energetic spark who hypes you up. |
calm | Luna | Gentle, patient, soothing. |
plain | Assistant | Neutral and professional, no quirks. |
Luna is the only one that resizes: drag the grip in its bottom-right corner, anywhere between 360×350 and 640×720. It opens at 420×460 and remembers the size you leave it at until you quit. Every other persona stays a fixed 360×350 widget with no grip.
providers
Four supported providers, one client each behind a common interface. Switch anytime with /setup.
| provider | get a key from |
|---|---|
| Anthropic | console.anthropic.com/settings/keys |
| OpenAI | platform.openai.com/api-keys |
| Google Gemini | aistudio.google.com/apikey |
| xAI | console.x.ai |
Keys are stored in the macOS Keychain as "Twin <provider> API key", never in a file on disk.
your data
Everything Twin keeps lives in ~/.twin/, outside the repo:
~/.twin/twin.duckdb: parsed transactions, the full raw SMS text, and anything loaded with/ingest.~/.twin/config.json: your name, persona, provider, and setup state.
Your API key sits separately, in the macOS Keychain. There are no accounts, no sync, no analytics, and no Twin server. Everyone who installs Twin gets their own database, created in their own home folder.
Every insert into twin.duckdb also writes a row to its access_log table, recording what was read, when, and by which part of Twin.
reset to a clean state
Delete ~/.twin and Twin runs setup again on next launch, as if freshly installed:
rm -rf ~/.twin
uninstall
- Quit Twin, then drag
Twin.appout of Applications. - Delete
~/.twin(your database and config). - Remove the Keychain entry for whichever provider you used. For Anthropic, that's service
"Twin Anthropic API key", account"twin":
security delete-generic-password -s "Twin Anthropic API key" -a twin
faq
why does Twin refuse to tell me my exact spending?
Because the exact numbers never reach it. Before any chat request goes out, your five most recent transactions get turned into vague phrases like "spent money on food earlier this week." Amounts, balances, account numbers, and raw SMS text are stripped before they leave your Mac, not after.
why does an SEC filing come back with exact figures when my own data doesn't?
A filing is a public company disclosure, not personal data, so scrub_system() passes its excerpt through unredacted. Your Messages are yours, so they get vagued into fixed phrases first, and anything you /ingest gets redact.py's full PII pass. Same pipeline, different rules for public disclosure versus personal data.
why doesn't /ingest work in the downloaded app?
Its dependencies (Pillow, pypdfium2, torch, transformers, sentencepiece, and protobuf) aren't bundled into the .dmg. Together they'd take the app from about 150 MB to nearly 1 GB for a feature most people won't use. Run Twin from source (see Install) to get /ingest; everything else works the same either way.
why does an SEC lookup ask for a contact first?
SEC EDGAR requires a contact string in the User-Agent header on every request, as part of its fair-access policy. Set it once with /setup edgar or the SEC_EDGAR_CONTACT environment variable, and lookups stop asking.
does anything leave my Mac?
Only what you explicitly send: your chat message, your name, today's calendar titles, and a vague transaction summary go to whichever AI provider you picked. Voice, the digest, and nudges are all built and matched on-device and never touch a provider. There's no Twin server to send anything to in the first place.
troubleshooting
| problem | fix |
|---|---|
| Setup says the key didn't work | Check you copied the whole key and picked the matching provider. Out of credits? Check billing with that provider. |
| Twin says your provider didn't accept the key | Type /setup and paste a new key. |
| Setup shows up again on launch | Setup wasn't finished, or ~/.twin/config.json was removed. Finish the steps once. |
| Twin can't read Messages | Give Full Disk Access to Twin (or your terminal), then restart Twin. |
| Hotkey does nothing | Allow Twin (or your terminal) under Accessibility, then restart Twin. |
| "Calendar access isn't allowed yet" | Use the Calendar button on setup, or turn on full access under Privacy & Security → Calendars. |
| "I can only add calendar events right now" | Twin has add-only access. Switch it to Full Access under Privacy & Security → Calendars. |
| Twin can't see your screen | Allow Twin (or your terminal) under Screen & System Audio Recording. |
| Permissions stopped working after rebuilding | Each rebuild gets a new signature. Turn Twin back on in each Privacy & Security list. |
| Twin knows nothing about your spending | Check Full Disk Access, and make sure TWIN_DB_PATH (if set) matches for both the pipeline and Twin. |
| Pipeline inserts 0 transactions | It only reads the last 200 messages, and only bank SMS in the supported formats count. |
limitations
- macOS only.
- The SMS parser targets Indian bank and UPI message formats. Other formats are ignored until someone adds patterns for them.
- The pipeline reads only the 200 most recent messages per run.
- Twin only looks at the five most recent transactions when chatting.
- Each chat message is answered on its own. Twin doesn't remember earlier turns.
- Answers about your screen are vague on purpose, since only a one-sentence summary is sent.
- One ingested document is active at a time. Loading a new one with
/ingestreplaces it; older ones stay stored but out of the conversation until re-ingested. - Filing lookup only searches EDGAR's "recent" filings window, roughly the last year. An older filing of that type comes back as not found even if it exists.
- Donut is fine-tuned on receipts, so extraction quality on other kinds of document photos is weaker.
Twin.appships ad-hoc signed, not signed with a paid Developer ID or notarized. That's a deliberate call for a portfolio project, not a gap to fill later.
Still stuck? Open an issue on GitHub.