Self-hosted · macOS · 42 actions · free tier

Run Your Mac From Your Phone

A remote control for a Mac that keeps every file, every secret and every photo on the machine itself. Four ways in — a chat bot, Apple Shortcuts over SSH, a web app on your phone, and an encrypted relay for when you are out — all sharing one core that knows nothing about any of them.

Your Mac

Where everything lives

All 42 actions, the database, the transcription. Nothing leaves the machine unless you ask it to.

~3,300 lines$0 / month
The relay

Optional, and blind

128 lines that forward bytes they cannot decrypt. Off by default; nothing is stored there.

end-to-end encrypted$0 / month

This started as a Telegram bot that could check a battery level. It became something more useful once the interesting question stopped being what can it do and turned into how do I reach it — from a chat, from a shortcut, from a browser, from another country. What follows is the architecture that made those four answers possible without writing the features four times, and the things that went wrong on the way.


The shape of it

Everything hangs off one decision: the core does not know how it is being called. Each action is a plain function that takes text arguments and returns a result — some text, maybe a file, and a flag saying what kind. It imports no networking library at all, which is verified by a test that loads it with the chat library blocked from importing.

How you reach it Chat bot · Shortcuts over SSH · Phone web app · Encrypted relay

Four front ends. Each one translates its own world into a call, and turns the result back into whatever it can display.

↓
The core — knows nothing about the above 42 actions, standard library only

Battery, disk, files, camera, clipboard, reminders, power. Plain functions returning a structured result.

↓
What it keeps A SQLite file in Application Support

Reminders and notes survive restarts. Anything still pending is rescheduled at startup, and anything that fell due while the machine was off is reported instead of swallowed.

The payoff is that adding a fifth way in means writing a transport, not touching a single action — and that a bug fixed in one place is fixed everywhere. The chat front end shrank from 1,561 lines to 629 when the actions moved out of it, because what was left was only the part that is genuinely about chat: keyboards, file uploads, voice notes.

Four ways in

A chat bot

Familiar, works from any device, free push notifications. The weak point is privacy: bot conversations are not end-to-end encrypted, and this thing sends webcam photos and screenshots.

good for notifications · weakest privacy

Shortcuts over SSH

Apple Shortcuts ships a "run script over SSH" action. One shortcut per command, on the home screen or by voice, and they sync to every device through iCloud. No third party involved at all.

most private · home network only

A web app the Mac serves

The machine serves a small mobile page with every action as a button. Add it to the home screen and it behaves like an app; photos and screenshots render right in the page.

best day to day · instant

An encrypted relay

For when you are not home. A tiny server forwards messages it cannot read, because the phone and the Mac encrypt to each other. Off unless you switch it on.

works anywhere · ~1s per command

Everything it can do

Generated from the source, so this list cannot drift from reality. Nothing here reaches outside the machine except the three that ask the internet a question: weather, exchange-free location lookup, and the speed test.

System status

/bateria
Battery level, charging state and time remaining
/donde
Rough location from the public IP, with a map link
/espacio
How much free disk space is left
/procesos
The processes eating the most CPU right now
/red
Connected Wi-Fi network, local IP and public IP
/sistema
Uptime, CPU load, memory pressure, disk and battery in one go
/trabajos
What is running in your terminals and what is waiting on you
/velocidad
Internet speed test: download, upload, latency

Files

/buscar <palabra>
Searches the whole machine by filename through Spotlight
/descargar <url>
Pulls a file off the internet into the Downloads folder
/descargas [nombre]
Lists Downloads, or sends a file from it
/desktop [nombre]
Lists the Desktop, or sends a file from it
/docs [nombre]
Lists Documents, or sends a file from it
/imprimir <nombre>
Sends a document to the default printer
/zip <nombre>
Compresses a file or a whole folder and sends the archive back

Screen and camera

/foto
Takes a still photo through the webcam
/pantalla [doc]
Captures whatever is on the monitor right now
/video [segundos]
Records a webcam clip with sound, 3 to 60 seconds

Controlling the machine

/abrir <algo>
Opens a website, a file or an application
/apps
Lists every application currently open
/atajo [nombre]
Lists Apple Shortcuts, or runs one by name
/cerrar <app>
Quits a running application by name
/copiar <texto>
Places text onto the clipboard, ready to paste
/decir <texto>
Makes the machine read text aloud through its speakers
/musica [play|pausa|siguiente|anterior|que]
Drives the music player: play, pause, skip, now playing
/notificacion <texto>
Pops a native notification banner on the screen
/portapapeles
Sends back whatever text is on the clipboard
/tarea <texto>
Creates a task in Reminders, which syncs to the phone
/volumen [0-100|mute]
Sets output volume, or mutes it

Power and safety

/apagar si
Shuts the machine down — needs an explicit confirmation
/bloquear
Turns the display off, locking the machine if set to
/dormir
Puts the machine to sleep
/reiniciar si
Restarts the machine — needs an explicit confirmation
/sonar
Loud alarm for fifteen seconds, then restores the volume

Audio

/acta
Records a meeting and returns audio plus a written transcript
/grabar
Starts recording the room through the microphone
/parar
Stops the recording and delivers the result

Daily life

/clima [ciudad]
Current weather conditions
/nota <texto>
Saves a quick note to the local database
/notas [vaciar si]
Shows recent notes, and can clear them
/recordar <cuando> <texto>
Sets a reminder in minutes, hours, or at a clock time
/recordatorios [borrar N]
Lists pending reminders, and deletes one by number

No command matches that.


The relay, and why it cannot read anything

Reaching the machine from your own network is easy. Reaching it from a train is where most designs quietly give up their privacy: the usual answer is a tunnel that terminates encryption on someone else's server, which then sees every screenshot you ask for.

The alternative is to keep the server but make it blind. The phone and the machine already share one secret — the key you scanned to pair them — so both sides derive an encryption key from it and talk through a forwarder that only ever sees ciphertext:

key     = HKDF-SHA256(shared secret, info: "relay-v1")
mailbox = base64url(SHA256(shared secret + "mailbox-v1"))
message = base64url(nonce ‖ AES-GCM(key, payload) ‖ tag)

The mailbox is how the relay knows which two devices a message belongs to, and it is a hash: useless for decrypting anything, and unguessable without the secret. The server stores nothing beyond a couple of minutes, and every route it exposes takes and returns opaque bytes.

One detail made this pleasant to build: the encryption primitives line up exactly across Swift and the browser. A key derived with CryptoKit and one derived with WebCrypto come out byte for byte identical, so the phone encrypts in JavaScript and the Mac decrypts in Swift with no glue and no third-party crypto library. That also solved a constraint — the bundled Python runtime has only the standard library, which has no modern authenticated encryption in it.

Packaging it so a stranger can run it

A script that needs a terminal, a package manager and a config file edited by hand is not a product; it is a thing only its author can run. Turning it into something installable meant a native menu bar app with the runtime inside it.

55 MBthe whole app
152 KBthe actual code
0things to install
42actions

The interface is native rather than scripted, which keeps launch instant and makes the thing signable for distribution. The Python runtime travels inside the bundle, so the app runs on a machine with nothing installed — verified by launching it from an unrelated folder with an empty environment. That turned out to be easy for an unglamorous reason: the service only ever needed the standard library, so there was nothing to install into the bundle at all.

What went wrong, and what it taught

These are the things that only show up when you stop testing the happy path and actually drive the thing from a phone. Most of them would bite anyone building something similar.

A command over SSH cannot use the camera

macOS grants camera, microphone and screen recording to whatever launched the process. A command arriving over SSH runs outside your graphical session, so all three are denied — photos and screenshots failed from the phone while everything else worked. Granting permissions does not help; neither does launchctl asuser, which needs root.

The fix is a small service started from your own session, which therefore has the permissions. Remote commands hand it the request instead of doing the work themselves.

An SSH session has almost no PATH

It is /usr/bin:/bin:/usr/sbin:/sbin — no Homebrew. The bot cheerfully reported that a tool "is not installed" when it was installed, just not on that path. Resolve the binaries you depend on by full path rather than trusting the environment.

There is more than one Python, and only one of them works

A Mac with Homebrew has at least two, and your libraries are in exactly one. The shell usually resolves python3 to the other one. A launcher that trusts the name dies on a missing module; one that probes each candidate for the import it needs does not.

A full menu bar hides your new icon

When the bar runs out of room, macOS collapses the overflow behind a chevron. The app launched perfectly and looked like it had done nothing at all — the worst possible first impression. A welcome window on first run costs half an hour and removes the entire problem.

Reading the Documents folder scares people

Keep an app's code anywhere under Documents and macOS greets the user with "wants to access your documents" before they have done anything. Ship the code inside the bundle and keep data in Application Support: no prompt, and the app becomes movable.

Local speech-to-text does not fit in a download

Running transcription on-device is the right call for privacy, but the machine learning runtime behind it is over 300 MB — six times the rest of the app. Worse, the feature failed after recording a meeting, which is the most expensive moment to fail. Check for the capability before you promise it, and never lose the audio even when the transcript cannot be produced.

A page served over HTTP cannot be made offline-capable

Service workers require HTTPS, so a page served from a machine on your own network cannot be cached. Turn off Wi-Fi and there is no page to load at all — the graceful fallback to the relay never runs, because the code that would run it never arrives. Serve the page from somewhere reachable and let it talk back through the relay.

The corollary: a page served over HTTPS cannot call an address on the local network either. Browsers block that as mixed content. Those two facts together decide the whole topology.

Two copies of a chat bot become a message storm

The chat API only lets one process read messages. Two instances evict each other in a loop; if the error handler reports failures through the chat itself and a supervisor restarts the process, you wake up to hundreds of notifications sent overnight.

Three defences, all cheap: a file lock so a second instance refuses to start, a rule that the same error is only reported once in a window, and a supervisor that gives up after a few rapid crashes instead of retrying forever.

Telling "working" from "waiting for you" is not about process state

Long-running jobs that pause for confirmation are the defining annoyance of working alongside agents: you leave, it asks a question, and nothing happens for an hour. The process state does not distinguish that from waiting on the network — both look asleep. What gives it away is that its accumulated CPU time stops advancing between two samples.

Look only at the foreground process of each terminal; if that is the shell itself, the terminal is simply idle. Beware that the process listing hands you paths containing spaces, escapes newlines, and that a shell running -c something is not idle at all.

Pick the storage that matches the job

A queue needs a write to be visible on the very next read. Eventually-consistent key-value storage is the wrong tool and will lose or duplicate messages; a small SQL database is the right one, and usually has a far more generous write allowance too.


Security posture

This is remote-control software. It reads files, watches through a camera, listens through a microphone and can power the machine down. That reach is the point, and it also means the account controlling it becomes as sensitive as the login password. Five things are worth building in from the first commit rather than bolted on later.

The uncomfortable part worth saying out loud: a chat bot is the most convenient front end and the least private one, because those conversations are not end-to-end encrypted. For a tool that ships webcam photos, that is the weakest link in the chain — which is exactly why the other three ways in exist.