A working pattern for controlling a Mac from your phone: one bot on the machine for files, camera, microphone and power; a second bot on Cloudflare that keeps answering after the Mac shuts down. 58 commands, no server bill, no open ports.
Python on the machine itself. Reaches out to Telegram over long polling, so there are no open ports and no public IP.
JavaScript on a Cloudflare Worker. Woken by webhook when a message lands, and by cron once a minute to fire reminders.
The split is the whole idea. A bot running on your laptop can do things no cloud service can — read your files, take a webcam photo, quit an app, shut the machine down — but it dies the moment the lid closes for good. A bot running on Cloudflare can answer at 3am from anywhere, but it has no idea what a Mac even is. Run both, on two separate tokens, and you stop having to choose.
What follows is the full command surface of a working setup, then how each half is deployed and why neither costs anything.
/start does the sameThe three folder commands list their contents when called bare, and send the named file when given one.
doc for full qualityShutdown and restart both require a confirmation word, so a stray tap cannot kill the machine.
/volumen muteWhile active the Mac is held awake, the display is slept, and the volume is muted, so the setup is not obvious to anyone in the room. The original volume comes back when you disarm it.
Transcription runs locally through Whisper, so recordings never leave the machine. Anything past 3,500 characters arrives as a text file rather than a wall of message.
/atajo is the extensible one: anything built in the Shortcuts app becomes remotely triggerable without touching the bot's code.
20m, 2h, 18:30A separate bot on its own token, answering whether or not the Mac is running. The morning digest bundles the date, the weather, the pending reminder count, and the shopping list size into one message.
/start does the sameNo command matches that.
A Python script using python-telegram-bot, running on the Mac. It uses
long polling: it opens an outbound connection to Telegram and waits. Nothing
listens on an open port, so there is no public IP to expose, no port forwarding, and no
firewall hole. The machine reaches out; the internet never reaches in. That single design
choice removes most of the attack surface a self-hosted bot would otherwise have.
Cost is zero. Telegram's Bot API is free and unmetered at this scale. The only thing consumed is the machine's own electricity.
brew install imagesnap ffmpeg
/usr/bin/python3 -m pip install --user \
"python-telegram-bot[job-queue]" Pillow openai-whisper
Launching is a double-click on a .command file, which is just a shell script macOS
opens in Terminal. Three things are worth building into that launcher:
cd "$(dirname "$0")", so the project folder can be moved without breaking anything.python3 from PATH.while true loop with a short sleep, so a dropped network connection does not end the day.
The second point is the one that bites people. A Mac with Homebrew typically has at least two
Python installs — Homebrew's at /opt/homebrew/bin/python3 and Apple's at
/usr/bin/python3 — and your libraries are installed into exactly one of them.
PATH usually resolves plain python3 to Homebrew's, which may not be the one holding
your dependencies, and the bot dies on ModuleNotFoundError. Probing sidesteps the
whole question:
PY=""
for c in /usr/bin/python3 /opt/homebrew/bin/python3 python3; do
if command -v "$c" >/dev/null 2>&1 && \
"$c" -c "import telegram" >/dev/null 2>&1; then
PY="$c"; break
fi
done
To start it on boot: System Settings → General → Login Items → add the
.command file. Have the bot message you on startup, so you know it came back up.
The limitation that justifies a second bot: polling needs a running machine. A sleeping Mac queues commands and runs them on wake, since Telegram holds updates for 24 hours, but a powered-off Mac cannot answer at all. No setting fixes that — the process has to be running somewhere else.
The second bot is a Worker: a small piece of JavaScript that Cloudflare runs on its edge network, on demand, with no server to maintain. It uses webhooks instead of polling — Telegram sends an HTTP request to the Worker whenever a message arrives, and the Worker answers. Between messages nothing runs and nothing is billed.
Two pieces support it:
A once-a-minute cron costs 1,440 invocations a day against a 100,000 ceiling, so the headroom is enormous. The one real constraint is that writes are capped at 1,000 a day, which is why the scheduled handler should only write back to KV when something actually changed — not on every tick. Cron granularity also bottoms out at one minute, so a reminder can land up to sixty seconds late.
The outside services involved — wttr.in for weather, frankfurter.app for exchange rates — are free and need no API key.
TELEGRAM_TOKEN and a self-invented WEBHOOK_SECRET as encrypted secrets, not plain variables.* * * * *.curl "https://api.telegram.org/bot<TOKEN>/setWebhook\
?url=https://<your-worker>.workers.dev\
&secret_token=<YOUR_WEBHOOK_SECRET>"
Roughly fifteen minutes end to end. The Worker can be served from a subdomain of your own
domain instead of the default workers.dev address, though that is cosmetic.
This is remote-control software by design. It reads files, watches through the camera, listens through the microphone, and can power the machine down. That reach is the point, and it also means your Telegram account becomes as sensitive as your login password. Four things are worth doing on day one:
/revoke in BotFather
issues a new one in seconds.