Your AI coding agent is mid-task on your Mac. You need to leave your desk. What now?
Open your phone, tap the session, and you're back — cursor blinking, context intact, agent still running.
Works with every terminal tool — Claude Code, Codex CLI, Gemini CLI, vim, ssh, docker, git — anything running in tmux.
- Run
handoff pairon your Mac — it shows a QR code - Scan it with the app on your phone
- All your terminal sessions appear — tap to connect
No cloud relay. No port forwarding. Your Mac and phone talk directly over an encrypted WireGuard tunnel via Tailscale. Your terminal data never touches a third-party server.
brew install handoff # see "Install" note below
handoff setupThis installs Tailscale, enables SSH, configures iTerm2 to use tmux transparently (your terminal looks and feels exactly the same), and generates an SSH key.
Install note (pre-release): the Homebrew formula in
Formula/handoff.rbisn't published to a tap yet —brew install handoffwill fail until the tap goes live. Until then, install from a checkout:git clone …handoff && cd handoff && ./install.sh.
- Install the Handoff app (Play Store / APK releases)
- On your Mac, run
handoff pair— choose sessions, access mode, and expiry - Scan the QR code with the app
- Sign in to Tailscale once (free account)
- Verify the 6-digit code matches on both devices
Tailscale networking is embedded in the app — no separate VPN app needed.
- Build from
ios/Handoff/(Xcode 15+) — seeios/README.md - Install Tailscale from the App Store
- Run
handoff pairon your Mac, scan the QR from the app
You don't change anything about how you work. Open iTerm2, run claude, run vim, run whatever. Everything runs inside tmux transparently — you won't notice.
When you walk away:
$ handoff
frontend 3 tabs (claude, server, git)
api 2 tabs (claude, logs)
Ready — open Handoff on your phone.
On your phone, tap a session. You're in. Same state, same scroll position, same everything.
When you're back at your Mac — it's still there. Both sides stay in sync.
Each phone gets its own identity and permissions, set during pairing:
handoff pair # Pair a new device (interactive)
handoff devices # List all paired devices
handoff devices edit "Pixel 7" # Change permissions
handoff devices rm "Pixel 7" # Revoke access
handoff devices renew "Pixel 7" # Extend expiry
handoff devices log # View access audit logDuring pairing, you choose:
- Session visibility — which tmux sessions the phone can see (
*for all, or patterns likemain,work-*) - Read-only mode — watch sessions without being able to type
- Expiry — access automatically expires after 1/7/30 days (phone can request renewal)
All permissions are enforced server-side via handoff gate — the phone can never bypass them, even with the raw SSH key.
Get a push notification on your phone whenever something on your Mac wants your attention — Claude waiting for input, a long build finishing, a CI script bailing out. Tap it to deep-link straight into the right tmux tab. If you're already viewing that tab on your phone, the notification is suppressed — no spam. One notification per tab; newer events for the same tab replace the older one in place.
The pipeline is intentionally generic. handoff itself is tool-agnostic;
each source of notifications is its own opt-in plugin.
your tool ──► handoff notify ──► ~/.handoff/events.jsonl
│
handoff gate subscribe (NDJSON over SSH/Tailscale)
▼
Handoff Android app ──► system notification
No new ports, no FCM, no third-party push service. The phone reuses the same per-device SSH key it already uses for terminal access; the gate filters events by the device's session patterns server-side.
handoff notify is the public API:
make build && handoff notify --type custom --message "build done"
pytest || handoff notify --type custom --message "tests failing"Inside tmux, the current session and window are inferred automatically from
$TMUX_PANE. Outside tmux, the event still delivers as a "no tab"
notification.
The handoff-notify plugin wires Claude's
Notification and Stop hooks to handoff notify. Once
SagiMedina/handoff is public,
install with two slash commands inside Claude Code:
/plugin marketplace add SagiMedina/handoff
/plugin install handoff-notify@handoff
A manual fallback that doesn't need the marketplace is documented in the plugin's README.
See docs/notifications.md for the event JSON
schema, gate protocol, dedupe/suppression rules, and how to add new
sources.
- Walk away from your desk without killing a long-running Claude Code task
- Approve an edit from your phone while grabbing coffee
- Check on a build from the couch
- Monitor logs on the go
- SSH into a server from anywhere on your Tailnet
- Peer-to-peer encrypted — WireGuard via Tailscale, no relay servers
- Per-device identity — each paired phone gets a unique Ed25519 key
- Server-side permissions — forced SSH commands prevent arbitrary execution
- Biometric lock — optional fingerprint/face unlock to access SSH key
- Two-tier expiry — keys auto-expire, phone can request renewal
- Device management —
handoff devicesto list, revoke, or audit paired phones - Encrypted storage — SSH keys in Android Keystore / iOS Keychain
- Open source — audit every line of code
┌──────────────┐ WireGuard ┌──────────────┐
│ Your Mac │◄──────────────────────────►│ Your Phone │
│ │ (Tailscale / tsnet) │ │
│ tmux ←─── iTerm2 │ Handoff app │
│ ↑ │ ↓ │
│ claude / vim / ssh │ terminal │
└──────────────┘ └──────────────┘
Mac side: tmux runs transparently under iTerm2 via tmux -CC. Each iTerm2 tab is a tmux window. handoff is a CLI that lists sessions, manages pairing, and enforces permissions via handoff gate.
Phone side: Native Android app (Jetpack Compose) with embedded Tailscale networking (tsnet via gomobile). Connects over SSH through a local tsnet proxy. Terminal emulation via embedded Termux libraries.
Phone shows "open terminal failed: not a terminal"
Almost always a stale tmux server. iTerm2 starts tmux at login, so if you brew upgrade tmux later, your running server is still the old binary while your new attach clients are the upgraded one. The protocol mismatch surfaces as that error.
Fix (kills all in-flight tmux state — save work first):
# quit iTerm2 first (⌘Q)
tmux kill-server # in Terminal.app, or skip if no server remains
# reopen iTerm2
Confirm versions match after:
tmux -V # client binary
tmux display-message -p '#{version}' # running server
The gate also prints this error explicitly when it detects the mismatch, so you shouldn't hit the cryptic tmux message from the phone side anymore.
PRs welcome. The project is split into:
bin/— Mac CLI (handoff)android/— Android app (Kotlin/Compose)android/gobridge/— Go tsnet bridge compiled to .aar via gomobileios/— iOS app (Swift/SwiftUI)
# Build the Go bridge
cd android/gobridge && ./build-aar.sh
# Build the Android app
cd android && ./gradlew assembleDebugHandoff is provided "as is", without warranty of any kind. Use at your own risk. The authors are not responsible for any damage, data loss, or security issues arising from the use of this software. Handoff enables SSH access to your Mac over Tailscale — securing your Tailscale account and paired devices is your responsibility.


