Cloud agents + MobAI = iOS development
Claude Code on the web, Codex in the cloud and Cursor background agents run in Linux
sandboxes. No Xcode, no simulator, no phone, so they could write iOS code but never see
it run. MobAI 3.0 changes that with mobai-dev,
one binary installed by a sandbox setup script. Preview the app on Linux, build it in
your own CI, run it on a simulator, then install it on the iPhone on your desk and drive it.
See iOS development without a Mac for the overview.
Preview
Flutter, React Native and SwiftUI apps rendered in a phone-sized viewport on the sandbox's own Linux machine.
Build
iOS builds on your own macOS runners: GitHub Actions, Codemagic or Bitrise. One, or all three to stack their free minutes.
Simulator
The same runner boots a simulator, installs the build and hands the agent the controls.
Your own device
Install and test on your own iPhone over your Tailscale network, with your computer off.
What you need
For previews and builds: nothing but the setup command below. No API key, no Tailscale, no Mac. Simulators need a free MobAI account; when the agent signs in with your email, the account is created on the spot.
For your own phone, and for over-the-air installs on it:
- The MobAI desktop app on macOS, Windows or Linux, for the one-time phone setup. Download.
- An iPhone on iOS 17 or later, connected by cable during that setup.
- Tailscale on the phone, connected to the same tailnet the sandbox joins.
Set up your sandbox
One setup command for every platform. The script installs mobai-dev,
Tailscale, and the skills that teach the agent every command on this page. It reads
the environment first, and for whatever is missing the agent asks you in the session:
an emailed sign-in code, a Tailscale login link. The binary
lands in ~/.mobai/bin; the agent uses the full path when that
directory is not on its command path.
Set the sandbox's network access to full internet. No host allowlist covers what a build needs: GitHub, your CI provider, npm, pub.dev, CocoaPods, Apple's servers and Tailscale.
The desktop app shows the same setup, with your account email and API key filled in: Integrations > Cloud Agents, or the last step of the phone wizard.
Claude Code
Open the cloud environment's settings and set its Setup script to:
curl -fsSL https://mobai.run/cloud/install.sh | sh
In the same dialog, add this to Environment variables:
MOBAI_ACCOUNT_EMAIL=<your mobai account email>
Claude Code's environment variables are visible to everyone using the environment, and they are absent while the setup script runs. Do not put API keys, Tailscale keys or signing credentials there. The script installs tools only. In the chat, the agent signs you in with an emailed code; for phone access it also gives you a Tailscale login link to approve.
Network access: new environments start on Trusted, which only reaches an allowlist. Set it to Full.
Cursor
Cursor builds the environment from a Dockerfile in your repository, so the fastest start is the prebuilt image, with Swift, Flutter, Node, the preview engines and mobai-dev already in it. Commit these two files to your default branch:
# .cursor/Dockerfile
FROM ghcr.io/mobai-app/mobai-dev:1.3.1
// .cursor/environment.json
{
"build": { "dockerfile": "Dockerfile" },
"install": "curl -fsSL https://mobai.run/cloud/install.sh | sh",
"start": "$HOME/.mobai/bin/mobai-up"
}
The files alone change nothing until Cursor has built them. In the Cloud Agents dashboard:
- Create a new environment and select the repository.
- Click Skip & Save (not the agent setup, which installs onto Cursor's stock Ubuntu and ignores the Dockerfile).
- Open the environment and click Trigger New Build, with Draft build off. Cursor pulls the image, runs the install command and saves the result.
- Agents started after that build run in the image. In one,
echo $FLUTTER_ROOTprints/opt/flutter; empty means the agent is still on the stock image.
Without the image, leave the Dockerfile out and set only the install command in
.cursor/environment.json:
curl -fsSL https://mobai.run/cloud/install.sh | sh
Either way, add these as secrets in the Cursor dashboard, under Cloud Agents, Secrets tab. Scope them to the environment above so only its agents get them; Cursor hands each one to the agent as an environment variable:
MOBAI_API_KEY=<your mobai API key>
MOBAI_TAILSCALE_KEY=<tailscale ephemeral auth key>
MOBAI_ACCOUNT_EMAIL=<your mobai account email>
The email identifies your account, the API key authenticates the sandbox, and the Tailscale
key is only needed for your phone. Without an API key the agent signs in with an emailed
code; without a Tailscale key it gives you a login link to approve when phone access is wanted.
To ship to TestFlight or the App Store from the agent, add the App Store Connect key the same
way: ASC_ISSUER_ID,
ASC_KEY_ID and
ASC_PRIVATE_KEY.
Network access: Cursor allows all traffic by default. Keep it that way. If you turned filtering on, turn it off for this environment.
Codex
In the environment settings, set the setup script to:
curl -fsSL https://mobai.run/cloud/install.sh | sh
Add these values to the environment's secrets:
MOBAI_API_KEY=<your mobai API key>
MOBAI_ACCOUNT_EMAIL=<your mobai account email>
The API key is optional if you sign in during the session with an emailed code.
MOBAI_TAILSCALE_KEY is accepted the same way as on Cursor.
Your phone is not reachable from Codex yet. Its sandbox blocks the connection a tailnet join needs, and a Tailscale key does not change that. The script installs Tailscale and tries anyway, so nothing needs re-pasting the day Codex allows it, and the agent is told why the join fails. Previews, builds and simulators work. Use Claude Code or Cursor when the phone is the point.
Network access: Codex filters outbound traffic by default. Set the environment's internet access to full. The tailnet restriction is separate from that setting.
Grok bot
Grok has no setup command field. Paste the same setup command into the chat and the bot runs it in its sandbox:
curl -fsSL https://mobai.run/cloud/install.sh | sh
No environment variables needed. The agent signs in from the chat with an emailed code.
First session
Open a session in your project and say:
Set up the MobAI preview for this project and show me the main screen.
The agent runs the project setup, installs any missing preview engine, starts the preview and comes back with a screenshot. If a sign-in is needed it asks for an emailed code:
mobai-dev login --email you@example.com
mobai-dev login --email you@example.com --code <code>
Signing in is free and creates the account when the email is new. From there, ask for the next thing:
- Show the paywall in dark mode.
- Test onboarding with location permission denied.
- Build the iOS app.
- Run this version on a simulator.
- Install it on my phone and tap through onboarding.
1. Preview
Flutter, React Native and even SwiftUI apps render in a phone-sized viewport on the sandbox's own Linux machine. The preview runs your app's code without a device, a simulator or a Mac.
Run setup once per project, naming the framework and the agent:
mobai-dev setup --agent claude --framework <flutter|react-native|swiftui>
# --agent cursor or --agent codex on those platforms
Setup writes .mobai/config.json, installs the skill that teaches
the agent the preview workflow, and prints the commands for anything the machine still
needs. The agent follows that plan, then starts the preview:
mobai-dev preview run
The agent can:
- Read the screen as a semantic tree.
- Tap and type by label, scroll, take screenshots.
- Hot reload after edits.
- Mock location, permissions, the camera, network responses and the signed-in user.
- Exercise loading, empty, error and denied-permission states.
Packages that need real hardware are repaired with small adapters; a catalog of ready-made ones ships with the engines. Use the preview for UI iteration, then verify native behavior and performance on a simulator or your phone.
2. Build
iOS builds run on a macOS runner in your own CI. GitHub Actions is the default; Codemagic and Bitrise are one flag away. Set a project up for GitHub Actions and build:
mobai-dev build init
mobai-dev build --ios
Or on a configured provider:
mobai-dev build --ios --provider codemagic
mobai-dev build --ios --provider bitrise
The command pushes a snapshot of the working tree, uncommitted changes included, to your
GitHub repository. The selected provider builds that snapshot, and the agent gets the
artifact, normally under ./build/. Without
--provider the command uses the provider in
builder.json, then GitHub Actions.
Use one provider, or set up all three and stack the free minutes each gives you. Builds are free in MobAI; runner time belongs to your CI account. The setup per provider is in CI providers.
Android builds run in the sandbox itself when the Android toolchain is present:
mobai-dev build --android.
3. Simulator
Start a simulator running the project:
mobai-dev sim start
mobai-dev sim start --provider codemagic
mobai-dev sim start --duration 30m
The CI runner builds the working tree, boots an iOS simulator, installs the build and publishes the simulator to your MobAI account. The agent receives a device id and takes it from there: live screen, taps, typing, flows, tests. You can watch and take over from the MobAI desktop app.
The provider needs MOBAI_API_KEY in its secret store to publish
the simulator. The default idle window is 30 minutes; Codemagic and Bitrise cap it at 60.
An unused simulator ends when its window runs out, or earlier with:
mobai-dev sim stop
4. Your own device
After the one-time phone setup, the agent installs a signed build on the iPhone on your desk and drives it from the sandbox over your own tailnet. Your Mac can be off. Before device work the agent brings the session up:
~/.mobai/bin/mobai-up
It handles the MobAI sign-in, joins the tailnet and starts device discovery. A reachable phone appears in the device list within about a minute:
mobai devices
mobai app install ./build/App.ipa
mobai bridge start
mobai observe
mobai screenshot --path /tmp
The agent sees the phone's UI tree and screenshots. It launches apps, taps, types, scrolls and runs complete test flows, as if the phone were plugged into the sandbox.
Install over the air, without the bridge
A signed build can also go onto the phone with nothing running on it:
mobai-dev ota App.ipa
mobai-dev ota stop
The command checks the build's provisioning profile against your onboarded phones before
serving anything, then serves the build and an itms-services
install link over your tailnet. Open the link on the iPhone and tap Install. If Tailscale
Serve approval or HTTPS certificates are missing, the command names the setting and the
next step. This installs the app only; driving it still needs the bridge.
CI providers
Project setup is local and makes no network request, so you or the agent can run it from
the sandbox. mobai-dev build init
writes builder.json and the provider's workflow files, never
overwrites a file that exists, and lists what remains: commit and push, create and connect
the provider app if needed, supply the token, add secrets. The first provider you set up
becomes the project default; --set-default moves it.
GitHub Actions
Nothing to create. The workflows run in the repository's own Actions on GitHub's macOS runners.
mobai-dev build init
# commit and push: builder.json, .github/workflows/ios-build.yml, .github/workflows/ios-share.yml
GitHub runs the copy of a workflow that is on the default branch, so the files must be
merged there before the first build. Secrets go under the repository's
Settings > Secrets. The sandbox needs a GitHub credential
that can push the snapshot and use the Actions API: GITHUB_TOKEN,
a gh login, or the sandbox's injected credentials.
Codemagic
In Codemagic, add an application, connect it to this GitHub repository and pick YAML
configuration. The app id is the value after /app/ in its URL.
mobai-dev build init --provider codemagic --app-id <id> --branch <branch>
# commit and push to <branch>: builder.json, codemagic.yaml, .builder/ci/runner.sh
Create an API token under Account settings and give it to the sandbox as
CODEMAGIC_API_TOKEN. In the app's environment variables, create
a group named builder: the runner's secrets go there, marked
Secret, plus the non-secret BUILDER=1 used for unsigned builds.
Bitrise
In Bitrise, create a CI project connected to this GitHub repository and choose a macOS Xcode
stack. The slug is the value after /app/ in the project URL.
mobai-dev build init --provider bitrise --app-id <slug> --branch <branch>
# commit and push to <branch>: builder.json, bitrise.yml, .builder/ci/runner.sh
Create a personal access token under Account settings > Security and give it to the
sandbox as BITRISE_API_TOKEN. The runner's secrets go in the
project's Secrets tab, not in bitrise.yml.
Tokens, secrets and branches
Codemagic and Bitrise read their workflow file from the branch you named at setup; the source snapshot never carries it. Both still need the sandbox's GitHub credential to push the snapshot. Keep provider tokens in the platform's secret store; Claude Code's plain environment variables are not a place for them.
The runner reads these secrets, exact names, in every provider's own store:
| Secret | Value | Needed for |
|---|---|---|
| IOS_CERTIFICATE_<SET> | Base64 of your signing .p12 | Signed builds |
| IOS_CERTIFICATE_PASSWORD_<SET> | The .p12 password, plain text | Signed builds |
| IOS_PROVISIONING_PROFILE_<SET> | Base64 of your .mobileprovision | Signed builds |
| IOS_EXTENSION_PROFILES_<SET> | JSON of extension bundle ID to base64 profile, {} when none | Apps with extensions |
| MOBAI_API_KEY | Your MobAI API key, plain text | Shared simulators |
<SET> is the distribution of the build profile:
DEVELOPMENT, AD_HOC or
STORE. A build signs when the builder.json
profile it uses (--profile, else defaultProfile)
has a distribution; an unsigned build needs none of these secrets. A phone needs a signed build
whose provisioning profile includes that phone. builder signing setup
uploads the set to GitHub; on Codemagic and Bitrise it prints the values for you to paste
(Signing).
Older projects with ios.signing and no profile still sign with the
unsuffixed IOS_CERTIFICATE, IOS_CERTIFICATE_PASSWORD
and IOS_PROVISIONING_PROFILE. That path is legacy.
What the agent sees when something is missing
With --json every failure is one envelope with a code and a
suggestion naming the next step:
| Code | Meaning | Fix |
|---|---|---|
| CONFIG_INVALID | The provider has no app id or branch in builder.json. | Create and connect the app, then run build init with --provider, --app-id and --branch. |
| AUTH_REQUIRED | The provider token is missing, or the GitHub credential cannot use the Actions API. | Set CODEMAGIC_API_TOKEN or BITRISE_API_TOKEN. For GitHub, use a credential with Actions access. |
| CONFIG_NOT_FOUND | The workflow files were missing; they have just been written. | Commit and push them to the branch the envelope names. |
| BUILD_FAILED | The provider rejected the request, did not confirm it, or the run failed. | Rejected: check app id, branch and token. Unconfirmed: check the dashboard before retrying, a job may be running. Failed: open the run URL in the envelope. |
Your phone
Connect the iPhone to your Mac by cable. In the MobAI desktop app, hover its row in the device list and press the cloud button. The wizard has five steps.
- Pair for cloud. A separate pairing identity for cloud access, stored encrypted in your account. Unlock the phone and confirm Trust if asked. Local pairing stays separate, so revoking cloud access never breaks your local setup. An already onboarded phone can jump straight to any step.
- Prepare the device. The bridge is installed on the phone. Keep the cable connected and the phone unlocked; this can take a few minutes. A cloud session uses the bridge already on the phone.
- Tailscale. Install Tailscale on the iPhone, sign in and leave it connected. Same tailnet as the sandbox, or the phone never appears.
- API key. The key the sandbox uses for your account and your devices. Revoke it any time from API Keys. Claude Code signs in from the chat instead and does not need it.
- Set up the agent. Pick your platform and copy its setup command and environment values. The build project setup and the optional signing steps are here too.
After that, disconnect the cable. Keep the phone awake and on Tailscale when the agent needs it.
The same agent setup without a phone lives under Integrations > Cloud Agents in the desktop app, for previews, builds and simulators.
Signing
A physical iPhone needs a signed build whose provisioning profile includes that phone's UDID. Signing also decides how long the bridge on the phone stays usable:
- Paid Apple Developer account: a year.
- Free Apple ID: seven days. Reconnect the phone and repeat step 2 of the wizard.
There are two sets of signing secrets, for two different jobs:
IOS_*_<SET>in the repository: the macOS CI runner signs your app build with them. A signedmobai-dev buildormobai-dev releaseneeds these.MOBAI_SIGN_*in the agent's sandbox: a phone session signs the bridge (and the apps it installs) itself, so the phone stays ready without your Mac. This needs a wildcard profile, so it is made by hand.
Set up once: ios-builder (recommended)
Signing for a repository is set up once, by you, on your own machine: agent sandboxes usually
cannot write repository secrets (Cursor's cannot). The quickest way is the desktop app:
Integrations > Cloud Agents > Sign builds (also the optional last step of the cloud
wizard). Sign in to GitHub, pick the repository, choose development and/or TestFlight, and
it creates the certificate and profile with your App Store Connect key, writes the secrets
and commits builder.json; the private key stays on your computer,
and a year later the same panel renews the set. The same thing from a terminal:
builder, the
ios-builder
CLI, does the Apple side with an App Store Connect API key, uploads the secrets and writes the
build profile:
# install: macOS, Linux, WSL
curl -sSL https://raw.githubusercontent.com/MobAI-App/ios-builder/main/install.sh | bash
# Windows: builder-windows-amd64.exe from github.com/MobAI-App/ios-builder/releases,
# renamed to builder.exe and put on PATH
builder auth github # once
builder auth apple # once, the App Store Connect API key
# in the repo, once per app (and again with --distribution store for TestFlight)
builder signing setup --distribution development --device <udid> --yes \
--out-dir ~/.mobai/signing/myapp
git add builder.json && git commit -m "signing profile" && git push
# the agent, every time
mobai-dev build --ios --profile development
It registers the phone, creates the bundle ID if it is missing, has Apple issue a certificate
from a private key made on your machine, creates the profile and uploads four repository secrets:
IOS_CERTIFICATE_DEVELOPMENT,
IOS_CERTIFICATE_PASSWORD_DEVELOPMENT,
IOS_PROVISIONING_PROFILE_DEVELOPMENT and
IOS_EXTENSION_PROFILES_DEVELOPMENT. Then it writes a
development profile and ios.bundleId into
builder.json. The profile is written only when the secrets were
uploaded, so a profile in builder.json means the repository can sign.
Running it again reuses what is still valid. It never revokes anything.
- Keep the key out of the repo. Without
--out-dir, the private key, the .p12 and the .mobileprovision are written to the current directory, which is your repo. Keep that folder: later runs find the key there and reuse its certificate. Leavesigning.dirout of the commit: it is that path on your machine. - Apple caps certificates per team. When the team already holds its limit, Apple answers 409, "You already have a current Development certificate or a pending certificate request". Revoke one you no longer use in the developer portal, or pass
--key <path>with the private key of an existing certificate. - Agents that can write secrets can run
mobai-dev signing setupin the sandbox instead. It checks first and creates nothing when the token cannot write the secrets. - No repository signing, phone only. With the App Store Connect key in the sandbox,
mobai-dev signing generatemakes a development certificate for that sandbox only and prints how to sign the unsigned build with zsign. Development builds only, never TestFlight.
Manual: developer portal and Keychain
The path for MOBAI_SIGN_*, or when you have no API key. The wizard shows the same steps:
- Register the phone in the Apple developer portal.
- Create a wildcard provisioning profile (App ID
*) that includes the phone. Wildcard matters: it has to cover both your app and the bridge. - In Keychain Access, export your Apple Development certificate as a .p12.
- Base64 both.
base64 -i profile.mobileprovision | tr -d '\n' | pbcopy
base64 -i identity.p12 | tr -d '\n' | pbcopy
For CI builds, set them as IOS_CERTIFICATE_DEVELOPMENT,
IOS_CERTIFICATE_PASSWORD_DEVELOPMENT and
IOS_PROVISIONING_PROFILE_DEVELOPMENT in the provider, and add
"profiles": {"development": {"distribution": "development"}} to
builder.json. For signing inside a phone session, set these in the
agent's secret store:
MOBAI_SIGN_PROFILE_B64=<base64 of your .mobileprovision>
MOBAI_SIGN_P12_B64=<base64 of your .p12>
MOBAI_SIGN_P12_PASSWORD=<the p12 password>
With these set and a paid Apple Developer account, sessions keep the phone ready on their own and sign the apps they build. That needs a platform with a secret store and phone access, which today means Cursor. On Claude Code, keep the signing secrets in CI and let the build come out signed there.
TestFlight and the App Store
The agent can take a build all the way to testers or to App Review from the sandbox. The upload and the submission go through the App Store Connect API, so no Mac, Transporter or altool is involved; the runner only builds and signs. It needs an App Store Connect API key (Users and Access > Integrations, a team key with role App Manager or Admin), never your Apple ID password. Put it in the sandbox as secrets, or store it once:
# as sandbox secrets: ASC_ISSUER_ID, ASC_KEY_ID and ASC_PRIVATE_KEY (or ASC_KEY_PATH)
mobai-dev asc login --issuer-id <id> --key-id <id> --key ./AuthKey_XXXX.p8
Store signing is set up once by you, on your machine, the same way as development: the
bundle ID, a distribution certificate, the App Store profile, the repository secrets, and a
store profile in builder.json to commit.
The sandbox never signs a store build itself. That would put your distribution private key
in a short-lived environment, and every new sandbox would issue a new certificate until the
team reaches Apple's cap. The runner signs with the repository secrets instead.
builder signing setup --distribution store --yes --out-dir ~/.mobai/signing/myapp
The same caveats as development signing apply: keep the key out of the repo, and revoke or reuse a certificate when Apple's cap is reached. See Signing.
Create the app record once yourself, before the first upload: in App Store Connect, My Apps
> + > New App, with the same bundle ID. The API cannot create it.
mobai-dev asc apps lists the apps your team has.
Then every build is one command:
mobai-dev release --profile store --group "Beta Testers" --notes "What to test"
mobai-dev release --profile store --app-store --release after-approval
It picks the next build number from App Store Connect, builds on your CI provider, checks
the IPA, uploads it, waits for processing, answers export compliance, and adds the build to
the TestFlight groups (created when missing) or submits the version for review. The pieces
are also separate verbs: mobai-dev upload --wait,
mobai-dev submit --testflight,
mobai-dev submit --app-store, and
mobai-dev asc for apps, builds, groups, testers and the team.
The releasing-to-app-store skill installed with mobai-dev
carries the rest: the pre-submission checklist App Review actually rejects on, what each
App Store Connect status means, and how to read and answer a rejection. The listing itself
(description, screenshots, pricing, privacy answers) stays yours to finish in App Store
Connect; a submission against an incomplete one comes back as
ASC_METADATA_INCOMPLETE with the reason.
Limits
- Previews and builds are free in MobAI. CI minutes count against your provider's allowance.
- Shared simulators are free with an account. Your phone gets one 60-minute session a day for free; Pro allows sessions up to 4 hours with no daily cap.
- Codex cannot reach a phone yet. Previews, builds and simulators work there.
- iOS 17 or later on the phone, awake and on the same tailnet as the sandbox.
- One session per phone. Two sandboxes driving the same phone fight over it.
Troubleshooting
The agent says there are no devices
An empty list is not an error. The phone may be asleep, off the tailnet or still connecting. Wake it, confirm Tailscale is connected and on the same tailnet as the sandbox; a reachable phone appears within about a minute of the session starting. If it still does not, reconnect the phone to Wi-Fi, toggle Tailscale off and on, and bring the MobAI app on the phone to the front.
Ask the agent to read ~/.mobai/mobai.log. It records which
peers were found on the tailnet, what each connection attempt failed on and when it retries.
A phone can show as online in Tailscale and still refuse the connection MobAI needs.
On Codex the tailnet join is blocked by the sandbox; retries will not fix it. Continue with a preview or a simulator, or use Claude Code or Cursor for the phone.
The setup script fails on a missing variable
Use the exact command for your platform from this page. Claude Code's script receives no
environment variables and needs none; a script that demands them there is the wrong script
or an old copy. On Cursor and Codex, check the values sit in the environment's secrets. Without
MOBAI_API_KEY, finish the emailed sign-in in the session instead.
Downloads fail partway through
Set network access to full internet and run the setup command again. Release downloads redirect, and package installs reach more hosts than any list anticipates. On Cursor, check whether you turned filtering on; on Claude Code and Codex it is on by default. On Claude Code, the level is called Full.
GitHub Actions cannot be reached
If the agent reports that its GitHub credential cannot use Actions, fix the credential. If the sandbox itself blocks the GitHub API, no token helps: run the workflow from GitHub's web interface, or build on Codemagic or Bitrise, which use their own APIs and only need a git push.
The app will not install on the phone
The build is unsigned, or its provisioning profile does not include the phone's UDID. Run
builder signing setup --distribution development --device <udid> --yes
on your machine (it registers the phone and renews the profile) and rebuild with
--profile development. By hand: register the phone in the Apple
developer portal, refresh the profile and update IOS_PROVISIONING_PROFILE_DEVELOPMENT.
mobai-dev ota
checks both before it serves anything.
It worked yesterday and now it does not
With a free Apple ID the bridge's signing expires after seven days. Connect the phone to your Mac and repeat step 2 of the wizard, Prepare the device.
What is shared
Previews run in your sandbox. Builds and simulators run in your own CI account. Phone traffic goes between the sandbox and your iPhone over your own tailnet. MobAI hosts nothing in that data path.
For each phone you onboard we store its identifier, name, iOS version and an encrypted pairing record, so your sessions can find and connect to it.
Remove a phone from the cloud setup wizard and everything stored for it is deleted. The API key is revoked separately, from API Keys. The binary the sandbox runs is open source: github.com/MobAI-App/mobai-dev.