← All case studies · McKeen Asma · Portfolio

Case study · Automation infrastructure · Python + SSH + Chrome

Tether: one desk, two Macs

An old MacBook Pro with a swollen battery, brought back as a second worker. The MacBook Air keeps the code, the files and Claude. The 2015 Pro is taking over the Chrome automations, so the Air stays free for the real work.

The 2015 MacBook Pro's screen as Tether streams it to the Air: its Chrome open on a TikTok login page while the social automations were being moved over
The Pro's screen, as Tether streams it to the Air's browser: its Chrome on a TikTok login while the social automations were being moved over (19 Oct 2026). Top and bottom edges cropped to hide a personal inbox.
~14 msper Tether frame, down from ~150 ms in v1
94%of rated CPU speed under full load, plugged in
1keyboard and mouse for both Macs (Synergy)

The problem

My 2015 MacBook Pro has a swollen battery. macOS reports it as Service Recommended after only 187 cycles, so the laptop lives plugged in on the corner of the desk. It was too good to throw away, and my MacBook Air was doing everything at once: code, Claude, video rendering, and a pile of Chrome automations that log in, upload and click all day.

Before trusting the Pro with work I checked whether the bad battery slows it down. Under full load on all 4 threads, plugged in, it ran at 2,520 MHz, 94% of its rated 2.7 GHz. A battery throttle would show 800–1,500 MHz. To be kinder to the battery I capped charging at 80% with bclm.

How the two Macs work together

Diagram: the MacBook Air (the brain: Claude Code, code and files, rendering, scheduler, pro-up.sh) and the MacBook Pro 2015 (the worker: Chrome for automations, Tether server). Links: Synergy shares one keyboard and mouse; the Air drives the Pro's Chrome over a CDP SSH tunnel; Tether streams the Pro's screen to the Air; the Pro's `air` command opens Claude on the Air over SSH; the Air runs commands on the Pro over SSH.
The Air is the brain, the Pro is the worker. Tap to open full size.
PieceWhat it does
TetherMy own browser remote: the Pro's live screen plus mouse and keyboard in any browser, no app to install.
SynergyOne keyboard and mouse for both Macs: the Pro sits to the right of the Air, and the pointer just slides across.
Chrome over a tunnelAutomation scripts stay on the Air but drive the Pro's Chrome through the Chrome DevTools Protocol over SSH, so the page loading and clicking happen on the Pro.
air commandFrom the Pro, one word opens Claude Code on the Air over SSH. Claude Code needs macOS 13 and the Pro is stuck on 12, so the Air does the thinking.

Tether: from ~150 ms to ~14 ms per frame

I first built Tether in June 2026 so a 2012 iPad stuck on iOS 10 could control the Pro. Parsec, NoMachine and modern VNC apps won't install on iOS 10, but its browser still works, so the screen had to come through the browser.

The first version ran screencapture and sips for every frame: about 150 ms each. Profiling showed the cost was starting new processes and writing to disk, not the network. Capturing and encoding inside the Python process (mss + Pillow), with HTTP keep-alive, brought it to about 14 ms per frame, with frames of 6–42 KB depending on the quality setting.

v1v2v3 (in-process)
Frame time~150 ms~80 ms~14 ms
Frame size~200 KB57 KB6–42 KB

Stack: Python standard-library http.server with threads, mss for capture, Pillow for resizing, JPEG and drawing the cursor, PyObjC/Quartz for the real cursor position, cliclick for mouse and keyboard, and plain HTML/JS on the client so it runs on very old browsers.

Moving the Chrome automations to the Pro

The scheduled jobs (job-board applications and social-media uploads) need a real, logged-in Chrome. Before each run, a precheck script makes sure the job will use the Pro's Chrome and never quietly fall back to the Air:

  1. Is the Pro reachable over SSH? If not, the run is skipped.
  2. Start the right Chrome profile on the Pro if it isn't running.
  3. Open an SSH tunnel from a local port on the Air to that Chrome's debugging port.
  4. Check that the local port really is the Pro's Chrome by comparing the browser's websocket id on both ends.
# The local port must be the Pro's Chrome, never a browser on this Mac.
want=$(ssh_pro "curl -s -m 3 localhost:$RPORT/json/version" | ws_id)
got=$(curl -s -m 3 "http://127.0.0.1:$LOCAL/json/version" | ws_id)
[ -n "$want" ] && [ "$want" = "$got" ] || { echo "skip: port $LOCAL is not the Pro's Chrome"; exit 1; }
echo "Pro ready: $PROFILE Chrome is on 127.0.0.1:$LOCAL"
The last check in pro-up.sh. Exit 1 tells the scheduler to skip the run.

The first job moved over is the daily job-board run at 8:05 AM: since 19 Oct 2026 its precheck is pro-up.sh, so it uses the Pro's Chrome or doesn't run at all. The social-media automations are next.

Using Claude from the old laptop

Over SSH, macOS keeps the login keychain locked, so Claude Code on the Air says "Not logged in". The air command on the Pro runs this small script on the Air:

#!/bin/zsh
# Run over SSH from the 2015 MacBook: unlock the keychain so Claude sees the Max login, then pick a session.
security unlock-keychain ~/Library/Keychains/login.keychain-db || exit 1
cd ~/orca/workspaces/playground-macos/personal_random
exec claude --resume "$@"
~/.local/bin/air on the Air. On the Pro, air is an alias for ssh -t mcknx@<air> air.

Choices and limits

  • The Pro is the slower machine, so it gets long, boring jobs (Chrome automations), not urgent ones. Claude, rendering and the code stay on the Air.
  • Tether has no password. It is a personal tool for my home Wi-Fi only.
  • Client work never goes on the Pro. It only runs my personal automations.
  • Only the daily job-board run has moved so far; the social-media automations are still being switched over.
  • The swollen battery still needs replacing. Software can't fix that.

Honest note: a personal setup, not client work. The numbers come from my own runs: Tether's frame times from its README, the battery and CPU figures from system_profiler and powermetrics on the Pro, and the job move from the scheduler's own settings on 19 Oct 2026.