Remote access
Use buddylist from your phone, wherever you are. Your phone opens a small app made for it, served by buddylist on your Mac through Tailscale, a private network between your own devices. There is no server of ours in between: your conversations stay on your Mac, and Tailscale encrypts the traffic between your devices. Keep the app running on the Mac while you use it.
iPhone on 5G or any Wi-Fi Your Mac
┌──────────────────────┐ Tailscale (HTTPS) ┌───────────────────────────────┐
│ the phone app │══════════════════════▶│ tailscale serve (https :443) │
│ (Safari, or on the │ │ └▶ buddylist on 127.0.0.1 │
│ Home Screen) │◀── replies, prompts ──│ the same conversations │
│ Tailscale app (on) │ │ as its windows │
└──────────────────────┘ └───────────────────────────────┘
Set it up
Install Tailscale on your Mac and your phone, and sign in to the same account on both.
In buddylist, open Settings → Remote access and Set password…. Use at least 12 characters. Only a salted scrypt hash of it is kept, in
remote.jsonin buddylist's data folder, readable by your user only.Turn on Use buddylist from your phone.
Under Tailscale, choose Set up Tailscale Serve. buddylist shows the exact command and runs it only after you confirm:
tailscale serve --bg --https=443 http://127.0.0.1:6767The first time, Tailscale may ask you to turn on HTTPS certificates for your tailnet; the link it prints opens from Settings. You can also copy the command and run it yourself. Stop serving turns it off again (
tailscale serve --https=443 off). If something else already uses port 443 on Serve, buddylist uses 8443.Under Pair a phone, choose Show code. On your phone, with Tailscale connected, scan the QR code with the camera. Or open the address shown, such as
https://your-mac.your-tailnet.ts.net, and type the code.To open it like an app, add it to your Home Screen: in Safari, tap Share, then Add to Home Screen. On Android, Chrome offers Install. The phone app says how until you add it or tap Not now. The copy on the Home Screen keeps a sign-in of its own, so it asks for a code once more.
The status line under the switch says the address to open, or why it isn't running, such as another program using port 6767.
Keep this Mac awake while remote access is on (on at first) stops the Mac from sleeping by itself while remote access is on, so your phone can reach it. A closed lid still puts a MacBook to sleep unless it is plugged in with an external display.
The phone app
It is drawn in the professional look, in the palette of your theme, by day or at night as your phone is.
Home
┌─────────────────────────────┐
│ buddylist ● Mac Sign out │
├─────────────────────────────┤
│ WAITING FOR YOU 3 │
│ api-worker │
│ [Permission] Wants to run… │
│ docs-bot │
│ [Question] Asks 2 questions │
│ web-fixer Finished │
│ WORKING 1 │
│ build-runner Reading app.js│
│ ▸ EVERYONE ELSE 12 │
└─────────────────────────────┘
- Waiting for you lists the contacts that asked you something first, saying what: a permission, a question, a plan or a change to the project's instructions. Then the ones that finished with something you haven't read, with how much.
- Working lists who is at work, and what each is doing.
- Everyone else is folded away while anyone is above it. A project's manager and its project manager wear a badge. Archived contacts aren't listed.
Tap a contact to open its conversation. One that waits for you opens at its card, with what led to it above. The phone's Back goes back to Home. ● Mac at the top is green while the phone reaches your Mac, and amber while it tries again.
A conversation
- What was said, with replies in Markdown as they stream in, a line for each tool call with ✓ or ✗, and how each turn ended. A long conversation opens at its newest part, and Show earlier above it has the rest.
- Permissions: the command, change or address to look at, then Decline, Always allow where it is offered, or Allow.
- Questions: pick an option, or tick as many as you like where a question takes more, or write your own answer, then Send answers. Skip lets the agent go on with its own picks.
- Plans: read the plan, then approve it in one of the ways the agent offers, such as Yes, auto-accept edits, or No, keep planning, with what should change.
- Project instructions an agent wants to save: read the new text, then Save or Don't save.
- Saying something: type and tap Send, or press Return. A message sent while the agent works waits its turn, marked Queued, with Send now to have it read now and Cancel to take it back into the box.
- Stop stops whoever is at work, in one tap, as the conversation window's Stop does. With a keyboard, Esc twice does the same.
- A conversation you have open on the phone counts as read, on the Mac too.
- Managers and project managers are talked to as anyone is.
A card answered on the Mac, or on another phone, goes away on yours too. A lost connection says so at the top, and once it is back the page reads everything again. A message interrupted by a lost connection is not sent twice: check the conversation before sending it again.
What stays on the Mac
Adding or changing contacts, Settings, message history and search, pictures and voice clips, files, changes, terminals, the Run button, browser panes, inviting guests and editing a plan stay in the Mac's windows. The phone app shows none of them, rather than buttons that don't work.
Signing in
- A code pairs a phone. It is shown only in Settings on the Mac, works once and for five minutes, and is cancelled after five wrong tries. The QR code carries it after the
#in the address, so it never reaches Tailscale or a log; the page takes it out of the address at once. - The password signs in a browser that can't scan the code. Changing it signs out every device, unless you untick that.
Each phone or browser that signs in is a device, listed under Signed-in devices with its name (such as iPhone · Safari, or iPhone · Home Screen), how it came (Tailscale, with its tailnet address and login, or this Mac) and when it was last used. Your Mac says when a new device signs in. Sign out ends a device at once and sends its open pages back to the sign-in page; Sign out all ends every one. Sign out in the phone app ends that one device. A device stays signed in across restarts until it goes unused for 30 days.
Safety
- buddylist listens on 127.0.0.1 only. Nothing else on your network reaches it; Tailscale Serve, on the Mac, passes on what comes from your tailnet.
- Tailscale Funnel would put buddylist on the public internet. buddylist won't turn on while Funnel is on for it, and says how to turn Funnel off (
tailscale funnel --https=443 off). If Funnel is turned on later, or a request comes through Funnel, remote access turns itself off and signs every device out. - Others on your tailnet can reach the sign-in page, but need a code shown on your Mac or the password. Wrong passwords and codes are limited to 5 from one address and 20 in all within 15 minutes.
- Cookies are HttpOnly and SameSite=Strict, and only sent over https behind Tailscale. Every form post and WebSocket must come from buddylist's own page (its Origin), and requests for any other host name are refused, so other websites can't use your session.
- A signed-in device can read every conversation, send messages and answer permission requests, as you can in the app. Agents run on the Mac with each contact's own permissions. A device can't change settings, pair other devices or see the device list. If you lose a phone, sign it out in Settings.
- Before you sign in, only the sign-in page is there, with the app's name and icons for a Home Screen. The phone app and everything it loads come after.
How it works
src/main/remote/access.js turns the host on and off as Settings says, and keeps the password, the devices and whether it is on in remote.json. host.js is a Node HTTP server on 127.0.0.1:6767. Signed in, / is the phone app (src/renderer/phone/), and only it and what it loads are served: the shared code, the professional look's colours and the packages that draw Markdown. The Mac's own windows are not. The host checks Host and Origin, refuses Funnel requests (Tailscale marks them Tailscale-Funnel-Request), limits sign-in attempts and connection sizes, and carries requests and events over a WebSocket. devices.js keeps a SHA-256 of each device's token, never the token. pairing.js makes the codes. tailscale.js reads tailscale status --json and tailscale serve status --json, and runs its fixed commands with no shell. Static files are checked after resolving symlinks.
The phone app runs the app's own preload over remote/network-ipc.js, so it has the same window.messenger as a window. remote/routes.js forwards only an explicit list of channels, the ones the phone app uses; none of Settings' remote requests are on it. A page hears what happens in the conversation it opened last. X-Forwarded-For, X-Forwarded-Proto and Tailscale-User-Login, which Tailscale Serve sets, are only trusted from a loopback peer.
BUDDYLIST_REMOTE_PORT picks another port for a test run (0 for any free one).
node --test test/remote-*.test.js test/phone-app.test.js
npm test
npm run lint
For a real-app check, have Playwright's WebKit (npx playwright-core install webkit) and use the development Electron executable (or a packaged app executable):
node scripts/check-remote-ui.js node_modules/electron/dist/Electron.app/Contents/MacOS/Electron dist/remote-check .
This creates and removes an isolated demo profile with a few contacts at work, turns remote access on in Settings, and pairs an iPhone-sized WebKit with the code. It checks Home's sections and their order, a reply, a permission, questions, a plan, a queued message's Cancel and Send now, Stop, marking read, a manager, a dropped connection, and signing out from the phone and from Settings, and takes a still of each screen by day and at night. BUDDYLIST_CHECK_VIDEO=1 records a video too. Agent replies are simulated; no paid agent account is needed.