---
title: "An agent that checks your PR on a real iPhone | Mobster"
description: "Use mobster ci init to write a CI workflow for your Mac. It checks your PR's diff, runs checks for changed screens on a real iPhone and posts the verdict."
canonical: "https://mobster.dev/blog/agent-checks-your-pr-on-real-iphone"
last-updated: "2026-11-14"
---

Use mobster ci init to write a CI workflow for your Mac. It checks your PR's diff, runs checks for changed screens on a real iPhone and posts the verdict.

# Let an agent verify your iOS app change before you merge the PR

26 October 2026

A pull request changes the paywall and the settings screen. A reviewer reads the Swift and approves it, and nobody opens the app on a phone. Use `mobster test pr` to read the diff, find the saved checks that cover the changed screens, build the branch and run the checks on simulators and on a real iPhone plugged into your Mac. Use `mobster ci init` to write a GitHub Actions or GitLab CI workflow for that Mac as a self-hosted runner. In it, `mobster ci run` runs `mobster test pr` with video, and `mobster ci comment` keeps one comment on the pull request with the verdict and the command that reruns it.

## From a diff to the checks it needs

`mobster test pr` runs the checks a branch needs, not the whole suite. The steps, from [the testing docs](https://docs.mobster.dev/testing):

1. It reads `git diff` from where the branch left `origin/main`, uncommitted work included.
2. Each changed Swift view becomes a screen: its name, and the accessibility identifiers, strings and deep links on the changed lines.
3. It picks every saved check that names a changed id or string, quotes a changed string in a step, carries the screen as a tag or file name, or was itself changed. When it picks none and an app file changed, it runs every check; a change to docs or tests alone runs the checks tagged `smoke`.
4. It builds the app with `xcodebuild`, for the simulator or, with `--device`, for the iPhone.
5. For each changed screen no check covers, it writes a draft check from the screen’s new ids and strings, at most 3. A draft runs beside the checks and never fails the pull request.

Steps 1 to 3 and 5 are plain code over a diff. No model reads your code. Here they are on a throwaway repository, run through the same functions `mobster test pr` calls. The branch renames a paywall button’s id and its title, and adds a toggle to the settings screen. Two saved checks exist: one for the paywall, one for a screen the branch doesn’t touch.

```
Changed screens: PaywallView, SettingsViewThe paywall shows three plans    expects id upgrade_button    expects “Choose a plan”    names PaywallViewuncovered: SettingsViewPaywallView   ids: upgrade_cta, upgrade_button   texts: Choose your plan, Upgrade, Choose a planSettingsView  ids: reminder_toggle               texts: Settings, Daily reminder
```

The paywall check is picked for three reasons, and two of them are about the removed lines. The diff took `upgrade_button` and “Choose a plan” out, and a check that expects them is exactly the check that should run. The other check is left out: nothing it names changed. `SettingsView` has no check, so Mobster drafts one at `.mobster/drafts/pr/settings.yaml`.

A check is never picked on a common word alone, so a PR that touches “Settings” in a string doesn’t wake every check that mentions it.

## What the run prints

On a Mac with Xcode and a simulator runtime, from the branch:

```
mobster test pr
```

The docs show the output for the same two changed screens in a sample app:

```
Changed screens: PaywallView, SettingsView✗ failed  The paywall shows three plans  on iPhone 17 Pro, iOS 26.4    text “Choose your plan”: not on screen; closest: "Choose a plan"~ draft  .mobster/drafts/pr/settings.yaml  on iPhone 17 Pro, iOS 26.4, passedMobster checks: 0 passed, 1 failedComment  .mobster/test-results/20261009-150652-1215/pr-comment.mdGIFs     .mobster/test-results/20261009-150652-1215/pr/gifs (2)
```

The failure says what the check expected and what the screen had. Mobster checks saved assertions ([how a verdict is decided](https://docs.mobster.dev/checks)). The suite folder also holds `pr.json` (the changed screens, each check picked and why, the drafts), the frames with the expectations outlined, and a captioned GIF of each failed check. The exit codes are `mobster test`’s, counted without the drafts. A build that fails exits 3 and the comment quotes its first errors.

On a real iPhone, add the phone. `mobster test pr --device "Test iPhone"` builds for the phone and installs that build. With `--testflight com.example.yourapp` it skips the build and tests the TestFlight build already on the phone.

## A Mac of yours is the runner

This setup runs the tests from a Mac, with the iPhone plugged into it. So the runner is a Mac you own, and a Mac mini on a shelf is the usual choice. Mobster hosts no runners or phones, and charges nothing per run or per device. Your code, builds and run artifacts are stored on your Mac and in your CI host’s artifacts. During model-driven steps, screen content also goes to your AI provider.

Setup is a standard user for the runner, automatic login, a Mac that never sleeps, Xcode, and the runner service as that user’s LaunchAgent. The docs’ page on a Mac as your runner has each step, [Running checks in CI](https://docs.mobster.dev/ci) has the rest, and [Run iOS UI checks in CI without a device farm](https://mobster.dev/blog/ios-ui-checks-ci-no-device-farm) walks through the workflow file and the reports. Then, in your project:

```
mobster ci init
```

```
Wrote .github/workflows/mobster.yml.Next:  1. On the Mac that runs the checks: mobster ci doctor  2. Register it as a self-hosted runner with the label mobster: https://docs.mobster.dev/ci/mac-runner  3. Add ANTHROPIC_API_KEY as a repository secret, for checks with steps.  4. Commit the file and open a pull request.
```

The workflow it writes runs on a runner with the `mobster` label, so your other jobs stay on hosted runners. The core of it is two commands: `mobster ci run` fetches the base branch and runs `mobster test pr` with `--video`, and `mobster ci comment` creates the comment or updates the one it made on an earlier push. The comment never fails the job. GitLab gets the same job in `.gitlab-ci.yml`.

`mobster ci doctor` tells you what the Mac still needs before the first job. This is the docs’ example of a runner with two things to fix:

```
✓ Apple silicon              this Mac is arm64✓ Xcode                      Xcode 26.4✓ iOS Simulator runtime      iOS 26.4✓ Mobster for Mac            in Applications✓ Mobster plan               active on this Mac✗ System sleep               after 10 minutes                             Run `sudo pmset -a sleep 0 disksleep 0`, so the Mac never sleeps through a job.! Restart after a power cut  off                             Run `sudo pmset -a autorestart 1`.✓ Automatic login            as ci✓ Runner service             GitHub Actions runner✓ Free disk                  212 GB✓ Phones this runner allows  none: checks run on simulators only
```

A row marked `✗` fails and names the fix. A row marked `!` works and is worth fixing.

## The comment

Each push updates one comment. A row per check, with what it expected, what it found and the frame. A run on one device names it once, after the commit:

```
### Mobster checks: 11 passed, 1 failedChanged screens: `PaywallView`, `SettingsView`, at `a6d8d39` on iPhone 17 Pro, iOS 26.4| Check | Expected | Found | Frame || --- | --- | --- | --- || **Failed** The paywall shows three plans | text “Choose your plan” | not on screen; closest: "Choose a plan" | paywall.jpg || **Passed** Marking a task done lowers the count | 3 expectations | as expected | tasks.jpg |
```

Each check that didn’t pass gets a folded repro: the device, the deep link it opened, the steps it took, what it expected and found, its video and GIF, and the command that runs it again on the same device. Up to 5 checks get one. Typed text, email addresses and phone numbers in a step show as `•••`. GitHub doesn’t show images from a workflow’s artifacts inside a comment, so frames, GIFs and videos are links to the uploaded report.

With Mobster Checks, the Mobster GitHub App, installed on the repository, the verdict also shows as a check named `Mobster`: success when every check passed, failure when one failed or needed review, neutral when no saved check covers the pull request. Add `Mobster` as a required check in the branch’s protection rules to block a merge on a failure.

## Your iPhone, and who may reach it

A pull request can edit the workflow file. So a phone named only in the workflow doesn’t reach it. The runner’s owner lists the phones it may test, and any TestFlight apps, in the runner’s own `.env`. These settings are separate from the workflow file; pull-request code still runs as the runner user:

```
# ~/actions-runner/.envMOBSTER_RUNNER_DEVICES=Test iPhoneMOBSTER_RUNNER_TESTFLIGHT=com.example.yourapp
```

```
      - name: Test what this PR changed        run: |          "$MOBSTER" ci run --device "Test iPhone" --testflight com.example.yourapp
```

- A `--device` or `--testflight` the runner doesn’t list stops the job before a check runs.
- On a phone, a check drives only builds you installed: the device build from the job, a build Xcode installed, or a TestFlight build you named. An App Store app can’t run.
- Smart’s steps stay in your app, and never send, buy, post or delete on the phone.
- A replayed step that sends, posts, deletes or buys asks the same approver a live step asks, and on an iPhone every commit is refused. [The safety docs](https://docs.mobster.dev/safety) list what counts.
- With several phones, list each in `MOBSTER_RUNNER_DEVICES` and add `--spread` so each check runs once on whichever phone is free.
- Use a test phone signed in to a test account. Keep it unlocked, awake and charging.

One rule you can’t configure around: a self-hosted runner runs the code of every pull request it picks up, as the runner’s user, on the Mac the phone is plugged into. Don’t connect one to a public repository that takes pull requests from forks. GitHub’s own [hardening guide for self-hosted runners](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions) explains why.

## When the agent is your coding agent

Your coding agent can start the same run. Over MCP, `run_tests` with `pr: true` reads the diff, picks the checks, drafts the missing ones, builds, runs, and answers with the verdict, each check’s frame and GIF, the failing frame as an image and the comment’s path. A long build answers with a `suite_id` that the agent waits on.

The phone is a separate decision, and the server makes it, not the agent. `pr: true` runs on a real iPhone only when the server was started with `--allow-device` naming that phone, and tests a TestFlight build only when `--allow-testflight` names the app too. Without the flags the `device` argument is refused, so an agent can’t widen either. The simulator needs no flag. [AI end-to-end tests for iOS](https://mobster.dev/blog/ai-e2e-tests-ios-assertions) covers the loop and the assertions, and [the open-source Mobster engine](https://mobster.dev/blog/why-mobster-is-open-source) covers the code that stops an agent at a send button. The MCP server and the `mobster` command are free and open source too: install them with `curl -fsSL https://mobster.dev/install.sh | sh`, or use the ones inside Mobster for Mac.

## Set it up

Install [Mobster for Mac](https://mobster.dev/download) on the runner, run `mobster ci doctor`, then `mobster ci init` in your project and open a pull request. The first run of a check with steps needs a model key. Launch-only checks, the ones with no `steps`, need none. [The CI docs](https://docs.mobster.dev/ci) have every flag, including `--shard` to split a suite across Macs.

## Questions

- ### Can I test an iOS pull request on a real iPhone in CI?
  Yes, on a Mac of yours. Plug the iPhone into a self-hosted runner, list its name in MOBSTER_RUNNER_DEVICES in the runner's own environment, and run mobster ci run --device with that name. Mobster hosts no runners and no phones.
- ### Does Mobster run every check on every pull request?
  No. mobster test pr reads the diff, picks the saved checks that name a changed accessibility id or string, and runs those. When it picks none and an app file changed, it runs every check; a change to docs or tests alone runs the checks tagged smoke.
- ### Can a pull request point the runner at a different phone?
  Mobster checks the device against the runner's own .env file. A device named only in the workflow file stops the job before a check runs. Pull-request code still runs as the runner user.
- ### Can my coding agent run this on my iPhone?
  Over MCP, run_tests with pr true runs on a real iPhone only when the server was started with --allow-device naming that phone. Without the flag the device is refused.

## Sources

- [Mobster docs: running checks in CI](https://docs.mobster.dev/ci)
- [Mobster docs: test a pull request](https://docs.mobster.dev/testing)
- [Mobster docs: a Mac as your CI runner](https://docs.mobster.dev/ci/mac-runner)
- [Mobster docs: Mobster Checks, the GitHub App](https://docs.mobster.dev/ci/github-app)
- [Mobster docs: MCP server](https://docs.mobster.dev/mcp-server)
- [GitHub docs: security hardening for self-hosted runners](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions)

## Try it on your own iPhone

Download Mobster for Mac

## More from the blog

### [Run iOS UI checks in CI without a device farm](https://mobster.dev/blog/ios-ui-checks-ci-no-device-farm)

26 Oct 2026

### [How to test an iOS app with an AI agent](https://mobster.dev/blog/ai-e2e-tests-ios-assertions)

8 Oct 2026

### [Test your iOS app with Codex](https://mobster.dev/blog/test-ios-app-with-codex)

14 Oct 2026
