Per-Worktree iOS Simulators with a Shared OAuth Session
I run practically every agentic coding session in a separate Git worktree. Each worktree gets a dedicated iOS simulator. All simulators share one OAuth session.
The unit of work looks like this:
one task = one branch = one worktree = one simulator
The app authenticates through Telegram and login includes 2FA. A shared session lets me complete that flow once and reuse the result across fresh simulators.
Worktrunk Setup
I use Worktrunk to create, merge, and remove worktrees. Its lifecycle hooks run the project setup and cleanup automatically.
The relevant project hooks are:
pre-start = "scripts/worktrunk-on-create.sh --branch {{ branch }} --branch-slug {{ branch | sanitize_hash }} --primary-worktree {{ primary_worktree_path }}"
pre-remove = "scripts/worktrunk-pre-remove.sh"
I create a coding environment with one command:
wt switch --yes -c codex/my-task
The pre-start hook:
- checks the shared session broker and starts or repairs it under a repository-wide lock,
- creates or reuses a dedicated iPhone simulator named after the branch,
-
writes its UDID to
mise.local.toml, so builds and tests target that simulator.
wt remove shuts down and deletes the simulator recorded for the worktree. The broker stays alive as a repository-level service.
Shared Authentication
Every simulator has its own installed apps, containers, Keychain state, and user defaults. Simulator builds store authentication through a local HTTP service. Physical-device builds use Keychain. The app selects the implementation at compile time with targetEnvironment(simulator).
The broker is a FastAPI service listening on 127.0.0.1. It owns:
- the canonical access token, refresh token, and expiry date,
- one persistent development-machine device ID,
- coordination state for token refresh.
The session and device ID live in one named Docker volume shared by all worktrees. The iOS client reads the session, writes it after login or refresh, clears it on logout, and negotiates refresh ownership.
I vibe-coded the server in FastAPI because it was a fast way to build this local tool. The Swift app talks to it through a small HTTP client.
The server persists files through atomic replacement with mode 0600. The Docker volume contains plaintext tokens, and the service binds to 127.0.0.1. This setup stays on the development machine.
Coordinating Token Refresh
Several simulators can notice an expiring access token at the same time. The broker coordinates them through refresh ownership:
- A simulator asks to refresh a specific access token.
- The broker gives ownership to one client process.
- Other clients wait for the owner to update or delete the session.
-
The owner replaces the exact token used to start the refresh. A stale client receives
409. - Ownership expires after 45 seconds if the owner disappears, allowing another client to take over.
Refresh ownership lives in memory, so the server runs as one Uvicorn worker. The canonical session persists in the Docker volume. Refresh ownership resets with the process.
Preparing a Demo
I log in once through Telegram. Every worktree can then read the same session and open the authenticated part of the app.
Each agent builds and launches the app on its own simulator, navigates through the required flow, and leaves the app on the screen I need to review. Other agents can prepare their simulators at the same time because UI state and app data stay isolated per device.
I open the prepared simulator and perform the final interactions required for the demo. Installation, 2FA, preconditions, and routine navigation are already done.
Session Pools
The current broker has one canonical session. Logout clears it for every simulator.
Most worktrees can use this default pool. A logout or account-switching workflow needs its own pool. The broker can support that by addressing sessions with an ID passed through the worktree environment.
Named pools become useful when two authentication-focused worktrees need separate login state.
Fixed Device Pool
An alternative setup keeps a fixed pool of preconfigured simulators and leases one to each coding session. Every device can keep its own Keychain session, which reduces the session-specific code in the app.
The pool needs device creation, leasing, runtime updates, state resets, login maintenance, and cleanup of stale apps and data.
I keep device ownership tied to the worktree lifecycle:
- Worktrunk creates a simulator when the worktree starts.
- The worktree owns that simulator.
- Worktrunk deletes it when the worktree disappears.
- The broker owns the OAuth session and development device ID.
In daily use I create a worktree, let the hook prepare its simulator, and let the agent leave the app ready for review.