ios-ota-install
Skill0 starsChanged 5 days agoMITView on GitHub
Install a development build of an iOS app on your own phone over the air, from a Safari link served on your Tailscale tailnet. Use when asked to deploy, redeploy or install a build on the phone and the phone is not reachable by Xcode or devicectl (for example it is away from the Mac's Wi-Fi, or the device shows "unavailable"). Also covers what does and does not survive this route.
In this repository7 other files it gives its agents.
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Only devices on the tailnet can open the link, and only devices in the app's provisioning profile can install it.
- Very specific: line 70
All routes share https://<mac>.<tailnet>.ts.net:8443 (tailnet only).
- Distinctive: line 78
If the Mac restarts, the next publish restarts the server.
- Distinctive: line 93
Remove an app's route and stop its server when it is no longer needed:
- Very specific: line 98
Delete ~/ota-serve/<slug>/ only when the builds are no longer needed for reinstalling.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
iOS over-the-air install (Safari link on the tailnet)
Xcode and devicectl find phones by local-network discovery, which Tailscale does not carry, so
they cannot install over Tailscale (CoreDevice error 1011, device "unavailable"). This route works
instead: package the device-signed app as an IPA, serve an itms-services install page through
Tailscale Serve on the Mac, and send the user the link. They tap Install in Safari on the phone.
Only devices on the tailnet can open the link, and only devices in the app's provisioning
profile can install it. Nothing goes to Apple, TestFlight or the public internet. Never use
tailscale funnel for this.
Why not the usual routes
- Xcode or
devicectlover Tailscale: they find phones by local-network discovery (Bonjour), which Tailscale does not carry, so the phone shows as "unavailable". - TestFlight: needs an upload, processing and sometimes review. Minutes to hours, and the build leaves your machine.
- Third-party hosts (Diawi and similar): the IPA goes to someone else's server behind a public link.
This route takes seconds, stays on your own tailnet and needs nothing beyond Tailscale.
Requirements
- macOS with Xcode command-line tools (
xcodebuild,xcrun devicectl,codesign,security). - Tailscale running and logged in on both the Mac and the phone, with HTTPS certificates enabled for the tailnet (Tailscale Serve needs them).
- An app signed with a development (or ad hoc) provisioning profile that lists the phone.
- Python 3 (standard library only).
Steps
- Check where the phone is.
xcrun devicectl list devices. If it showsavailableorconnected, install directly instead (faster). If it isunavailable, use this route.tailscale statusshould list the phone as online. - Build a device-signed app from the working tree, as usual (
xcodebuildforgeneric/platform=iOS, or an equivalent device build). Use the same scheme and configuration you would install directly. The result is.../Build/Products/<Config>-iphoneos/<App>.app. - Get the phone's hardware UDID so the script can check the profile includes it:
This works even while the phone is unavailable.xcrun devicectl list devices --json-output /tmp/devices.json # result.devices[].hardwareProperties.udid, matched on deviceProperties.name - Publish:
It verifies the signature, the profile's expiry and that the UDID is listed; packages the IPA; writes the manifest and install page; starts a localhost-only file server if needed; adds the Tailscale Serve route if needed; then downloads the page, manifest and IPA over the tailnet URL and checks the IPA hash. It prints JSON withpython3 scripts/ota_publish.py \ --app /path/to/App.app --slug <app-slug> --title "<App Name>" \ --port <port> --udid <UDID>install_page. - Send the user the
install_pagelink, and tell them: keep Tailscale connected on the phone, open the link in Safari, tap Install, and install over the existing app (do not delete it unless they want a fresh start; installing over it keeps its data). - Report honestly. A published, read-back link is not an installed app. Installation and launch are confirmed only when the user says so.
Re-running step 4 for a new build replaces index.html and keeps the three newest IPAs.
Ports and paths
All routes share https://<mac>.<tailnet>.ts.net:8443 (tailnet only). Pick an unused local port
per app and keep it stable, and record it here so later runs reuse it:
| Slug | Local port | App |
/my-app-dev | 8771 | Example app (com.example.myapp.dev) |
Files live in ~/ota-serve/<slug>/ (outside every repo). The server log is server.log there.
If the Mac restarts, the next publish restarts the server; the Serve route persists.
What this route cannot do
- No launch arguments. Flags passed by Xcode or
devicectlare absent. Features gated on them are off unless the app has another switch. - No StoreKit configuration file. Purchases go to the real App Store sandbox for that bundle, so paywalls only load if that app's products exist in App Store Connect. Test paywalls that rely on a local StoreKit file by running from Xcode instead.
- Device must be in the profile. A new phone needs registering and the profile regenerating, which is a signing change: ask first.
- Not for App Store or TestFlight. This is development distribution to your own devices.
Cleanup
Remove an app's route and stop its server when it is no longer needed:
tailscale serve --https=8443 --set-path=/<slug> off
lsof -nP -t -iTCP:<port> -sTCP:LISTEN | xargs killDelete ~/ota-serve/<slug>/ only when the builds are no longer needed for reinstalling.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

