Open source · local-first · macOS

When local iOS Simulator test runs hang.

XCSteward is an OSS macOS tool for predictable XCTest, xcodebuild, simctl, and iOS Simulator execution. Single runs can hang at boot, destination resolution, or readiness; coding agents, scripts, and CI-like workflows sharing one Mac amplify that fragility.

Why this exists

The pain did not start with CI. It got much worse the day coding agents started running iOS tests for me — across more than one app at the same time, on the same Mac.

A single xcodebuild or Simulator run is already fragile on its own: runs hang before tests start, simctl stops responding, CoreSimulator wedges. Once several agents, scripts, CI-like jobs, and a human can all touch Xcode tooling on one machine, those same failures get frequent and stop being reproducible.

Is this for you?

XCSteward is narrow on purpose. It targets operational fragility around local test execution — not your test code.

A good fit when

  • Local simulator test runs that hang before tests start.
  • xcodebuild getting stuck resolving or booting destinations.
  • simctl or CoreSimulator getting wedged or deadlocked.
  • Cleanup and preboot scripts that have crept into your test ritual.
  • Local, CI-like test workflows running on a Mac.
  • Coding agents — or several scripts, jobs, and humans — touching Xcode tooling.

Not a fit for

  • Broken tests that fail on their own logic.
  • App-level UI test flakiness and timing bugs in your test code.
  • Code signing and provisioning problems.
  • Missing or un-downloaded Xcode runtimes.
  • Bugs inside a specific simulator runtime / vendor image.
  • Network, backend, or mock-server instability.

How XCSteward helps

It does not rewrite your tests or replace xcodebuild. It governs how runs are executed, isolated, and cleaned up so the simulator subsystem stays in a state you can reason about.

Human UX, machine contract

XCSteward is a local CLI, not a dashboard, SaaS, or MCP layer. The current developer experience has two surfaces: compact human output for watching long runs, and a JSON contract for agents and automation.

For humans

Plain submit --wait is no longer a silent black box. It prints the queued job id, status/log/watch/follow commands, job directory, and compact wait updates. From another terminal, status <job-id> --watch [--interval <seconds>] polls until the job is terminal and logs --follow streams the combined log until completion.

xcsteward submit --project app --wait --wait-timeout 900
xcsteward status <job-id> --watch
xcsteward logs <job-id> --follow

For agents and automation

Machine users should keep using --json. Long-running JSON waits can add --progress for JSON-lines events on stderr while stdout stays reserved for the final JSON document. When command events are available, those progress events add phase and phase_elapsed_seconds. For streaming status, status <job-id> --watch --json emits newline-delimited full JobSummary objects.

xcsteward profile init --detect --json
xcsteward submit --project app --wait --wait-timeout 900 --json --progress --env API_BASE_URL=http://127.0.0.1:8080
xcsteward explain <job-id> --json

When setup fails before XCTest attaches

If CoreSimulator fails before XCTest attaches, XCSteward reports an environment failure with the simulator error and artifacts, rather than making you decide from a silent terminal or raw xcodebuild output whether the app regressed. The result class runner_bootstrap_failure means runner or environment setup failed before XCTest attached; it can come from simulator boot, destination, launch session, artifact, or runner setup issues.

Signatures such as Unable to boot the Simulator, launchd failed to respond, Failed to start launchd_sim, NSPOSIXErrorDomain code=60, Failed to prepare device, or testmanagerd connection loss are classified separately from real test failures. submit --wait, status, and explain --json preserve the underlying detail and may suggest a bounded next step, such as shutting down or erasing the selected simulator before retrying once.

If the test command times out before XCSteward observes XCTest attach or test-execution evidence, it still stays runner_bootstrap_failure with diagnostic_excerpt.subtype = pre_xctest_timeout. Plainly: the test command hit its timeout before XCSteward observed XCTest attach evidence. The final summary says XCTest did not attach before the test command timed out, and terminal JSON can include the phase, timeout seconds, evidence paths, and a capped diagnostic excerpt.

xcsteward status <job-id> --watch
xcsteward explain <job-id> --json
xcsteward logs <job-id>

When a job is still queued or in simulator/bootstrap setup and the combined log is not ready, logs <job-id> points back to status <job-id> --watch instead of turning a pending log into an opaque missing-file error.

Failure-mode library

Symptom-first writeups of the failures XCSteward is built around. Each page has quick checks, manual mitigations, and an honest note on fit — useful even if you never install anything. A few to start with, from a library of 18:

See all 18 failure modes →

Try the alpha

Have a real failure mode around xcodebuild, simctl, Simulator readiness, cleanup, local CI-like workflows, or coding agents running iOS tests? Try the OSS alpha, or open an issue with your setup. It is local-first — no signup, no waitlist. The early feedback that helps most is specific: real logs, real setups, real failure modes, and the cases where XCSteward does not help.

Install with Homebrew:

# requires macOS with a recent local Xcode
brew tap acyment/tap
brew install xcsteward

Dogfooded across 16 real iOS projects and 160 local jobs — each tracked in the public dogfood ledger. It is honest about where it stands: early, alpha-stage OSS, narrow on purpose.

Read the full alpha & reporting guide →