neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
翻译准备中
Run and debug your iPhone from Xcode wherever it is — keeps wireless debugging working over Tailscale when the iPhone is on another network.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗RoamRun is a macOS menu bar app, with a roamrun command, that makes Xcode's wireless debugging
work over Tailscale or another mesh VPN. An iPhone, iPad or Apple Vision Pro on a different
network from the Mac stays visible to Xcode and devicectl as a local device: you can build,
install, launch and debug as usual. RoamRun calls that connection a bridge.
It also does three related things:
For a machine without RoamRun — Linux, Windows, or another Mac — there is a separate command,
roamrunctl. With it, a CI job can install and launch your app on your
device (Installing and launching from CI).
Since iOS 17, wireless debugging finds devices with Bonjour (_remotepairing._tcp). Bonjour
stays on the local network, so it does not cross a VPN such as Tailscale. The Mac's
remotepairingd also ties its connection to the network interface where it found the device (as
seen), so advertising the device's VPN address is not enough either.
RoamRun tells Xcode that the device is on the same Wi-Fi, and carries the traffic over the VPN:
flowchart LR
subgraph mac["Mac at home"]
xcode["Xcode / devicectl"] --> rpd["remotepairingd<br/>(Apple)"]
fake["Stand-in Bonjour record<br/>(pointing at this Mac)"]
relay["RoamRun relay<br/>(on the Mac's LAN interface)"]
end
subgraph away["Away"]
iphone["iPhone<br/>(on some Wi-Fi)"]
end
rpd -. "① looks for the iPhone" .-> fake
rpd -- "② connects to this Mac" --> relay
relay == "③ over Tailscale" ==> iphone
Pairing verification and encryption stay end to end between the Mac and the device, as Apple implements them. RoamRun relays bytes; it does not read or change them. The details are in docs/how-it-works.md.
remotepairingd's log with log stream, and stops with a message when macOS answers that an
administrator is required.devicectl (Xcode 15 or later) that supports the device's iOS version; see
Apple's table. roamrun logs and
roamrun run --logs use devicectl's --console, which came with Xcode 16.Seeing and operating the device needs iOS 27 or later. Pairing a far Mac for Xcode needs iOS 27 and Xcode 27.
With Homebrew:
brew install --cask mh-mobile/tap/roamrun
This installs RoamRun.app in /Applications and links the roamrun command. To update:
brew upgrade --cask roamrun.
Without Homebrew, download the dmg from
Releases, drag RoamRun to Applications and open
it. The first screen offers to install the roamrun command; later it is under Open RoamRun ›
Settings › Command line tool › Install…. To update, quit RoamRun and replace the app. There is no
automatic update. Saved devices stay, and bridges that were running start again.
The app is signed with a Developer ID and notarized by Apple.
To build from source you need Xcode 26 or later and Rust from rustup (make
builds device control's library with the Rust version the repository names):
git clone https://github.com/mh-mobile/RoamRun && cd RoamRun
make run # builds RoamRun.app here and opens it; quit an installed RoamRun first
make install-cli # links /usr/local/bin/roamrun to this build (BINDIR=$HOME/bin for another place)
A build without a signing certificate is signed ad hoc, and macOS treats each rebuild as another
app. If you set up device control (seeing and operating the device) with such a build, the
Keychain asks about RoamRun's key after every rebuild: answer "Always Allow". On a Mac reached
over SSH only there is no screen to answer on, and the new build cannot read the saved pairings:
delete the two Keychain items (see
What it creates on your Mac) and set up or import
the pairing again. make app signs with an Apple Development certificate when the keychain holds
one.
While the device is on the Mac's own Wi-Fi the bridge stands aside and shows "On this Wi‑Fi": Xcode reaches the device directly. When the device moves to another network, the bridge takes over by itself. When the Mac's address changes, the bridge restarts.
You can leave the Mac at home and run the whole build-and-try loop from the device in your hand.
To start a bridge, the device has to be on a Wi-Fi with internet access. A café's or hotel's Wi-Fi, a pocket router, or another phone's hotspot all serve. Cellular alone does not, and neither does the device's own hotspot, since the device is then not on a Wi-Fi itself.
To keep a session on cellular, turn on Settings › Network › Keep debugging on cellular. It is off by default, because with it on every Run uses the device's mobile data. It applies only from Ready for Xcode, the bridged state. When the device shows "On this Wi‑Fi", Xcode reaches it over the local network, so leaving home straight onto cellular ends the session. To avoid that, keep the device on a Wi-Fi that another device makes even at home — a travel router, or a spare phone sharing its connection — so that RoamRun bridges it from the start.
A new session needs Wi-Fi again, for example after the device restarts. With the setting off, RoamRun closes the session when the device leaves Wi-Fi and reconnects when it is back.
On a Wi-Fi with a sign-in page, sign in first. Joining a Wi-Fi marked "No Internet Connection" and
sending traffic over cellular did not work when tried. Where a network blocks UDP, Tailscale goes
through its relay servers: that works, more slowly, and roamrun doctor shows the path.
Three ways to drive the Mac from the device:
| Method | On the device | Good for |
|---|---|---|
| An AI agent | A feature for driving an agent on your Mac from your phone, such as Claude Code's Remote Control or Codex in the ChatGPT app | Asking "build it and run it on my iPhone", looking at the result, and repeating |
| A terminal over SSH | An SSH client and Tailscale | Using roamrun, xcodebuild, devicectl and lldb directly |
| The Mac's screen | Screen Sharing or a VNC client, and Tailscale | Xcode's Run and breakpoints in its window |
Away from home, running with the debugger takes longer than at home: attaching lldb takes hundreds of round trips. With a 600 KB app over tethering it took about a minute with the debugger and about 4 seconds without. When you don't need breakpoints, turn off "Debug executable" under Edit Scheme › Run › Info. When you do, turning off "Queue Debugging" (Options) and "Main Thread Checker" (Diagnostics) there made it faster.
The agent features in the table send your conversation through each service's servers. Check your company's policy before using them on a work Mac.
The app's binary is also the roamrun command, for use over SSH or in scripts.
roamrun devices # saved devices (name, UDID, id) and their status
roamrun up <name> # start a bridge and show its progress; it runs until Ctrl-C
roamrun up <name> -d # start it in the background; it survives closing the terminal
roamrun status <name> # exits 0 when Xcode can use the device
roamrun down <name> # stop a bridge, whether the app or another terminal started it
roamrun doctor # check Mac → Tailscale → device step by step, and say how to fix
roamrun run <name> [--scheme S] [--logs] # in a project folder: build, install, launch
roamrun install <name> <App.ipa|App.app> # install a build signed for the device
roamrun logs <name> <bundle-id> # relaunch the app and stream its print / os_log output
roamrun screenshot <name> [file.png] # save the device's screen as PNG and print the path
roamrun ota [<name>] <App.ipa> [--replace] # publish a build for a device to install by itself
<name> is the name you gave the device in RoamRun. It is
case-insensitive and unique; roamrun devices lists the names, and the pencil on the device's page
renames it. The name in the device's Settings app is a separate one. When no saved device has that name, the device's Tailscale name (iphone-15-pro, as roamrunctl takes it, not the whole iphone-15-pro.tail1234.ts.net) works too, and so does its id or its first eight or more characters. A device is added in the app, or from the command line with
roamrun devices add <line> (a line that another Mac made), roamrun key import <file>,
roamrun pair xcode --with <Tailscale name> or roamrun pair control --with <Tailscale name>. All four are described
below.
What each command does, more exactly:
up -d waits up to 60 seconds for Ready (or On this Wi‑Fi) and exits 1 if neither came. The
bridge keeps trying in the background unless it met an error that retrying cannot fix. Its log
is in ~/Library/Logs/RoamRun/.status without a name lists every saved device and exits 0 if any one is ready, so name the
device when a script needs the answer for that one. --wait N waits up to N seconds; it can
return a few seconds late, because each round asks devicectl once.doctor takes a name too. Without one it checks the devices that have a bridge running, or all
of them if none has.run builds the Xcode project in the current folder with -allowProvisioningUpdates, so like
Xcode it may create provisioning profiles. --scheme is needed only when there are several;
--workspace, --project and --configuration (default Debug) are also taken.run and logs take launch options: --arg A (repeatable), --env NAME=value (repeatable)
and --url myapp://settings. logs relaunches the app, because devicectl cannot attach a
console to one that is already running.install checks the signing first. It takes a build exported for Debugging or Release Testing
(Ad Hoc), whose provisioning profile names the device, or for Enterprise (any device that trusts
the certificate). It refuses a build made for App Store Connect. It reaches only devices paired with this Mac; for others, use
roamrun ota or TestFlight.screenshot writes ./<name>-<date and time>.png when no file is given. It was run with
Xcode 26.3 and later; earlier versions were not tried.--json is taken by devices, status and doctor; -v by up. In devices --json, ready
says only that the bridge is Ready or the device is on this Wi‑Fi; status also asks CoreDevice. A command rejects an option
it does not take, with exit code 2. roamrun --help lists everything.The app and the CLI never bridge the same device at once. Whichever starts second refuses, with
one exception: a bridge that is standing aside or has an error can be taken over, by the app's
Start button or by roamrun up (not by the app's automatic retries). A second roamrun up beside
a first one does not start: it exits 0 when the first has the device ready or standing aside, and
1 while the first is still starting or has an error.
The CLI uses the app's settings. Where you cannot reach the app's Settings window, such as over
SSH, change them with defaults:
defaults write io.github.mh-mobile.roamrun keepDebuggingOnCellular -bool true # false to turn it off again
While the bridge is Ready, Xcode's command-line tools reach the device as if it were on this Wi-Fi. These were run that way:
xcrun devicectl device capture screenshot --device <udid> --destination shot.png # what `roamrun screenshot` runs
xcrun devicectl device process launch --device <udid> <bundle-id>
xcodebuild test -destination id=<udid> … # UI tests (XCUITest) on the device
xcrun devicectl device info files --device <udid> --domain-type appDataContainer --domain-identifier <bundle-id>
xcrun devicectl device copy from --device <udid> --domain-type appDataContainer --domain-identifier <bundle-id> --source <path> --destination <local path>
xcrun devicectl device pasteboard copy --file image.png --type public.png --device <udid>
xcrun devicectl device info files --device <udid> --domain-type systemCrashLogs # crash logs (.ips)
roamrun status <name> shows the UDID. Tools built on XCUITest work too, more slowly than on the
same Wi-Fi. Two were tried over tethering: WebDriverAgent took about 2 seconds a tap;
agent-device took 25–35 seconds an action and missed taps in a windowed iPad app.
Claude Code, Codex, Cursor and other agents can build, install and debug through RoamRun. A skill teaches them how:
roamrun init # add the skill to every agent found in ~ (.claude, .codex, .cursor, .gemini, .copilot, .devin)
roamrun init --client claude # or only to the ones you name (repeat --client)
init installs the skill that ships with your RoamRun, so the two match. After updating RoamRun,
run roamrun init again; roamrun status and roamrun doctor say so when an installed skill is
from another version. If another tool manages your skills, pin it to your RoamRun's release, for
example gh skill install mh-mobile/RoamRun roamrun --pin "v$(roamrun --version | cut -d" " -f2)".
The skill covers connecting the device (roamrun up -d, then roamrun status --wait 60 --json
for the UDID), screenshots, and what only a person can do, such as unlocking the device. Exit codes
are 0 for ready, 1 for not ready or failed, and 2 for a usage error.
RoamRun can show the device's screen and operate it: tap, swipe, type, press its buttons. This is called device control. It needs iOS or iPadOS 27 or later. It does not use the bridge or Xcode: RoamRun makes a pairing and a connection of its own. It was run on an iPhone; an iPad has not been tried.
Before you set it up, know this: while a device is switched on, any program you run on this Mac can see and operate it, not only the command or agent you meant it for. The details are under "Who can use it" below.
Set up once for each device, with the device on the Mac's Wi-Fi. Open the device's page in
RoamRun and press Device control › Set Up…. On the device, under Settings › Privacy & Security ›
Developer Mode, pick "RoamRun (this Mac's name)" and enter the code the Mac shows. The sheet then
says "Device control is set up", and roamrun status <name> shows "Device control: connected".
After that RoamRun connects whenever the device is on a Wi-Fi and reachable over the VPN. The RoamRun app holds the connection, so it has to be running. A connection that was open stayed open when the device's Wi-Fi was switched off (seen on iOS 27.0).
roamrun look iPhone /tmp/now.png # the screen now, as PNG, 1280 pixels at most on its longer side;
# prints the path, its size, then "look 12"
roamrun tap iPhone 295 640 # a point, in the pixels of that image (--look 12: of that look only)
roamrun look iPhone /tmp/now.png # look again before the next action
roamrun swipe iPhone 295 900 295 450 # drag from one point to another
roamrun type iPhone "hello" # US keys; right only while the device's keyboard is an English one
roamrun paste iPhone "任意の文字列" # any text, through the device's pasteboard; iOS asks "Allow Paste"
roamrun press iPhone home # home, lock, volume-up, volume-down
roamrun elements iPhone # what accessibility says is on the screen, without positions
The rules:
tap or swipe needs a look taken within the last minute, and
each look serves one action. type and paste may still go by a look an action used (look, tap
a field, type), and wait until a second after the last input before they are sent.--look <n> on tap, swipe, type and paste (after the text for those two) holds them to
look n, the number look prints: refused once another look (by any caller), a button,
elements or an action without --look came since, or after a minute. Without it, a tap or swipe goes by the last look, and type or paste needs none.type sends at most 2000 characters, about 16 a second, and a newline is Return. Under a
Japanese keyboard even a space converts instead of being typed (seen); use paste there.swipe takes a duration in milliseconds as a last argument (50–5000, default 300). elements
takes a limit (default 40) and may scroll the screen while it reads.For an agent the same are MCP tools: claude mcp add roamrun -- roamrun mcp, or the like for
another agent. The tools are devices, look, tap, swipe, type, paste, press and
elements, with the three for apps below. They differ from the commands in four ways: device is the name saved in RoamRun, as
devices lists it, not a Tailscale name or an id; look returns the picture to the agent,
with the look's id, instead of writing a file; type takes 500 characters a call; and tap,
swipe, type and paste must give that id ("look": 12), as --look does. An app older than
the roamrun it is asked from gets none of these four tools.
install, install_status and launch (0.11.0+) put an app on the device and start it, with no
Xcode: install takes the path of an .ipa or an .app folder on this Mac, built and signed for the
device, ends the app's running processes, and answers at once with an id; install_status with
that id says when the app is in, and until then other calls on the device answer that it is
installing; launch takes the bundle identifier. The file is copied into RoamRun's own folder for
the app, and removed when the install ends (or, if the app quit meanwhile, when it starts again;
one left by a command that was ended goes an hour later while the app runs).
An .ipa is read whole into the app's memory while it is sent to the device (an .app folder is sent
file by file).
For an agent elsewhere on your tailnet (0.11.0+), roamrun mcp --http <port> --allow <who>
serves the same tools over HTTP, until Ctrl-C. <who> is tag:<name> or a login, and --allow
may be given again; --token-file <file> also asks every request for Authorization: Bearer <the file's token> (a file only you can read, 32 characters or more), and a call from this Mac itself
is let in only with it. It listens on this
Mac's Tailscale IPv4 address only, at http://<that address>:<port>/mcp (give a cloud the
address: it usually has no MagicDNS), and lets in what tailscale whois says is a machine with
that tag or that user's untagged machine. Requests, refusals, turns and the log go as
with roamrunctl mcp --http (its rules), with these
differences: it offers the devices saved in RoamRun, each operated only while its switch in the
app is on, rather than devices named on its command line; a call whose caller goes, or takes it
back, after its turn here is given up at the app, as with roamrun mcp: not begun if it still
waits there behind another client of the device, stopped where it can if it is under way; and
uploads are kept in RoamRun's own folder rather than one made for the run. The RoamRun app has to
be running, as for roamrun mcp. What is shown and sent passes through whatever lies between: a
cloud's proxy sees the images, the text typed or pasted, what is uploaded, and the token. It was
called from this Mac itself with the token, and from an untagged Linux machine of the tailnet let in
by the login (scripts/mcp-http-check.py, then an upload, install and launch; under --allow tag:ci that machine was refused, 403); and on 2026-10-27 from a cloud's Linux machine on the tailnet
(Grok Bot's computer) through mcp-remote, a look and taps on an iPhone Duo. Not yet through a
cloud's proxy, nor with an upload of hundreds of MB.
With --allow-install [--max-upload <MiB>] it offers install and install_status, as
roamrunctl does: the caller sends an .ipa with PUT /upload (1024 MiB at most by default), gets
its id and sha256, and installs it by them, never by a path. One upload is kept, for the machine
that sent it to install. An install the app doesn't begin (the device not set up, switched off,
installing or not reached, the caller gone before its turn at the app) leaves it for another try.
It goes when the app answers that an install of it began, when any caller starts another upload,
after 30 minutes unused, or at Ctrl-C; one left by a server that was killed goes about a day later, while
the RoamRun app runs or when it, or another such server, starts. Without --allow-install there
is neither.
What the device does meanwhile. Each look and each action runs a screen-sharing session on the device, which RoamRun keeps for about five seconds after the last one. The device showed it as screen sharing (seen). What happened to sound and the microphone during such sessions was noted app by app in one sitting — some apps went on sounding, some played unheard, one stopped, and dictation took nothing: docs/sound-while-looking.md.
Who can use it. Device control is not given to one agent or one command. While a device is
switched on, any program you run on this Mac can see and operate it, and install and launch apps
on it, through the app: a build
script, a package's install step, another agent. A look shows whatever is on the screen,
notifications and messages included, and locking the device does not stop it: the lock screen is
seen and operated too (seen), though unlocking still takes the passcode or Face ID. While
the MCP server over HTTP (mcp --http, above) runs, so can any program on the machines its --allow lets in (each
untagged machine of a login allowed, each machine with a tag allowed), on every device switched
on, launch included; with --allow-install it can also replace the device's apps with any app
signed for it. --token-file is the one check beyond the machine.
roamrun key import <file> and roamrun pair control --with <Tailscale name>.
Turn it off when nothing of yours is using the device.look written to a file is readable by you alone and stays until you delete it.Setting up needs the Mac and the device on one Wi-Fi. A Mac elsewhere, such as one in a data centre, never is. When both Macs are on one tailnet, a Mac on the device's Wi-Fi introduces the far Mac, and the far Mac makes its own pairing and keeps it, sealed. The Mac beside the device relays the device's connection without reading it and saves nothing of it, but it shows the code to type: introduce with a Mac you trust.
roamrun pair control --with macbook-pro # on the far Mac, with the RoamRun app open
roamrun pair introduce --host cloud-mac --to iPhone # on the Mac beside the device, run by you; iPhone is saved in RoamRun there
roamrun look iPhone # on the far Mac, once it says the device is paired
The names are the Macs' Tailscale names. On the device, pick "cloud-mac" and type the code that
pair introduce prints. Run pair introduce yourself, in your own terminal: an agent that ran it
would have the code in its output.
pair introduce, not the far Mac's command. Run pair introduce again and
the far Mac makes a new code: five tries in all.roamrun pair control --last on the far Mac says what came of the last attempt, for when its
end did not reach the other Mac.Without Tailscale between the two Macs, make the pairing on a Mac beside the device and carry it:
roamrun key create iPhone ~/iphone-for-cloud.json --as cloud-mac # beside the device: pick "cloud-mac" on it, enter the code
roamrun key import ~/iphone-for-cloud.json # on the far Mac, with the RoamRun app running: saves the device and its pairing, removes the file
The file is a key. Whoever has it and reaches the device can see and operate it, so move it as you
would a password and keep no copy. key import keeps the pairing only if it connects, so the
device has to be reachable then. If the device restarted since the file was made, its port has
changed: for a device not yet saved on the far Mac, key import looks for the port; for one that
is saved, use Find RemotePairing Port on its page first. Each pairing made this way is listed on the device under its own
name and can be removed there alone.
Setting such a Mac up from nothing — Tailscale, RoamRun, the pairing — is in docs/another-mac.md, with a script that does it.
A bridge needs a Mac that Xcode has paired with the device, and Xcode pairs on one network. A Mac elsewhere — in a data centre, an agent's, yours in another place — never shares one with the device. With iOS 27 and Xcode 27, a Mac that does share one can introduce it, once.
The far Mac needs Xcode, RoamRun and Tailscale. Setting such a Mac up from nothing is in docs/another-mac.md.
When both Macs are on one tailnet, each names the other and nothing is carried by hand:
# on the far Mac: Xcode's Device Hub › + › Pair Nearby Device (before or after), and
roamrun pair xcode --with macbook-pro # macbook-pro: the Tailscale name of the Mac beside the device
# on the Mac beside the device, with the device saved in RoamRun:
roamrun pair introduce --host cloud-mac --to iPhone # cloud-mac: the far Mac's Tailscale name
# on the device: Settings › Privacy & Security › Developer Mode › Pair with “cloud-mac”,
# and the code that Device Hub shows on the far Mac
# on the far Mac, once it says the device is saved:
roamrun up iPhone
What to know:
roamrun up
there shows whether it was made. pair introduce cannot tell: a wrong code looks the same to it
as one not yet typed.pair introduce says whose Mac the far one is, as Tailscale has it, before it announces
anything. (For an Apple Vision Pro the far Mac says whose device it is instead.)Who gets the device. Introducing a Mac lets it use the device as a developer from then on:
install and run apps, debug them, read their data. To withdraw that, remove the Mac on the device
under Settings › Privacy & Security › Developer Mode. It is listed there under the name the Mac
gives itself, which pair introduce tells you. Unlike device control, Xcode's pairing cannot be
switched off in RoamRun: removing it on the device is the only way.
Other ways to do the same, and the details — the same exchange with a line carried by hand, what each side checks, debugging symbols on the far Mac, and what was tried — are in docs/far-mac.md.
Where no Mac with RoamRun is beside the device, an app on the device introduces instead: RoamRun
Introducer, in iOS/. It is built and installed with Xcode, for iOS 27 or later, and is
not on the App Store.
# on the far Mac: Device Hub › + › Pair Nearby Device, and
roamrun pair xcode --with iphone-15-pro --qr # iphone-15-pro: the device's Tailscale name
# on the device: the app, Scan (or type the far Mac's name), Introduce; then
# Settings › Privacy & Security › Developer Mode › Pair with “cloud-mac”, and the code Device Hub shows
# on the far Mac, once it says the device is saved:
roamrun up iphone-15-pro
--qr draws the far Mac's Tailscale name as a code in the terminal for the app to read. The app
keeps the name, so the code is not needed next time. roamrun pair control --with iphone-15-pro,
with the RoamRun app open on the far Mac, pairs for device control the same way; the code to type
then shows on the device, in a notification. If iOS refuses the app its background task, the app
has about 30 seconds and says so: go to Settings and type the code straight away.
Or the machine beside the device introduces, with roamrunctl, a
separate command for a machine without RoamRun:
# on the far Mac: Device Hub › + › Pair Nearby Device, and
roamrun pair xcode --with my-pc # my-pc: that machine's Tailscale name
# on that machine, on the device's Wi‑Fi:
roamrunctl pair introduce --host cloud-mac --to iphone-15-pro # the far Mac's Tailscale name, and the device's
# on the device: Settings › Privacy & Security › Developer Mode › Pair with “cloud-mac”, and the code Device Hub shows
# on the far Mac, once it says the device is saved:
roamrun up iphone-15-pro
roamrun pair control --with my-pc, with the RoamRun app open on the far Mac, pairs for device
control the same way. roamrunctl then prints the code to type, so run it yourself.
In the other direction, the --host of a Mac's roamrun pair introduce can also be a Linux or Windows
machine that waits with roamrunctl pair control --with <name>, for device control only. And
roamrunctl can hold a pairing and use a device with no Mac and no Xcode on that machine; see
its README. Keep roamrun
and roamrunctl at the same version, since they talk to each other. Before 0.7.0 pair introduce
named its machine with --mac; it is --host now in both.
An Apple Vision Pro is paired the other way round: it waits, and the Mac picks it. The two commands are the same as for an iPhone.
# on the device: Settings › General › Remote Devices, left open
# on the far Mac:
roamrun pair xcode --with home-mac # home-mac: the Tailscale name of a Mac on the device's Wi‑Fi
# on that Mac, with the device saved in RoamRun there:
roamrun pair introduce --host cloud-mac --to vision-pro
# on the far Mac, when its command says so: Device Hub › + › Pair Nearby Device › Apple Vision Pro,
# pick the device, Next, and type the code the device shows
roamrun up vision-pro
pair introduce refuses another
kind of --host, and neither the Introducer app nor roamrunctl introduces one.Run with these commands on 2026-10-13, with one Apple Vision Pro and a virtual Mac as the far one: the pairing, the far Mac's command ending by itself, and the bridge Ready there. Not tried: breakpoints, the device away from home, Apple TV.
roamrun ota publishes a build on a page in your tailnet, and the device installs it by itself
over HTTPS. This needs neither the bridge nor a pairing, and works on Wi-Fi and cellular alike. Use
it when:
It is install only: no debugger, no roamrun logs, no roamrun screenshot.
roamrun ota build/MyApp.ipa # checks it against every device RoamRun knows
roamrun ota iPhone build/MyApp.ipa # or name one, to check just that device
The command prints which of your devices the build covers, and the address of the page. Open that address on the device and tap Install. The page lists the last 5 builds of each app, newest first.
What it needs:
roamrun ota refuses a Development build, an App Store or TestFlight build,
an unsigned one, and one whose provisioning profile has expired.tailscale serve, on port 41443.For a device RoamRun has never seen, such as a tester's phone, the command warns that the build's profile names none of your devices. That is expected. The tester's device has to be in the Ad Hoc profile, and needs Developer Mode on to launch the app.
Anyone on your tailnet can open that page and install those builds. On a tailnet you share,
restrict it with Tailscale's access rules. To take the page down, delete
~/Library/Application Support/RoamRun/ota/.
The rest — the port and Funnel, --replace, Developer Mode on the device, and what RoamRun checks
before it stores a build — is in docs/ota.md.
A CI job can install an .ipa on your device (one it built, or one it was given) and launch it,
over Tailscale, with roamrunctl. It goes through neither the bridge
nor Xcode, so a Linux runner can do it too. The job joins your tailnet, then does one of two
things:
roamrun key create on a Mac with RoamRun, or roamrunctl pair control --to on any machine.roamrunctl mcp --http and keeps that pairing; the
job sends it the .ipa, and it installs and launches.| CI | How | Guide |
|---|---|---|
| GitHub Actions | the action mh-mobile/roamrun-action, after Tailscale's own action; Linux and macOS runners | docs/ci.md |
| Xcode Cloud | a script in the project runs Tailscale in userspace and reaches the device or a relay through Tailscale's HTTP proxy (ROAMRUNCTL_PROXY, roamrunctl 0.10.0 or later), or calls a relay with curl | docs/xcode-cloud.md |
| Bitrise | the Step mh-mobile/bitrise-step-roamrun, which joins the tailnet itself | docs/bitrise.md |
Each was run with an iPhone 15 Pro on iOS 27.0; the guides say how, and what wasn't tried. What it
needs: a device on iOS 27 or later with Developer Mode on, on Wi‑Fi with Tailscale on and unlocked
once since it started; and an .ipa signed with a development profile that lists it. Whoever can
read the CI's secrets can operate the device; the guides say who that is and how each secret is
taken back.
When in trouble, run roamrun doctor first.
roamrun doctor
first.roamrun status and roamrun doctor. Allow RoamRun under System Settings › Privacy & Security ›
Local Network. If the switch is already on, the permission is stuck. The only way found to clear
it was to uninstall with brew uninstall --zap and install again
(#23). --zap deletes the folder
~/Library/Application Support/RoamRun, with your saved devices and device control's pairings in
it. To keep them: stop the bridges roamrun up started, with -d or in a terminal
(roamrun down <name>), and quit RoamRun; copy that folder aside; run brew uninstall --zap --cask roamrun and install
again; then put the folder back before you open RoamRun or run roamrun. Whether the
permission stays cleared with the folder put back hasn't been tried, nor whether device control
then opens its devices as before.devicectl. Flutter and React Native have not been tried
(#5).RoamRun keeps its own files in these places:
| Location | Contents |
|---|---|
~/Library/Application Support/RoamRun/ | Saved devices, bridge status, device control's pairings (sealed), builds kept for over-the-air installs, and apps on their way to an install (control/: copies, and .ipa files uploaded to its MCP server over HTTP) |
~/Library/Logs/RoamRun/ | Logs of roamrun up -d |
io.github.mh-mobile.roamrun (defaults) | Settings, and which bridges were running |
/usr/local/bin/roamrun | Only if you installed the CLI from the app or with make install-cli. Homebrew links /opt/homebrew/bin/roamrun instead |
~/.claude/skills/roamrun/ and the like | Only if you ran roamrun init |
| Login Keychain: "RoamRun device control", "RoamRun device control (devices switched on)" | Only if you set up device control |
Beyond those: temporary files (while it unpacks a build, and for devicectl's answers), files
that commands write where you run them or where you say (a screenshot, a look, a key), a login
item if you turned on "Open at login", and one tailscale serve entry while it serves builds for
roamrun ota.
To uninstall, in this order:
roamrun up started, with -d or in a terminal (roamrun down <name>). They
run without the app.roamrun init --uninstall now, while the command still exists.tailscale serve entry back.brew uninstall --zap --cask roamrun removes the app, the CLI link, settings,
logs and saved devices. The two Keychain items stay: remove them with the security lines
below. Without Homebrew, run the lines below that apply to you.If you changed the over-the-air port, look it up before you delete anything, and use it in the
tailscale serve line: defaults read io.github.mh-mobile.roamrun otaPort shows it.
rm /usr/local/bin/roamrun # if you installed the CLI from the app or with make
rm -rf ~/Library/Application\ Support/RoamRun ~/Library/Logs/RoamRun
security delete-generic-password -s io.github.mh-mobile.roamrun.device-control -a pairings # if you set up device control
security delete-generic-password -s io.github.mh-mobile.roamrun.device-control -a allowed
tailscale serve --https=41443 --set-path=/ off # only if RoamRun was force-quit or crashed while serving builds
defaults delete io.github.mh-mobile.roamrun # after the line above: it holds the port, if you changed it
defaults delete com.roamrun.app 2>/dev/null # left by versions before 0.1.12
# finally delete /Applications/RoamRun.app
Something broken, or the docs unclear? Open an
issue. Please include roamrun --version, your
macOS, Xcode and iOS versions, and the output of roamrun doctor --json.
How to report a vulnerability, and what RoamRun exposes: SECURITY.md.
RoamRun was built by Claude Code, powered by Claude Opus 5.5, under mh-mobile's direction.
It builds on a public write-up: Kevin Paterson,
"How to remotely iterate & deploy your sideloaded iOS-apps over tailnet",
which shows the same arrangement with dns-sd -P and socat.
Device control is built on idevice (Jackson Coxson), a Rust implementation of the protocols a Mac speaks to a device; RoamRun builds it from a fork at a pinned commit. The licenses of it and of the other crates are in THIRD-PARTY-LICENSES.txt, which the app carries too.
Projects on the same problem, or the same ground with other tools: