remote-mac lets you control a remote Mac from a Linux server or VPS over SSH — run commands, take screenshots, open/quit apps and transfer files — with two plain bash scripts (only ssh, scp and python3 needed locally). Built for AI agents (e.g. OpenClaw) and automation that need hands on a real macOS machine.
从 Linux / VPS 通过 SSH 远程控制 Mac:执行命令、截屏、控制 App、传文件,适合 AI agent 使用。
| Script | Purpose |
|---|---|
mac_exec.sh |
Run commands, take screenshots, control apps, transfer files |
channel_check.sh |
Check SSH (and an optional bridge URL) availability, output JSON |
Works well paired with OpenClaw as a remote Mac control layer for AI agents.
On the machine running the scripts (Linux / VPS):
bash,ssh,scppython3— used bychannel_check.shand for JSON escapingcurl— only if you setMAC_BRIDGE_URL
On the Mac:
- Remote Login enabled (System Settings → General → Sharing → Remote Login).
- SSH key login configured (your public key in
~/.ssh/authorized_keyson the Mac). The scripts useBatchMode=yes, so password login does not work. - For screenshots and app control: the Mac must be logged in to a desktop session with the screen unlocked.
# 1. Clone
git clone https://github.com/ythx-101/remote-mac.git
cd remote-mac
# 2. Configure (copy example, fill in your SSH host/user)
cp remote-mac.conf.example ~/.remote-mac.conf
$EDITOR ~/.remote-mac.conf
# 3. Run
bash scripts/mac_exec.sh --run "sw_vers"Not sure the connection works? Run bash scripts/channel_check.sh first (see Channel status).
Or configure via environment variables (no config file needed):
export MAC_SSH_HOST="your-mac-ip-or-hostname"
export MAC_SSH_USER="your-username"
export MAC_SSH_PORT="22" # default: 22bash scripts/mac_exec.sh --help # list all commandsbash scripts/mac_exec.sh --run "ls -la ~"
bash scripts/mac_exec.sh --run "brew list"
bash scripts/mac_exec.sh --run "some-long-script.sh" 120 # custom timeout (seconds)bash scripts/mac_exec.sh --screenshot # saves to /tmp/mac_screenshot.png
bash scripts/mac_exec.sh --screenshot /tmp/myscreen.pngRequires an active desktop session. Won't work if the screen is locked.
bash scripts/mac_exec.sh --app "open:Safari"
bash scripts/mac_exec.sh --app "activate:Finder"
bash scripts/mac_exec.sh --app "quit:Safari"
bash scripts/mac_exec.sh --app "kill:AppName" # force kill via pkill -x# Mac → local
bash scripts/mac_exec.sh --file "get:~/Desktop/report.pdf:/tmp/report.pdf"
# local → Mac
bash scripts/mac_exec.sh --file "put:/tmp/script.sh:~/Desktop/"The argument format is get|put:src:dst. Remote paths are passed to scp as-is (no extra quoting), so ~/... works. For paths containing spaces, escape them with a backslash when downloading (e.g. get:~/My\ Files/a\ b.txt:/tmp/); how scp treats spaces in upload destinations depends on the scp protocol in use (legacy vs SFTP), so prefer space-free destination paths for put.
bash scripts/channel_check.sh{
"timestamp": "2026-03-03T07:42:00Z",
"ssh": { "available": true, "latency_ms": 42, "target": "user@mac.local:22" },
"ag_bridge": { "available": false, "latency_ms": 0, "status": "" },
"preferred": "ssh"
}All config via ~/.remote-mac.conf or environment variables:
| Variable | Default | Description |
|---|---|---|
MAC_SSH_HOST |
mac.local |
SSH hostname or IP (Tailscale, LAN, etc.) |
MAC_SSH_USER |
current user | SSH username on the Mac |
MAC_SSH_PORT |
22 |
SSH port |
MAC_BRIDGE_URL |
(empty) | Optional: Antigravity bridge health URL |
REMOTE_MAC_CONF |
~/.remote-mac.conf |
Custom config file path |
REMOTE_MAC_CONTROL_DIR |
auto | Override SSH ControlPath directory |
See remote-mac.conf.example for a commented template.
| Option | Good fit | Notes |
|---|---|---|
Plain ssh |
You only need a shell on the Mac | remote-mac builds on ssh/scp and adds one command set for screenshots, app control and file transfer, plus JSON channel status |
| VNC / macOS Screen Sharing | You need to watch the live screen or use the mouse and keyboard | remote-mac does not stream the screen or simulate mouse input; --screenshot takes a single still image |
| OpenClaw Node | You already run an OpenClaw Node on the Mac | Can serve as a second channel next to SSH (see Dual-channel) |
| remote-mac | A Linux server, VPS or AI agent needs to run commands, grab screenshots, open/quit apps or move files on a Mac | Two bash scripts, no extra software on the Mac beyond Remote Login |
How do I control a Mac from Linux / a VPS? / 怎么从 Linux 远程控制 Mac?
Enable Remote Login and SSH key login on the Mac, set MAC_SSH_HOST / MAC_SSH_USER, then run bash scripts/mac_exec.sh --run "sw_vers". See Requirements and Quick start.
Why do I get "Mac SSH unreachable" or "permission denied"?
Typical causes: Remote Login is off, your SSH key is not authorized on the Mac, or the host / port is wrong. Run bash scripts/channel_check.sh — if ssh.available is false, fix the SSH connection first (try the same ssh -p <port> user@host by hand).
Why is my screenshot failing or blank?
screencapture needs an active desktop session, so it will not work with a locked screen or without a graphical session. It may also be worth checking the Mac's screen-recording permissions for the SSH session.
Can I use it over Tailscale?
Yes — set MAC_SSH_HOST to the Mac's Tailscale IP or name.
Is it safe?
See Security: host keys use accept-new, app names are shell-escaped and passed via argv, and no secrets are hard-coded.
Does it work with password login?
No. The scripts run ssh with BatchMode=yes, which disables password prompts. Use SSH keys.
How do AI agents use it?
As a skill or shell tool: the agent calls scripts/mac_exec.sh with --run, --screenshot, --app or --file, and can parse the JSON from scripts/channel_check.sh to see whether the Mac is reachable. See SKILL.md.
- No hardcoded secrets — all config via env vars or
~/.remote-mac.conf(never committed) StrictHostKeyChecking=accept-new— non-interactive but won't silently accept changed host keys- Shell injection protection — app names are single-quoted via
shell_escape(); AppleScript usesargvpassing - JSON injection protection — all dynamic values in
channel_check.shoutput are encoded viapython3 json.dumps - Portable ControlPath — uses
XDG_RUNTIME_DIR→TMPDIR→~/.cache/remote-mac(works on both Linux and macOS)
When used with OpenClaw, you can keep two independent control channels to the same Mac — SSH and OpenClaw Node — as mutual fallbacks:
┌─────────────┐
│ VPS │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
SSH (mac_exec.sh) OpenClaw Node
any shell command persistent WebSocket
via OpenClaw
│ │
└────────────┬────────────┘
│
┌──────▼──────┐
│ Mac │
└─────────────┘
| Channel | How | Best for |
|---|---|---|
| SSH | mac_exec.sh via ssh |
Any shell command, file transfer, screenshots |
| OpenClaw Node | nodes.run tool built into OpenClaw |
AI agent workflows when a Node is installed on the Mac |
channel_check.sh checks the SSH channel and, if MAC_BRIDGE_URL is set, that optional bridge health URL (reported as ag_bridge). It does not check the OpenClaw Node channel — call that yourself from OpenClaw with nodes.run. The preferred field is ssh when SSH is reachable and none otherwise:
bash scripts/channel_check.sh
# → { "ssh": { "available": true }, "ag_bridge": { ... }, "preferred": "ssh" }If preferred is none, try the Node channel from OpenClaw.
Keep Mac awake:
bash scripts/mac_exec.sh --run "caffeinate -d &"SSH multiplexing is enabled by default (ControlMaster=auto, ControlPersist=10m). First connection is slow; subsequent calls reuse the connection.
Tailscale: set MAC_SSH_HOST to your Mac's Tailscale IP to reach it outside your local network.
MIT