Command reference
Commands use stim. If Stim is not installed globally, replace stim with
npx stim.
Run stim <command> --help for parser help. Run stim guide for the full
reference that ships with the installed version. Every refusal code has an
entry in the troubleshooting reference.
start --json, ios --json, android --json and macos --json report
agentDevice: { stateDir }, an absolute path under
$STIM_HOME/workspaces/<name>/agent-device/. Each status --json environment
reports the same field, shared by its slots, without creating the directory.
Set AGENT_DEVICE_STATE_DIR to it when driving a device yourself; see
agent-device actions.
Normal workflow
- Global
- npx
stim doctor
stim start
stim ios # or: stim android, or: stim web
stim logs --errors
stim stop
npx stim doctor
npx stim start
npx stim ios # or: npx stim android, or: npx stim web
npx stim logs --errors
npx stim stop
React Native and Expo Debug runs use a dev server and start it when needed. Their Release builds embed JavaScript and skip that requirement. Native Xcode apps run without Metro in every configuration.
When Stim Desktop is installed, worktree warm, start,
ios, android and web print an HTTPS workspace link, on
stderr, and add it to their --json payload as links.desktop:
Open in Stim: https://stim.appandflow.com/open?v=1&machine=<home-id>&workspace=<workspace-id>
The agent guide tells agents to share this link with you once, when they begin work in a workspace.
reload is a recovery command that reloads JavaScript in the live app and never
restarts it. Use it when an error screen remains after a fix, not after every
JavaScript edit. It also recovers an Android app whose first bundle failed; an
iOS app in that state never connects to Metro, so reload cannot reach it.
Named device slots
ios, android, device lock, device unlock, logs, and stop accept
--slot <name>. Omitting it selects the default target for a device run;
plain logs and stop still cover the whole workspace. status lists every
slot. Slots can hold multiple simulators of the same model as well as physical
devices. See multiple devices with slots
for commands, a copyable agent prompt, and shared-server limitations.
doctor
stim doctor [--platform <ios|android>] [--json] [--fix]
Inspects the current app and, in a repository with linked worktrees, the source
checkout's fitness as a seed. It reports missing or stale dependencies,
CocoaPods state, cache conflicts, device capacity, remote session problems, and
a linked native library whose Git metadata enters the fingerprint. On a
checkout without installed dependencies, it also reports fingerprint
differences against a fresh worktree. The checkout is left untouched unless
--fix is passed.
Doctor never loads fingerprint.config.js or fingerprint.config.cjs: when
either exists, it skips the fingerprint checks and reports that it did. It reads
a project eas-cli's version from its package.json, and runs eas whoami only
with an eas on PATH outside the repository and outside any node_modules/.bin
above the project. On a checkout without its own dependencies, it skips the
fingerprint parity check and says why when Expo packages resolve from outside
the app or when the check would need npx react-native config, which it does for
Android or a project without an ios directory. Its linked-library check can still run installed Expo packages on the
main checkout, which evaluate a dynamic app config, as a build does.
worktree warm, start, ios, and android run project code.
--platform ios or --platform android limits native findings to that
platform while keeping shared project checks. The selected project integration
supplies framework checks and repairs; React Native and Expo checks include
Metro, CocoaPods and fingerprint parity. Shared findings appear before
framework findings. Each run in a supported native app is recorded for the
inspected platforms in Stim's state, which also registers the project for
stim status, so stim guide can tell when doctor is due again. A run in a
directory that is not an app records nothing.
The offload-candidate note appears after at least 3 successful local cold builds in 7 days average over 3 minutes, with no remote.machines and an online tailnet Mac; it points to Stim Desktop Settings > Remote Machines > Add.
The readiness-markers-missing note appears when no JavaScript or TypeScript source under the app directory contains [stim:readiness]. The scan skips node_modules, the native project and build output directories (ios, android, macos, windows, Pods, build, dist, web-build, coverage) and dot-directories. It does not read shared packages outside the app directory, and it reports nothing in an app with more than 5000 source files. It points to stim guide lifecycle readiness and the app-declared readiness logs.
On macOS, a memory-culprit finding names each process whose physical
footprint is abnormal, with its restart command when one is known to be safe.
See memory pressure.
Doctor also prints the running CLI version and the stim installation resolved
from PATH, and flags a resolved installation that is older than another
available one.
On macOS, a Toolchain section reports where node, ANDROID_HOME, JAVA_HOME,
stim, brew and adb come from: this shell's environment, or the location
stim-server would discover them at without a login shell (a version manager's
default Node, Homebrew, ~/.local/bin, the standard Android SDK, a system or Homebrew JDK
or Android Studio's bundled runtime), with how to override each. When the app
pins Node in .nvmrc, .node-version, .tool-versions or package.json's
volta.node, it also names the installed Node Metro runs on when the node on
PATH does not satisfy the pin: an nvm, fnm, Volta, asdf, mise or linked
Homebrew node/node@NN install. When no install has the version the pin
names, it names the newest installed Node of the pinned major that is not older
than the pin, and says so. --json adds this as toolchain.
A budget line reports the free disk on the volumes that hold the app and
$STIM_HOME, and the estimated committed memory, against the
machine budget. When the machine is over
budget, a finding lists what the next start, ios, or android would reclaim
first. Doctor itself never reclaims. --json adds this report as budget.
doctor also flags when an agent harness sandboxes shell commands and Stim is
not allowed through it, which shows up as unrelated-looking failures against
the simulator service, the adb server, and Stim's own state directory.
For that finding, --fix writes the missing allowance into .claude/settings.local.json at the
repository root, the per-user file, merging it with whatever is already there
and preserving other settings. It cannot add a Codex allowance because that
sandbox has no per-path allowance to add. This repair runs only when the
report shows that finding, so an unsandboxed session leaves the file alone.
See stim guide errors sandbox for the failure signatures and the manual
settings.
Unless --platform ios is selected, --fix also removes stale ignored,
untracked Android .cxx configurations with obsolete compiler launchers,
including those in installed native modules. Stop native builds before this
repair: its cache-lock check cannot detect uncached, release-swap fallback, or
direct Gradle builds. The next build recreates these files; source, custom launcher settings,
and shared ccache entries are preserved. See stim guide lifecycle options.
Doctor reports a checkout that holds linked worktrees inside it, such as
.worktrees/<name>, when the checkout's .watchmanconfig does not list them in
ignore_dirs. A Watchman root at that checkout, which Jest or a Metro started
there registers, crawls every worktree's files, node_modules, and build output,
which grows the shared Watchman daemon's memory and its recrawls. The finding
says when Watchman watches the checkout now. --fix merges the worktrees' shared
parent directory (such as .worktrees) when Git tracks nothing in it, otherwise
each worktree path, into that .watchmanconfig, keeping every other key and
entry. It refuses a file that is not a JSON object or whose ignore_dirs is not
an array, and names the entries to add by hand. Watchman matches entries
literally, so .worktrees/ or ./.worktrees ignores nothing. Commit
the file: Watchman reads ignore_dirs only from a root's .watchmanconfig, with
no global equivalent. It reads the file only when it adds a root, so for a root
it already watches run watchman watch-del <checkout>, then restart Watchman to
free its memory now. Doctor never runs either command.
When remote.machines names remote machines,
doctor reports their separate device-host approval in deviceHosts. Only
--fix requests approval, retries a revoked or lapsed request, and
forgets names removed from the setting. It makes a setup ticket and prints the
stim-server setup command a person on the hosting Mac runs. Setup tickets stay
private. The first connection after approval pins the Mac's stim-server key,
and a Mac that cannot prove that key is refused; unreadable credentials and
uncertain replies preserve the pin. Invalid hosting
settings report an error and preserve every saved credential. JSON states
are approved, pending, not-asked, revoked, lapsed, key-changed, outdated,
not-on-tailnet, tailscale-off, unreachable, invalid,
credentials-unavailable and busy. A pending entry carries expiresAt,
when its setup ticket expires, except for a Stim Desktop ticket; lapsed means
the request expired before approval. Hosting approval does not yet change
ios or android placement.
When the remote.machines setting names
remote machines, doctor reports each one this machine is not approved on: not on the
tailnet, not asked yet, waiting for approval (with the stim-server setup
command to run there), revoked, lapsed, answering with a different
stim-server key than the one this machine pinned at approval, or running a
stim-server too old to speak protocol 2 (outdated, with the
stim-server service update --release <version> command to run there). A listed machine
that never granted an approval is not used for it. --fix makes one setup
ticket for each named machine without a pairing, covering build and device-host
access, asks again one that revoked or let the request lapse, and forgets the
pairing of a machine no longer named. It never accepts a different key: remove
the machine from remote.machines and run --fix to forget the old pairing,
then add it back. --json lists
each named machine's build approval under remoteMachines, and its device-host
approval under deviceHosts, with a state: approved, pending, not-asked,
revoked, lapsed, key-changed, outdated, not-on-tailnet, tailscale-off, unreachable or
invalid. A pending entry carries expiresAt, when its setup ticket expires;
until a person runs setup, the remote machine answers unknown-secret and every
doctor run tries again. lapsed means the ticket expired before approval, while
revoked means the remote machine revoked this machine.
Approved entries in remoteMachines and deviceHosts also include
host: { name, screenRecording, accessibility } when the worker reports its
Stim Host permissions; otherwise host is omitted.
Doctor also asks each approved machine for one build offer, the same offer
stim ios and stim android ask for, and reports every reason that machine
would not take this app's builds now, each as a finding with a fix: it does
not answer, it runs an incompatible Stim build (update it to this machine's build), its
CPU differs; for iOS (unless --platform android) its Xcode, simulator SDK or
CocoaPods selected for this project differ, it has no Bundler for a project whose Gemfile.lock pins
CocoaPods (its own CocoaPods version does not matter then), or it has no
iPhone simulator on the runtime stim ios builds for here; for Android (with
--platform android, or an app with android/ or Expo) its JDK major differs
or its SDK lacks the NDK, build-tools or compile platform; its worker volume has less than 10 GB free;
or it is busy. A machine that is not a Mac cannot build iOS or macOS apps: it
reports one platform reason for them instead of every missing Apple tool,
and when it can build another of the app's platforms, such as Android on
Linux, doctor leaves those platforms out and judges it on the rest. Busy and
no answer are notes; the others cost time. In --json such a machine also carries offloadable,
reasons, problems (each reason with its finding code), and capacity,
its reported load:
{
"machine": "janics-mac-mini",
"state": "approved",
"offloadable": false,
"reasons": ["Stim build 6bbe9103995f7eb6 there, e7749c9011f4d423 here"],
"problems": [{ "code": "stim-build", "reason": "Stim build 6bbe9103995f7eb6 there, e7749c9011f4d423 here" }],
"capacity": {
"running": 0,
"max": 1,
"cpus": 10,
"loadPerCore": 0.4,
"builds": 0,
"maxBuilds": 0,
"maxLoadPerCore": 2,
"declined": null,
"diskFreeBytes": 812000000000,
"minDiskFreeBytes": 10737418240,
"memoryUsedBytes": 9663676416,
"memoryTotalBytes": 17179869184
}
}
ports
stim ports
stim ports get <label>
stim ports stop [label] [--dry-run]
stim ports release [label]
Reserves TCP ports 8900–8999 for web or API servers started by the
project. get prints only the number and reuses an existing allocation.
New allocations skip reserved and occupied ports; retry notices go to stderr.
Allocation reads native TCP tables through netstat on macOS and Windows and
/proc/net on Linux. When the table is denied, empty or unreadable, it checks
each port with lsof and loopback connects instead, and refuses only when
none of them can answer. Stopping listeners still requires lsof on macOS and
Linux, or netstat on Windows.
ports lists named allocations and Metro, marked managed.
Labels start with a letter and contain up to 64 letters, digits, underscores,
or hyphens. metro is reserved for stim start and stim stop.
The workspace is the nearest package.json directory. In a monorepo, a package
that is not a React Native or Expo app, such as a Vite web app in apps/web,
uses the one Stim app registered in the same Git worktree instead, and stderr
names it. web, settings, logs, reload, stop and status follow the
same rule; status stars the app instead of printing the note. Every ports command there then acts on that app's ports, including
stop and release without a label. status, worktree remove and gc
treat the ports as the app's. A package that already holds ports keeps them
until you release them. With no registered app, or more than one, run ports
from the app directory.
stop terminates listeners on the selected named ports, including processes
outside the workspace, and prints their PIDs and commands. It sends SIGTERM,
then SIGKILL after two seconds if needed. --dry-run previews without killing
or releasing. Failed stops retain the allocation. release removes the
reservation without signalling the server. Omit the label to select all named
ports. Neither command touches Metro; stim stop leaves named ports alone.
worktree remove, gc --delete and automatic maintenance release named ports
without signalling their listeners. gc reports allocations for missing
workspaces, and gc --delete releases them. Unmounted or unresolved workspace
paths remain registered.
See server examples and limitations.
start
stim start [--wait <seconds>] [--remote] [--reset-cache] [--json]
Starts the project dev server on the workspace's reserved port. Stim supervises
the process and captures its output. A healthy existing server for the same
project is reused. A Debug ios or android run starts the dev server the
same way when it is not running, so running start first is optional.
--wait <seconds>changes the startup timeout. The default is 60 seconds. It waits for the dev server;--waitonios,android, anddevice lockinstead bounds the wait for a device another workspace holds.--remoteprepares Metro for a remote device.--reset-cacherestarts only this app's verified owned Metro, preserving its port and devices, with Metro's own reset (resetCacheon a bare server,expo start --clearon Expo). Every store in the app's Metro config is cleared, including this app's shared transform store, so other worktrees of the same app rebuild their transforms too; the file map is rebuilt. Other apps and native build caches are unchanged. Externally started servers are left alone, and a failed startup can be retried withstim start.--jsonprints one stable result object on stdout.
ios
stim ios [--slot <name>] [--scheme <name>] [--configuration <name>] [--device-type <name>] [--runtime <version>]
[--simulator-app <xcode|siniulator|stim-desktop>] [--device [udid]] [--wait <seconds> | --no-wait] [--remote <eas|proxy|auto|local|machine>]
[--eas-profile <name>] [--no-metro-check] [--no-build-cache] [--plan] [--json]
Builds or restores the iOS app. Stim then boots an owned simulator, installs the
app, opens it, and checks launch logs. Native builds run locally by default;
--eas-profile downloads an existing EAS development build. A simulator Debug
build can compile on a paired remote machine instead: see remote.build in
machine settings.
--configuration <name>selects an Xcode configuration. The default is Debug.--remote-build <local|auto|remote|remote-only|name>overridesSTIM_REMOTE_BUILDandremote.build.remotetries the remote machines first, inremote.machinesorder, and builds here only when none takes the build;remote-onlyrefuses instead. A name requires that configured, paired worker and fails withSTIM_OFFLOAD_REFUSEDwithout local fallback. Unlisted or unpaired names refuse at setup before checking the cache, without a build record or failed-run stats. A listed paired name with a cache hit contacts no worker. Prebuild and pod install run on this machine before a named offload, after a check of the worker's toolchain. A refusal starts no local xcodebuild, Gradle or SwiftPM compile. Runstim doctor --fixto ask for build access if not paired; a person on the worker approves it by running thestim-server setupcommand doctor printed, with its ticket and expiry. See machine settings for supported builds and remedies.--scheme <name>selects an exact shared Xcode scheme when the automatic app selection is not the one you need. Explicit schemes have separate build caches and DerivedData. This is a build scheme, not the app's URL scheme.--device-type <name>creates this workspace's owned simulator as that model, overridingios.deviceTypefor one invocation. A model no installed runtime can create refuses withSTIM_BAD_ARGand prints the ones they do offer.--runtime <version>creates it on that iOS runtime, overridingios.runtimethe same way. It takes a version (26.5) or a runtime's full name (iOS 26.5), exactly. When the workspace's simulator already runs another installed version,stim iosrefuses instead of booting it: remove the simulator withstim worktree removeorstim gc --delete, or pass--slot <name>to create one beside it.- On an eas/proxy run (
--remoteorios.remote),--runtimerefuses withSTIM_BAD_ARGbecause the remote backend chooses the iOS version, and so does--device-typeon the proxy backend.--remote eashonors--device-typeby starting the EAS Simulator session witheas simulator:start --device <name>, which needs eas-cli 22.2.0 or later. A recorded EAS session still running another model refuses withSTIM_REMOTE_DEVICE_MISMATCH; runstim stop, then rerun. Theios.deviceTypeandios.runtimesettings are ignored on an eas/proxy run. A named hosting Mac uses both selectors. --simulator-app <xcode|siniulator|stim-desktop>overrides the machineiosSimulatorApppreference for this run. It also opens an already running owned simulator in that app without rebooting it. The preference is not saved. Local simulators only; cannot be combined with--deviceor a remote target.--device [udid]builds, installs, and launches on a connected iPhone instead of the owned simulator. With no UDID it takes the first connected device it can lease. It ignoresios.remoteand cannot be combined with--remoteother than--remote local. Stim never creates, boots, or deletes hardware.--wait <seconds>bounds the wait for a physical-device lease (default 60;0refuses immediately if busy). Without--device, it bounds the FIFO owned-device slot wait across the Stim home (default 600 seconds). Remote targets do not join this local slot queue. At the cap, only the queue head reclaims the longest-idle eligible owned device in another workspace, one per attempt, at most once every 15 seconds, then rechecks capacity.devices.reclaimIdleMinutesdefaults to 10;0disables it. The same idle checks as supervisor shutdown (default 30 minutes) exclude drivers, locks, builds, viewers and recent activity. Reclaim shuts down, never deletes, and excludes this workspace, physical, hosted, remote, parked and other homes' devices. Failed reclaim is logged and skipped.--no-waitbypasses leasing, including when another workspace holds the device. Installing the same app terminates that workspace's running app. Without--device, it refuses a full device cap or existing slot queue immediately. It cannot be combined with--wait.--remote <machine>runs on a named approved Mac inremote.machines, with no local fallback.autoplaces on an approved Mac when this machine is full or busy, falling back to the local device queue when no host admits.remotetries the approved Macs first, inremote.machinesorder, and runs here only when none admits;remote-onlynever runs here and waits for a host with room. See hosted iOS. Withhosting.agentDriver=agent-deviceon the host and agent-device 0.21.20 or later on both machines,ios.host.agentreturns a per-slot 0600 remote config. Useagent-device <command> --remote-config <file>from status, starting withopen <bundleId>. It reaches only that hosted simulator; inventory, installs, uploads, device selectors, shutdown and host paths are refused.stim stopcloses its connection and removes the config.--remote proxyuses a configured Agent Device daemon.--remote easuses an EAS remote simulator. It needs eas-cli 21.6.0 or later.--eas-profile <name>selects a compatible EAS development build, including with--device. It needs eas-cli 18.9.0 or later. A miss stops and prints an EAS build command; cloud builds require authorization. Cannot be combined with--scheme,--configuration, or--no-build-cache.--no-metro-checkskips the Debug dev-server check and does not start the dev server. Hosted Debug iOS refuses this flag withSTIM_BAD_ARG; it requires the local supervisor's private gateway support.--no-build-cacheignores cached artifacts and replaces the matching entry.--planpredicts the next build instead of running it. See Predict the next build.--jsonprints one stable result object on stdout. It includesbuildMachine(the selectedauto,local, or machine name) andbuiltOn(hereor the worker name, absent when no build ran, including cache hits).
A React Native or Expo Debug run starts the workspace's dev server as stim start would when it is
not running, including after an idle stop. With eas/proxy --remote, it starts it as
stim start --remote would. The JSON result then carries
devServer: { "started": true, "reason": "not running" | "stopped (idle)" }.
The run refuses only when that start fails, with the start's error code.
A React Native or Expo non-Debug configuration embeds its JavaScript bundle.
A locally compiled device build is local-tier only. Its cache key ends -device, so it cannot
collide with a simulator build, and no build-cache provider or Expo remote cache
is read or written on that path, because every entry they hold is keyed
for the simulator. --eas-profile <name> --device uses EAS CLI's artifact cache and
installs the signed app without re-signing it.
A --device run installs with devicectl device install app and launches with
devicectl device process launch. The iPhone can be cabled or paired over
Wi-Fi; an Apple TV, Vision Pro, or simulator is never picked. With no UDID, a
cabled iPhone that is paired and has Developer Mode on is taken first, and a
Wi-Fi one only when there is none. A Wi-Fi run prints one line saying so,
allows each install step 15 minutes and the launch 120 seconds, and fails with
STIM_DEVICE_WIRELESS_FAILED when devicectl times out or loses the phone;
connect the cable and run again. A locked phone or another cause devicectl
names keeps its own error and remedy. Every device install is signed, Debug
included, so the app's own embedded.mobileprovision must be unexpired and must
name the phone, and the identity it names must be in this machine's keychain
whenever Stim modifies the bundle. A build never registers the phone or creates
a profile: when signing fails for a new phone, run
stim device provision ios or build once from Xcode.
In React Native or Expo Debug, the phone reaches Metro over the LAN, because it shares no loopback
with the host and USB carries no reverse forward. Stim gates a non-internal IPv4
address as this workspace's Metro, then hands it to the app: an expo-dev-client
app through the deep link (--payload-url), a bare app by writing
<addr>:<port> into a copy of the bundle's ip.txt and re-sealing that copy.
The cache entry is never modified. Set ios.lanHost when this machine has several
interfaces and the phone shares one that is not the first.
Two things a phone needs that a simulator does not, both one-time and both taps
on the phone: trusting the developer certificate under Settings > General > VPN &
Device Management, and allowing the Local Network prompt the first time the app
looks for Metro. Neither can be pre-granted from this machine. The trust tap has no
API at all and is always the user's; the Local Network prompt can be accepted by
a device tool once it is showing, and Stim's unverified remedy prints those
commands when this launch's device log carries iOS's path reason for an
ungranted app. A prior Don't Allow logs the same reason, and the remedy covers
that too. Until it is granted, launched comes back unverified. Run
stim guide errors unverified for the signature and the full recovery.
A React Native or Expo --device run in a Release configuration builds fresh every time: a cached
Release app carries its builder's JavaScript, and Stim does not swap JavaScript
into cached iOS physical-device builds.
Native Xcode apps
Run from the directory containing the .xcodeproj or .xcworkspace; no
package.json is required. React Native and Expo apps keep their existing
integration.
- Global
- npx
stim doctor --platform ios
stim ios --scheme MyApp --configuration Debug --json
stim ios --scheme MyApp --configuration Release --json
stim logs --errors
stim stop
npx stim doctor --platform ios
npx stim ios --scheme MyApp --configuration Debug --json
npx stim ios --scheme MyApp --configuration Release --json
npx stim logs --errors
npx stim stop
Replace MyApp and the configuration with names from your project. Shared Run
schemes select application targets; a unique application target can also use
its automatic scheme. A workspace owns its referenced projects. --scheme
selects an exact name, and duplicate container/scheme matches refuse rather
than guessing. --configuration overrides ios.configuration in .stim.json,
then defaults to Debug. Custom configurations such as Staging work the same way.
Every native configuration uses process launch without Metro or JavaScript
bundle injection. Results report metroPort: null; launched: true establishes
a live process, so verify the expected UI and interaction separately. Rerun
stim ios after native source edits. stim reload reloads JavaScript.
Existing simulator ownership, physical-device leases and signing checks, logs,
and scoped cleanup still apply.
Artifact identity includes configuration, source and known dependency inputs,
toolchain and build options. Native Release cache reuse is independent of
releaseBundleSwap, and compatible Xcode builds use the shared compilation
cache. --plan predicts the same local artifact without preparing dependencies
or choosing a device. An unbounded input graph skips artifact caching and
reports why: examples include shell build phases, custom build rules, C-family
header graphs, Swift package graphs and unresolved external inputs or compiler
overrides. Planning refuses these cases; run without --plan to build locally.
A compatible approved remote Mac can compile a native Xcode app with
--remote-build auto or --remote-build <name>. Both peers must support
native-xcode-build. The worker path requires a verified artifact identity and
all inputs contained in the repository and visible to git. Ignored or external
inputs keep the build on this machine, and placement names the first one, instead of
being silently omitted. The common case is a Finder .DS_Store that a global
gitignore hides: delete it, or stop ignoring it, to offload. Before compiling,
the worker compares its own toolchain and compiler environment identity with
this Mac's and refuses, naming the differing parameter, so the build falls back
to this Mac. Worker source mirrors remove stale inputs; Xcode compilation caches
remain in the worker's private Stim home. Physical builds stay local.
An already approved hosting Mac can run the built app:
- Global
- npx
stim ios --scheme MyApp --configuration Debug --remote janics-mac-mini --json
npx stim ios --scheme MyApp --configuration Debug --remote janics-mac-mini --json
The host must support hosted-ios-process; older hosts refuse with an update
remedy before reservation or upload. --remote auto skips incompatible hosts,
while recorded sessions stay on their host until stim stop. Every native
configuration builds for the offered simulator architecture, uses process
readiness with metroPort: null, and closes a previous session's Metro bridge.
The existing hosted iOS logs, view/control, agent access
and scoped cleanup apply. Verify UI and interaction separately from process
readiness.
Unbounded native Xcode build offload, hosted --plan, eas/proxy devices and EAS
artifact profiles remain unsupported. These limits do not change React Native or Expo support.
Copy this request to your agent, substituting your scheme:
Run this native Xcode app with stim ios --scheme MyApp --configuration Debug
--json. Verify its expected screen and interaction on the reported device,
inspect stim logs --errors, then stop only this workspace with stim stop.
android
stim android [--slot <name>] [--variant <name>] [--system-image <id>] [--device-profile <id>]
[--device [serial]]
[--wait <seconds> | --no-wait] [--remote <proxy|eas|local|auto|remote|remote-only|machine>]
[--eas-profile <name>] [--no-metro-check] [--no-build-cache] [--plan] [--json]
Native Android Gradle apps need a wrapper and settings.gradle or
settings.gradle.kts, with one AGP application module. Run stim android
(default variant debug) or pass an exact --variant freeDebug. Kotlin DSL,
Groovy DSL, version catalogs and convention plugins are evaluated by Gradle;
Stim does not infer their model from source text. No Node dependencies or Metro
are required. Stim verifies the APK signature and installs one universal or
matching-ABI APK on an owned emulator or leased physical Android phone.
Use --remote <approved-mac> or --remote auto for a hosted emulator. The host
must support hosted-android-process; older hosts refuse before reserving a
device or uploading the APK. Native variants require a live app process and
never open a Metro bridge.
Multiple application modules, density/split APK sets, unsigned APKs and
EAS/proxy targets are unsupported. Set org.gradle.configureondemand=false so all
application modules can be enumerated; configuration cache remains supported.
APK caching is opt-in through android.artifactInputs in .stim.json.
It declares complete repeatable inputs, including signing files and environment
names; the artifact-input contract
describes eligibility, sharing through configured file providers, and private
input handling. Without it, builds retain
Gradle's incremental and task-cache reuse and skip Stim artifact caching.
Build workers require their separate transfer declaration:
{
"android": {
"offloadInputs": {
"complete": true,
"ignored": ["app/src/main/assets/generated.json"],
"outputs": ["build", "app/build"]
}
}
}
Review this list, then run stim android --remote-build <approved-mac>. Replace
stim with npx stim when it is not installed globally. Paths are exact and
repository-relative. complete: true affirms that Git-visible files plus the
listed ignored files suffice; omitted optional inputs can silently change a
Gradle build. Never declare secrets or a user Gradle home. External inputs,
directory links, submodules and custom local.properties refuse. SDK-only
local.properties stays local. Custom project-state locations must be removed
or made explicit source; only default .gradle/.kotlin state is retained.
List every build directory of the build's projects that the build creates in
outputs: the root build and each module's, library modules included. A
remote build that creates an undeclared one fails with the directory's name and
Stim builds locally; add it to outputs. A buildSrc directory refuses before
upload. Included builds (includeBuild, such as a build-logic plugin build)
and externalNativeBuild staging (.cxx) are not reported by AGP and cannot be
declared, so remote builds of those projects fail and Stim builds locally.
Only declared output directories reported by AGP and recorded after a successful
worker build survive the next sync. Gradle incremental, task-cache and
configuration-cache reuse remain available; the transfer digest is never an APK
cache key. Old workers refuse before source upload. Native --plan uses the local
artifact-input identity and verifies the cached APK without running Gradle or
preparing a device. Missing declarations, unknown local ABI (including legacy
unowned emulator assignments), hosted placement and experimental CAS setup refuse
a prediction. Existing emulator image/profile mismatches use the same refusals as
the normal run. doctor reports native prerequisites. reload refuses for a
process-only app. Re-run stim android after an edit; stop keeps the existing
scoped device cleanup and physical-device lease behavior.
Builds or restores the Android app. Stim then boots an owned emulator, installs
the app, opens it, and checks launch logs. A React Native emulator debug build
can compile on a paired remote machine instead: see remote.build in
machine settings.
-
--remote-build <local|auto|remote|remote-only|name>overridesSTIM_REMOTE_BUILDandremote.build.remotetries the remote machines first, inremote.machinesorder, and builds here only when none takes the build;remote-onlyrefuses instead. A name requires that configured, paired worker and fails withSTIM_OFFLOAD_REFUSEDwithout local fallback. Unlisted or unpaired names refuse at setup before checking the cache, without a build record or failed-run stats. A listed paired name with a cache hit contacts no worker. Prebuild and pod install still run on this machine before a named offload. A refusal starts no local xcodebuild, Gradle or SwiftPM compile. Runstim doctor --fixto ask for build access if not paired; a person on the worker approves it by running thestim-server setupcommand doctor printed, with its ticket and expiry. See machine settings for supported builds and remedies. -
--variant <name>selects a Gradle variant. The default isdebug. -
--system-image <id>creates this workspace's owned AVD from that sdkmanager package id, overridingandroid.systemImagefor one invocation; an id this SDK has not installed refuses withSTIM_BAD_ARGand prints the installed ids. When the workspace already owns an AVD made from another image, Stim refuses with the same remedy as a profile change, unless that AVD never finished a boot: Stim then deletes it through owned-device teardown and creates one from the requested image.android.systemImagestill applies only to a new AVD. -
--device-profile <id>creates this workspace's owned AVD with that avdmanager hardware profile, such aspixel_tabletorpixel_fold, overridingandroid.deviceProfilefor one invocation. An id thatavdmanager list device -cdoes not print refuses withSTIM_BAD_ARGand prints the offered ids. When the workspace already owns an AVD of another profile, Stim refuses instead of booting it; use another--slotor remove the workspace's devices first.pixel_foldandresizableneed a system image with foldable support (SupportPixelFold = onin itsadvancedFeatures.ini, as recent images such as API 34 google_apis have); the emulator quits on boot without it. Stim refuses that pair withSTIM_BAD_ARGbefore creating anything, and the remedy names an installed image that has it:stim android --slot fold --device-profile pixel_fold \--system-image "system-images;android-36;google_apis;arm64-v8a" -
--system-imageand--device-profileselect the local or named host's owned emulator. On eas/proxy runs they refuse withSTIM_BAD_ARG: those backends choose their own device and ignoreandroid.systemImageandandroid.deviceProfile. -
--device [serial]installs and launches on a connected physical device. With no serial it selects a connected device this workspace can lease. It ignoresandroid.remoteand cannot be combined with--remoteother than--remote local. -
--wait <seconds>bounds the physical-device lease wait (default 60;0refuses immediately if busy). Without--device, it bounds the FIFO owned-device slot wait across the Stim home (default 600 seconds). Remote targets do not join this local slot queue. At the cap, only the queue head reclaims the longest-idle eligible owned device in another workspace, one per attempt, at most once every 15 seconds, then rechecks capacity.devices.reclaimIdleMinutesdefaults to 10;0disables it. The same idle checks as supervisor shutdown (default 30 minutes) exclude drivers, locks, builds, viewers and recent activity. Reclaim shuts down, never deletes, and excludes this workspace, physical, hosted, remote, parked and other homes' devices. Failed reclaim is logged and skipped. -
--no-waitbypasses leasing, including another workspace's lease. Installing the same app terminates that workspace's running app. Without--device, it refuses a full device cap or existing slot queue immediately. It cannot be combined with--wait. -
--remote <machine>runs on a named approved Mac inremote.machines, with no fallback.--remote autoplaces on an approved host when this machine is full or busy, or joins the local device queue when no host admits.--remote remotetries the approved hosts first, inremote.machinesorder, and runs here only when none admits;--remote remote-onlynever runs here and waits for a host with room (--no-wait,--wait 0or the end of--waitrefuse withSTIM_AT_CAPACITY).--system-imageand--device-profileselect the host's installed choices. See remote machines. -
--remote proxyuses a configured Agent Device daemon. -
--remote easuses an EAS remote emulator. It needs eas-cli 21.6.0 or later. -
--eas-profile <name>selects a compatible EAS development build, including with--device. It needs eas-cli 18.9.0 or later. A miss stops and prints an EAS build command; cloud builds require authorization. Cannot be combined with--variantor--no-build-cache. -
--no-metro-checkskips the Debug dev-server check and does not start the dev server. -
--no-build-cacheignores cached artifacts and replaces the matching entry. -
--planpredicts the next build instead of running it. See Predict the next build. -
--jsonprints one stable result object on stdout. It includesbuildMachine(the selectedauto,local, or machine name) andbuiltOn(hereor the worker name, absent when no build ran, including cache hits).
A Debug variant starts the workspace's dev server when it is not running, as
described for ios.
A variant that ends in Release embeds its JavaScript bundle and skips Metro.
Predict the next build
stim ios --plan and stim android --plan tell you whether the next run will
come from the cache and how long it should take, without building, booting a
device, installing, or starting Metro. A plan takes no workspace lock and
writes no Stim state, so it can run while another build is in progress.
The selected project integration supplies the plan using its build recipe's
identity and cache policy. If the integration has no read-only planner,
--plan refuses with STIM_BAD_ARG and reports no cache hit. Run the command
without --plan to build and run the app. React Native and Expo planning
behaves as follows.
$ stim ios --plan
plan ios 1b625d.. -> local cache hit
expect ~2.7s (median of 1 hit run)
A plan computes the fingerprint and cache key the same way the run does. It
honors --slot, --scheme, --configuration, --variant, --device-type,
--runtime, --system-image, --device-profile, --eas-profile and
--no-build-cache. It then
checks the caches in the run's order: the local cache, the cache.provider
setting's provider, and the app config's build cache provider. Providers have no
lookup that skips the download, so a remote check downloads the artifact. The
cache.provider tier downloads to a temporary directory that the plan removes;
the app config's provider keeps its download wherever it does during a run. On a miss, the plan also reports the
prebuild decision the run would make. With cache reads on,
it also says why the cache has no app, the way the run would
(cache misses):
$ stim ios --plan
plan ios 3d4168.. -> cache miss: compiles, regenerates the native dir
cache miss: native dependency added: expo-clipboard (before prebuild regenerates ios/)
expect unknown: no cold run of this project is recorded yet
A plan never runs expo prebuild. When the run would and there is an earlier
build to compare with, the reason's kind is prebuild-pending and it compares
the fingerprint before that prebuild. With
--eas-profile, it asks EAS for a matching build and downloads nothing.
--json prints
{ platform, slot?, fingerprint, cacheKey, cacheHit, provider, cacheSkipped, prebuild, outcome, expectedMs, basis, missReason?, placement?, refusal? }.
missReason has the shape of lastBuilds.<platform>.missReason in
stim status --json.
cacheHit is "local", "remote" or false. expectedMs is the median of
this project's recorded runs with that outcome, basis counts those runs, and
both are empty (null and 0) until the project has such a run. A run that
would refuse, such as an EAS miss, carries refusal and still exits 0.
A plan cannot see everything that happens during a run. Another workspace may
store the key first. A prebuild or pod install may move the fingerprint,
and the run then checks the new key. A Release hit whose JavaScript swap fails
builds from scratch. An Android plan uses the ABI of the emulator the slot
records, or of the system image a new emulator would use. A plan refuses
--device, --remote, --wait, --no-wait, --no-metro-check and
--simulator-app with STIM_BAD_ARG. With ios.remote or android.remote
set to auto or a Mac name, it asks that Mac for its device (a read-only
probe) and prints a placement line; it refuses eas, proxy and a Mac that
does not answer. Without --eas-profile, it also refuses the experimental
compiler CAS.
Try it with an agent:
Before building, run `stim ios --plan --json` in this worktree and tell me
whether the next build is a cache hit, how long it should take, and, on a
miss, which native change causes it.
macos
stim macos [--remote-build <local|auto|remote|remote-only|name>] [--remote <placement>] [--json]
Builds the explicitly configured Swift Package executable in Debug and launches
an isolated development .app. Run from the Package.swift directory with
macos.product and macos.infoPlist configured. It uses fixed SwiftPM commands,
with no Metro or custom build scripts. Workspace logs include compiler output
and runtime stdout/stderr. status reports the app and build, and stop signals
only their verified owners. Stim Desktop can preview one owned window and open
the verified app for native input on the same Mac using existing permissions.
--remote-build <local|auto|remote|remote-only|name> selects build placement as described in
machine settings. A name refuses with
STIM_OFFLOAD_REFUSED without fallback; local compiles here.
--remote <placement> and the macos.remote setting say where the built app
runs; the flag wins. A remote machine name from
remote.machines runs it on that approved Mac
over the tailnet and never falls back to a local launch. auto launches here
unless this machine is busy, remote on the first approved Mac in order that has
room and runs here only when none does, remote-only never launches here
(macOS apps take focus) and waits for a Mac with room, and local launches
here. stop or worktree remove stop a hosted app there. macOS refuses eas and
proxy because it has neither backend. These reserved names are case-insensitive
and trimmed; eas and proxy refuse with STIM_BAD_ARG before state access or a
connection. See the
native macOS prototype for metadata, arguments, hosting and current
limitations.
web
stim web [--headed] [--json]
Opens the workspace's page in a Stim-owned Chrome, headless unless --headed,
and captures its console, uncaught errors and failed requests in stim logs.
It never starts a web server: start the dev server first, then run
stim web. Expo apps open their Metro URL and start Metro with stim start.
Other apps set web.url, for example http://localhost:{port:web}/, and run
their own dev server on stim ports get web. When nothing serves the page,
launched is "unverified" and the remedy names that step. The payload
reports launched, the page URL, the Chrome pid, the profile directory and
the reserved cdpEndpoint. A second run with the same options navigates the
same Chrome again.
stim stop closes the browser and keeps its profile; stim stop --slot web
closes only the browser. worktree remove and
gc --delete also delete the profile. See Web in an owned Chrome.
reload
stim reload [ios|android|web] [--json]
Requests a JavaScript reload in the live app on this workspace's owned local simulator
or emulator, or reloads its owned Chrome page. It never builds, installs, boots,
or cold-launches. Omit the platform when exactly one owned app or page is live;
name it when more than one is live. A bare reload picks the Chrome page only when
no native launch is recorded. A web reload sends Page.reload to the owned page
and reports strategy: "cdp", with appId set to the URL the page was on when
Stim sent the reload.
Stim does not record launches on a physical device (--device) or an EAS
Simulator session (--remote eas). When no owned launch is live, reload asks
this workspace's Metro for its connected apps and reloads them, so it reaches an
app on a phone or in an EAS session that is running and connected to that Metro.
Without a recorded package name, every connected client that is not an iOS client counts as Android. Name the platform when Metro reports apps on both, and a platform with no connected client is never sent a request. The facts have appId: null
and deviceId: "metro:<port>" because Stim does not know the device, and
Stim changes nothing on it. An Android phone needs its adb reverse for the
Metro port, which android --device adds; reload does not restore it. When no
app is connected, it refuses with STIM_RELOAD_STOPPED. A launch error that
leaves an app without a Metro connection (an iOS first-bundle failure) is not
reached; use the device's own Reload control. An app in an EAS Simulator
session may not open Metro's websocket through the tunnel; reload then finds
no app there and refuses, and stim ios --remote eas relaunches it on the
session.
Every native reload goes over the workspace Metro websocket, on both platforms. It never reopens a development-client URL, because that restarts the app rather than reloading its JavaScript.
Before sending, Stim checks that the app process exists. On iOS that probe
(simctl spawn launchctl list) waits 5 seconds, scaled up with host load to at
most 30. When the device tool still does not answer, Stim sends the reload anyway;
STIM_RELOAD_PROBE_FAILED is then reported only if Metro finds no peer for the app after the reload fails.
A process the probe proves is gone still fails with STIM_RELOAD_STOPPED.
An Android reload first checks adb reverse on each live owned emulator launched
against this Metro and re-applies the Metro port's reverse where it is missing.
adb ties a reverse to its transport, so an emulator whose adb connection drops
and reconnects loses it silently, and the app stops reaching Metro and Fast
Refresh. After restoring one, Stim waits up to 8 seconds, sending nothing,
until the app on every emulator it checked is connected again, then sends the
reload once (a bare React Native dev server cannot name its clients, so there
it waits a fixed 2.5 seconds); reverseRestored names the serials it
restored. stim status reports the same loss as the android-reverse-missing
issue, with stim reload android as the remedy.
How the message is addressed depends on the dev server, and strategy reports
which you got. Where Metro can name its clients, Stim addresses every peer
matching the platform and reports metro-websocket. A workspace Metro serves one
app, so those peers are that app on however many devices are attached to the
port; targets says how many peers the request addressed. Android peers carry the package
name and iOS peers carry only role=ios, which is enough to keep a reload on one
platform but not to single out one iOS app among several.
The bare React Native dev server cannot name its clients at all, because
@react-native-community/cli-server-api answers that request out of a ws
property removed in ws 3.0. There Stim broadcasts: metro-broadcast means every
app on that port was sent a reload request, and that Stim could not confirm the recorded app was
among them. Verify the UI, and fall back to the app's own error screen or dev
menu if nothing changed.
When Metro names its clients and none match the platform, Stim still broadcasts before giving up, because matching is best-effort and an unmatched peer may be the app. It reports the miss either way, so verify the UI before acting on the remedy.
When Metro reports no peer for the app, retry once first: a client reconnects every 2 seconds, which is also the Metro probe's timeout, so a single miss can be a reconnect window rather than an app that never connected. If it stays unreachable on iOS, an error in the first bundle leaves the app without a packager connection at all, and no retry will make it a peer. The command then returns instructions to continue in the agent's existing automation session: press the error screen's Reload button, or open the dev menu and press Reload when no error screen is showing, and relaunch only when neither is reachable. Stim does not take over that stateful session.
When Metro itself does not answer within the probe's 2 seconds, nothing is known about the app, so the command says to retry and check the dev server rather than sending the agent to the device.
The command refuses release builds, stopped or unowned devices, a missing or
foreign Metro server, and ambiguous selection. --json prints one object with
platform, deviceId, deviceName, appId, metroPort, and strategy.
Success, including exit 0 with --json, confirms that the request was sent.
The command does not observe completion. Verify the expected UI on the reported
device and inspect stim logs --errors before claiming recovery.
logs
stim logs [--slot <name>] [--source <metro|client|device|build|agent|maintenance|gc|builds|all...>]
[--level <debug|info|warn|error|fatal>] [--since <duration>]
[--grep <expression>] [--tail <count>] [--errors]
[--follow] [--json]
Queries the workspace log timeline. No matching records is a successful empty result.
--errorsselects errors and fatals from Metro, client, and build logs, plus confirmed native app-crash reports and thestim webpage's failed requests and browser errors, since the last launch marker. The page's records start again at each page load instead, and a launch does not reset them. A completed bundle attempt resets only older Metro errors. Metro and client output is shared by every slot and names no device, so the newest launch of any slot resets it.--slot <name>includes that shared output windowed by the slot's own launch and the bundles of the platforms it launched, so afterstim androidhides an iOS JS error from the unfiltered query,--slot <ios-slot>still shows it. General device logs require an explicit--source deviceor--source all.--source deviceincludes operating-system device logs.--source maintenanceshows maintenance actions, planned (report mode) or taken (on mode), and failures for this workspace. Add--errorsto show only maintenance failures from it; failures before a later launch marker are hidden.--source gcshows the machine-wide gc log: one record per item thatstim gc, automatic maintenance or the budget gate cleared, kept or failed to clear, with the trigger, size and reason. It works from any directory, stands alone (--source allleaves it out), and takes--json,--since,--tailand--follow;--errorsshows only failed items.--source buildsshows the machine-wide build ledger: one record per nativeios,androidormacosbuild run, newest 500, whether it compiled here, was offloaded to a build machine or came from a cache, and whether it succeeded, failed or was cancelled. A record has the project, workspace and branch, the scheme or variant, where it ran (and why an offload fell back to this Mac), the cache result and key, the time in each phase, the failure code, message and remedy, the simulator or emulator it targeted, this Mac's load per core when the run started, when the run's owned simulator or emulator boot started and the load per core then (loadPerCoreandboot, absent from older records), and the Stim version. It outlives the workspace, works from any directory, stands alone (--source allleaves it out), and takes the same options as--source gc;--errorsshows only failed builds.stim gcnever removes it.--source agentshows what agent-device did on this workspace's owned simulators and emulators: taps, typing, app opens, screenshots, and failed commands. A plainlogsincludes it;--errorsincludes it only when selected with--source agentor--source all.--followstreams new matching records. It exits with status 0 on Ctrl+C, SIGTERM, or when its stdout closes. On Linux it also exits when the process that started it exits while its stdout is a pipe. It notices within a few seconds, even when no record arrives, except a closed stdout pipe on Linux whose starter is still running, which it notices at the next record.--jsonwrites NDJSON. Zero matches writes zero bytes. With--errors, an Expo error record also carries acontextarray: the code frame and stack lines Expo printed after it.
stop
stim stop [--slot <name>] [--json]
Without --slot, stops the supervisor and all log collectors, shuts down every
owned local device, ends an owned remote session, and frees the port. Owned
local devices stay assigned for reuse. A dev server left behind by a supervisor
that died is stopped when its recorded process identity still matches. An
external server on the reserved port is left running, the port stays reserved
while that server runs from this project, and a process whose ownership cannot
be verified is not signalled.
stop verifies that a device actually shut down instead of trusting the
shutdown command: it waits for a simulator to report Shutdown and for an
emulator's process to exit. A device that does not get there is reported as
failed, not shut down, with a remedy naming the manual command to run and
stim gc --delete as the fallback; --json carries the same outcome in
device.<platform>.status plus a remedy field. stop still never deletes
the device.
With --slot <name>, stops only that slot's owned devices and collectors and
releases its leases. Metro, the reserved port, and sibling slots keep running.
Use --slot default to stop only the workspace's default device -- the one
ios/android address with no --slot and status reports with no [slot]
label -- while a named slot stays up. --slot never ends an owned remote
session, even --slot default; use plain stop for that. --slot web closes
only the owned Chrome from stim web and keeps its profile; Metro and every
device keep running, so no device slot can be named web. A slot the
workspace has not recorded is refused with STIM_BAD_ARG and the list of its
slots, and nothing is stopped. A slot whose first build is still running
counts as recorded, and that build is interrupted.
Stopping during a build
ios, android, and stop take turns on the workspace's build lock. A
command that has to wait prints what it is waiting for on stderr right away and
every 30 seconds:
lock waiting for `stim ios` (pid 41233, running for 12m04s) in this workspace to finish
stop does not wait out a build that would be left with nothing to deploy to:
a plain stop, stop --slot <name> for the slot the build targets, or for the
workspace's only device. It asks that run to stop through the workspace state,
also sends SIGINT when the run is a Stim process of its own (the CLI, or the
worker behind createStim) whose recorded identity it verified, and waits up
to 60 seconds. A run inside a process that embeds Stim, such as stim-server, is
never signalled. The run stops xcodebuild or
Gradle, caches nothing from the interrupted build, and exits 130 with
STIM_CANCELLED. If the run does not exit in time, stop refuses with
STIM_STOP_BLOCKED and names the pid and lock to deal with. stop --slot <name> while a build for another, still-running slot is in progress leaves
that build alone and stops the slot right away. A plain stop ends an EAS
session as soon as it sees a build holding the lock, so the session stops
billing even when the build cannot be interrupted.
On a physical iPhone, stopping the log collector closes the running app.
stop also releases this workspace's device leases. It never uninstalls the
app or shuts down the phone; hardware has no owned-device registry entry.
On a physical Android phone, stop and worktree remove also remove the
adb reverse that android --device added for this workspace's Metro port, and
leave every other reverse on the phone alone.
device lock and device unlock
stim device lock <ios|android> [id] [--slot <name>] [--for <duration>] [--wait <seconds>] [--json]
stim device unlock [ios|android] [--slot <name>] [--json]
Leases a connected physical device to this workspace, so another workspace's
--device run waits instead of installing over it. --for takes a whole
number of seconds or minutes from 10s to 30m and defaults to 5m;
--wait bounds how long to wait for a device another workspace holds
(default 60 seconds, 0 refuses at once). Locking a device this workspace
already holds sets a new expiry, which can shorten it.
With no id, lock picks from the connected devices the resolver accepts: the
one this workspace already leases when it is connected, otherwise the first
free one in id order. The same rule serves ios --device and
android --device with no id, so two devices on one machine no longer refuse.
An id can also name this workspace's own Stim-owned simulator (its UDID) or
running emulator (its emulator-NNNN serial), in any slot. The lease then
tells agents and stim status that the device is being driven, reported as
driven by stim device lock. --slot, when given, must be the slot the device
is in. A workspace holds one lease per platform and slot, so this refuses with
STIM_DEVICE_BUSY while that slot already leases a phone.
With no id, lock still picks only physical devices.
unlock releases every lease this workspace holds, or only the platform
named. Adding --slot <name> restricts release to that slot; releasing nothing
is not an error. A --device run takes a lease of
its own for the length of the run, so lock is for holding a device across
runs, such as a device-tool session. stim status lists every lease on the
machine.
device provision
stim device provision ios [udid] [--scheme <name>] [--configuration <name>] [--yes] [--json]
Sets up signing for a connected iPhone or iPad in the project's Apple
Developer team, which stim ios --device needs before its first build for that
phone. It is the only Stim command that changes a developer account, and it
never runs as part of a build.
It finds the Xcode project the way stim ios does (ios/ for React Native
and Expo, the app's own project or workspace for a native app). A native app
with Swift packages uses the packages stim ios already resolved, with
automatic package resolution off, so provisioning never fetches packages or
rewrites Package.resolved. Until stim ios or stim ios --device <udid>
has resolved them once, it refuses with STIM_PROVISION_FAILED. It reads every
target of the scheme (--scheme, then ios.scheme, then the detected one)
that is signed with a provisioning profile, on any platform, in the
configuration (--configuration, then ios.configuration, then Debug), and
prints what it will change: register
the phone with the team named by the project's DEVELOPMENT_TEAM, create or
refresh the iOS App Development profile for each bundle ID, and create an
Apple Development certificate for this Mac when the keychain holds none. It
then asks [y/N]; Ctrl+C or end of input counts as no. Without a terminal, or
with --json, it refuses with STIM_PROVISION_UNCONFIRMED and changes nothing
unless --yes is passed.
On confirmation it runs xcodebuild -allowProvisioningUpdates -allowProvisioningDeviceRegistration for that phone in a throwaway
DerivedData directory and stops it at the first build task, which xcodebuild
only starts once it has resolved signing, before anything compiles. Ctrl+C
and SIGTERM are passed on to xcodebuild, the DerivedData is always removed,
and a run still unresolved after 15 minutes is stopped. It then reports the
profile it installed and any new keychain identity. Refusals use
STIM_PROVISION_FAILED: no Xcode account and no API key, an account or key
without access to the team, a target with no team or with manual signing, an
interrupt or a timeout. A refusal after xcodebuild ran still lists what had
already changed, and running the command again is safe.
It signs in with the Apple ID in Xcode > Settings > Accounts, or, on a Mac where nobody signs in to Xcode, with an App Store Connect API key:
- Global
- npx
stim settings set ios.appStoreConnectKeyPath /Users/me/.appstoreconnect/private_keys/AuthKey_ABC123.p8 --scope machine
stim settings set ios.appStoreConnectKeyId ABC123 --scope machine
stim settings set ios.appStoreConnectIssuerId <issuer-id> --scope machine
npx stim settings set ios.appStoreConnectKeyPath /Users/me/.appstoreconnect/private_keys/AuthKey_ABC123.p8 --scope machine
npx stim settings set ios.appStoreConnectKeyId ABC123 --scope machine
npx stim settings set ios.appStoreConnectIssuerId <issuer-id> --scope machine
STIM_ASC_KEY_PATH, STIM_ASC_KEY_ID and STIM_ASC_ISSUER_ID each override
their own setting when set and non-empty. Stim passes the path to xcodebuild,
never reads the key, and never prints the path.
Because it changes your team's account, the agent guide tells agents to run it only when you ask. A prompt you can give your agent:
My iPhone is connected but stim ios --device fails on signing. Run stim device provision ios without --yes, show me what it would change, and wait for me to confirm.
settings
stim settings [--json]
stim settings get <key> [--scope <layer>] [--json]
stim settings set <key> <value> --scope <layer> [--json]
stim settings unset <key> --scope <layer> [--json]
Lists every setting with its effective value and the layer it comes from, or
changes one layer. <layer> is machine, workspace, repo, or committed.
workspace is this project's entry in ~/.stim/config.json, repo is this
repository's entry, and committed is the app's .stim.json (the repository
root's for worktree.*). A key accepts only the layers Stim reads it from;
--scope can be omitted when there is one. Run it from the app directory, or
from a monorepo web package that resolves to the app (see ports).
Strings and choices are passed as-is. Booleans, numbers, arrays, and objects are JSON:
stim settings set ios.deviceType "iPhone 17 Pro" --scope workspace
stim settings set worktree.exclude '["node_modules","ios/Pods"]' --scope committed
stim settings set concurrency.maxBuilds 2
stim settings unset ios.deviceType --scope workspace
An unknown key, a layer the key is not read from, or a value of the wrong
shape refuses with STIM_BAD_ARG, names the expected shape, and writes
nothing. Machine-file writes take the config lock and replace the file
atomically. A committed write keeps the file's other keys and indentation.
--json on the list prints one object:
{
"project": "/path/to/app",
"files": { "machine": "...", "workspace": "...", "repo": "...", "committed": "..." },
"settings": [
{
"key": "ios.runtime",
"value": "26.2",
"origin": "repo",
"layers": { "repo": "26.2", "committed": "26.0" }
}
],
"unknown": [{ "key": "bogus", "scope": "committed", "file": "...", "value": true }]
}
origin is the winning layer, env when an environment variable overrides
the file (env then names it), default, or null when unset. A default
that depends on the machine carries defaultReason: iosSimulatorApp and
androidEmulatorApp default to stim-desktop with "defaultReason": "Stim Desktop installed" when Stim Desktop is installed. Plain settings get
prints the value on stdout and that reason on stderr. unknown
lists keys Stim does not read. android.keystorePassword is sensitive: it
prints as ********, and committed accepts only an env: or file:
reference for it. set and unset --json print the layer written, its file,
and the setting's entry after the write.
status
stim status [--json] [--watch]
Shows every Stim environment on the machine. Last builds include buildMachine
(the selection) and builtOn (here or the worker name, absent when no build
ran, including cache hits and refusals after setup). Configuration refusals
create no build record or failed-run stats. Older records may lack
these additive fields. The output includes worktrees,
ports, devices, supervisors, builds, logs, capacity, and free disk space.
Each linked worktree shows its uncommitted changes, commits ahead of and
behind its upstream, and whether its branch is merged, as a
git: 2 changed, 1 untracked, ahead 3 line. One-shot stim status shows it
only for live environments; stim status --watch shows it for every worktree. See
Parallel environments for the JSON
fields.
Each workspace is marked with its lifecycle phase: [warming: <step>] while
stim worktree warm runs in it, [ready] after a warm until its first run,
or [idle]. An active workspace (phase live) has no marker. In --json, each environment
carries phase (warming, ready, live or idle), phaseSince and, while
warming, warmStep; see
Parallel environments.
Each environment also carries stage, the conclusion Stim Desktop and the
phone app show beside the git chip: { kind, since, platform, closedApps }.
kind is the first of these that applies: building while a build runs,
warming or ready from phase when nothing is active, build-failed when the
newest run of either platform failed, running when the workspace is active or
holds a remote session, else stopped. since is when that began, platform
names the build for building and build-failed, and closedApps lists, for
running, the { platform, slot } of each device whose app is closed. A
worktree entry with git facts also carries gitChip: { parts, ci }: the
commits ahead and behind, the uncommitted count, the branch it is merged into
(unless its pull request is merged) or a missing upstream, in display order,
and the pull request's checks as failing, pending, passing or null.
A workspace that needs attention prints each issue under it with the command that fixes it:
! owned AVD stim-app is not detected by adb; run `stim android`
In --json, each environment's issues array holds
{ code, severity, message, remedy, workspace, slot? }, and warnings holds
the error and warning issues as text. Run remedy from workspace.
error is something Stim cannot verify or safely act on, warning something
broken now that needs someone to act, and info a fact the next normal Stim
command handles by itself; the phone app and Desktop hide info. An
info issue is a note that blocks nothing: when another app holds an idle
workspace's reserved port, plain status prints
- port 8083 is in use by a dev server in scratchpad/web-phase2/expo-web; stim start will choose a free port,
and metro.heldBy carries that process's pid and cwd. The same port
taken while the workspace's supervisor runs is a warning. An idle workspace's
shut-down emulator is not an issue: Stim warns that adb does not see an owned
emulator only when the workspace holds a lease on it, or launched onto it and
has not stopped it since while its dev server runs or within the last 30
minutes. stim stop --slot <name> counts as stopping that slot's device. A
supervisor record whose process is gone is not an issue either; the next
stim stop or stim start clears it. stim guide facts status lists every
issue code.
A workspace with a recorded EAS Simulator session prints a
remote <platform>: EAS session <id> billable line with the session's preview
URL. In --json, each environment's remoteDevices array holds
platform, backend, sessionId, state, startedAt, and webPreviewUrl.
status reads Stim's local records and does not query EAS; stim stop in that
workspace ends the session.
A physical phone or tablet the workspace leases with ios --device,
android --device, or device lock prints under its workspace as
ios: Old iPhone (physical, iPhone 12 Pro) connected -- leased until 17:04:53 (9m59s left).
In --json, each environment's physicalDevices array holds platform,
slot, id (the UDID or serial), name, model, owned (always false),
physical (always true), connection (connected, disconnected, or
unknown), and lease with holder, kind, grantedAt, and expiresAt.
Status reads the connection from xcrun devicectl list devices or
adb devices, only when the workspace holds such a lease. A run lease ends
with its run, so to keep a phone listed after the run, hold it with
stim device lock. Stim Desktop and the Stim phone app show each one as a
tile with a Physical badge.
A dev server that its supervisor stopped after
metro.idleStopMinutes with no use
prints as metro: port <port> stopped (idle). In --json that environment's
metro carries idleStop with reason, at, and idleMinutes. Any other
known cause prints after not running, and metro.lastStop carries it; see
why the dev server stopped.
Status prints the recorded managed Metro tunnel's provider and URL, with
tailnet-only for Tailscale. In --json, metro.tunnel carries { provider, url }
when a managed tunnel is recorded on the workspace's reserved port. This reports
the record without probing reachability. See Metro on your tailnet.
In --json, metro.bundle reports the dev server's bundle requests, from the
metro log: { bundling, platform?, startedAt?, percent?, last? }. bundling
is true while an app's request, or Stim's own prefetch before a launch, is in
flight; percent is present when Metro reports progress for it. last is the
newest finished request: { platform, status, durationMs, finishedAt }.
Once stim status --watch has measured them, each environment carries
disk: { worktreeBytes, nodeModulesBytes, buildBytes, measuredAt }, and each
owned simulator and emulator carries disk: { bytes, measuredAt } for its data
folder. buildBytes is Stim's own folder for the workspace, with Xcode derived
data, Gradle outputs and logs. The watcher runs du off its refresh path, at
most every 5 minutes per folder while the environment is active and every hour
otherwise, and one-shot stim status reads its cached sizes.
An environment carries agents when a coding-agent session works in it:
[{ tool, sessionId, title?, cwd, startedAt?, lastActiveAt?, pid?, openUrl?, webUrl? }],
with tool either claude-code or codex. A session works in an environment
when its working directory is the environment's path, a folder inside it, or
its git worktree root. The watcher finds running Claude Code sessions in
~/.claude/sessions and Codex threads updated in the last 30 minutes in
~/.codex, at most every 15 seconds, and one-shot stim status reads what it
found for 2 minutes. These are the tools' own internal files, so Stim reads them
best-effort. start, ios, android, web, reload and worktree warm also
record the CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID of the shell that ran
them, which names the session exactly. title is the short name the tool keeps
for the session; Stim reads no prompts or conversation. openUrl opens the
session in the Claude desktop app or the Codex app when that app is installed,
and is absent for a Claude Code session started in a terminal. webUrl is the
session's https://claude.ai/code/ link while Claude Code Remote Control is
connected, which opens it in a browser or the Claude mobile app; Stim's phone
app opens it from the workspace page.
When the watcher stops finding a session it found on its previous look, it
records the session in each workspace it worked in, and the environment
carries it for 3 days in endedAgents, most recently ended first. Each entry
has the fields above except pid, as the watcher last found them, including
its openUrl and webUrl, plus endedAt, the last time the watcher found it
running. A Codex thread ends 30 minutes after its last update. A session that
runs again is listed in agents only. Stim Desktop and the phone app show a
workspace's running and ended sessions alike, earliest startedAt first,
without times.
In --json, machine lists what uses CPU and memory now: each booted
simulator and emulator with its workspace, each Metro, running build and
stim web Chrome, stim-server, and a shared bucket for machine-wide processes
such as CoreSimulator services, the adb server and Gradle daemons. Each owner
carries cpuPercent, memoryMb, residentMb and processes, and every
process counts in exactly one owner. owned marks what Stim can stop: a
workspace's owned device with stim stop --slot <name>, and its Metro with
stim stop. memoryMb is the physical footprint Activity Monitor shows, read
by a small helper that Stim compiles with the Xcode command line tools on
first use. Without them, and on Linux, machine.memorySource is rss and
owners' memoryMb falls back to resident memory, which counts pages shared
between processes once per process, so a simulator reads many times its
footprint. An environment's memoryMb sums its own owners' footprints, builds
included, with memorySource: "footprint". Without a footprint, or when
nothing runs and machine is null, it is the fixed estimate the memory
budget uses, with memorySource: "estimate". stim guide facts status
lists every field.
Status always shows this Mac's load, as a load 17.6/core (busy at 2) line and
in --json as capacity.load: cpus, loadPerCore (the 5-minute load
average divided by cpus), busyAt (server.maxLoadPerCore, the load at
which automatic placement prefers another machine), limit and state.
state is free under busyAt, busy at or over it, or holding at or
over limit. No setting sets limit yet, so it is null and state is
never holding.
While concurrency.maxBuilds or concurrency.maxDevices is set, status shows
how many of those slots are in use and who holds them, as a builds 1/2 or
devices 6/6 line, and in --json as capacity.builds and
capacity.devices: max, inUse and holders. A build slot's holder names
the workspace building in it, or the client whose offloaded build this Mac
runs; a device's holder names the workspace label and device slot that record
it. capacity.devices.inUse is null, with unknown saying why, when the
devices could not be listed. With neither cap set, neither field appears.
--watch keeps running and prints the status again each time it changes.
With --json it prints one complete payload per line: one immediately, then
one per change, never two identical payloads in a row. In a watch line, a CPU or memory figure keeps
its value from the previous line until it moves by 5 percentage points or 16 MB;
resident memory and process counts follow their owner's row, and capacity.committedMb is held on its own, so it can differ from the sum of the environments' memoryMb.
capacity.load keeps its value until its loadPerCore moves by 1 per core or its state changes.
A change in only a bookkeeping timestamp (each pullRequest.checkedAt, maintenance.lastChecks, maintenance.lastPass.startedAt and durationMs, maintenance.running.startedAt) prints no line; a line that prints carries the current values.
It reacts to changes in
$STIM_HOME state and the EAS session ledger, git HEAD, index and ref changes
in the worktrees' repositories, the exit of a recorded supervisor, Metro,
build, browser or macOS app process (checked every 2 seconds), adb device
arrivals and departures, and simulator boots, shutdowns, creations and
deletions (from CoreSimulator's device set and metadata files, with a simctl check every 30
seconds), and recomputes every 30 seconds as a fallback. A log append updates the
log-derived facts within about a second; device activity follows at most every
5 seconds. The first line does not wait for git: worktree git summaries can be
null in it, and the next line carries them. With --json, while
machine is not null, it rereads the process table every 15 seconds so
machine stays current. It exits with status 0
on Ctrl+C, SIGTERM, or when its stdout closes. On Linux it also exits when the
process that started it exits while its stdout is a pipe. It notices within a
few seconds, even when nothing changes, except a closed stdout pipe on Linux
whose starter is still running, which it notices at the next change.
Use it to wait for a device, a build, or a dev server instead of polling stim status --json.
While stim ios or stim android runs, the workspace shows the build's phase
and an estimate of the time left:
build: ios compile, 1m10s elapsed -- about 3 min left (median of 4 cold runs)
In --json, each environment carries build: null, or
{ platform, slot, state, phase, startedAt, phaseStartedAt, outcome, outcomeKnown, cacheLookupOutcome?, expectedMs, expectedPhaseMs, completedPhaseMs?, basis, plannedPhases }.
phase is one of prepare, cache-lookup, wait, prebuild, pods,
compile, device, install and launch. Creating, adopting or booting the
owned simulator or emulator before the cache lookup counts as prepare.
device starts once the app is ready and covers waiting for the device (its
boot, adoption cleanup, or a physical device's lease and connection check); a
boot that finishes during the build adds no device time. state is running while the run's
native-run.lock claim is live, stale when that run was killed (the next run
replaces the record), and unknown when the claim cannot be read. outcome is
cold after the local/provider lookups resolve a miss, and hit after a cached
artifact is ready to reuse, including a shared-build hit or a recheck after
prebuild or pods. A later recheck can replace the first lookup's outcome.
Before resolution it follows the project's most recent run and outcomeKnown
is false. cacheLookupOutcome is hit or miss after an actual lookup resolves;
it is absent before resolution and on runs that skip lookup, such as --eas-profile
or --no-build-cache. Fingerprinting for a fresh build remains in prepare when
cache reads are disabled.
completedPhaseMs holds milliseconds spent in each phase the run already left,
summing repeated visits. It excludes the current visit; the current phase appears
only if visited earlier. It is absent before any phase completes and on older Stim versions.
expectedMs and expectedPhaseMs are
medians of this project's last successful runs with that outcome, and basis
counts the runs behind expectedMs. Both are null until the project has such
a run. plannedPhases lists, in order, the phases at least half of those runs
entered, each as { phase, expectedMs } with its median, so a progress bar can
be drawn before the run reaches them; it is null without such runs.
The run estimates twice: when it starts, and once its outcome is known. The second estimate uses only runs that created, adopted or cold-booted their device the way this run did, so a new worktree is not estimated from reruns that reused a booted device. Runs recorded before Stim tagged them also count for a run that reuses its device until three tagged ones exist. Runs that finish while this one is running do not change its estimate.
Once the run knows why its cache lookup missed, build carries missReason,
in the shape of lastBuilds.<platform>.missReason below. When prebuild or
pod install will run, the miss is that of the key looked up first and
build.missProvisional is true until the run looks the key up again after
them, because they can change the fingerprint. A hit then removes missReason and
sets outcome to hit; a miss replaces it with the final reason. Once the native
build tool prints a line Stim reads, build also carries detail while the
run is in compile:
{ step, unit, done, total, line, updatedAt }. step is the tool's step:
configure, compile, link, resources, script, dex, package or
sign. For xcodebuild, unit is targets, done counts the targets it
finished and total the targets in its dependency graph. A target counts once
xcodebuild touches or signs its product, so an incremental build can end below
total. For Gradle,
unit is tasks, done counts the tasks it reported and total is null.
For SwiftPM (stim macos), unit is steps, and done and total are the
[done / total] of its latest progress line; total can grow while SwiftPM plans the build.
line is the latest compile, link or task line with paths shortened to file
names. These are counts, not a completion percentage: one target can take ten
minutes and a cached one no time at all.
During device and launch, once Stim observes what the run waits on, build
carries activity: { name, startedAt, percent? }. name is booting while
device waits for the owned simulator or emulator the run is booting. During
launch it is bundling from the app's bundle request to this workspace's
Metro until Metro delivers it, and waiting-ready while the app reports its
readiness pending; activity is absent in between. startedAt is when that evidence was recorded. percent
is Metro's own progress for that bundle request, present only once Metro
reports one. Plain stim status shows it after the phase, as in
build: ios launch (bundling JS 45%, 12s), 58s elapsed. activity does not
change the run's launched result.
build.placement says where the build runs: "local", or, while it is
offloaded to a remote machine, an object with
the machine and its step there. phase is sync, deps, prebuild,
pods, build (xcodebuild or Gradle) or fetch; startedAt is when the offload
started and phaseStartedAt when that step did. Meanwhile build.phase
follows it as prebuild, pods or compile.
"placement": {
"host": "janics-mac-mini",
"phase": "build",
"startedAt": "2026-09-28T21:40:02.118Z",
"phaseStartedAt": "2026-09-28T21:40:09.530Z"
}
build.waitingOn is present while build.phase is wait: another workspace
is already building the same artifact, and this run waits for it instead of
building. waitingOn.path is that workspace's root, usually the path of another entry
in environments; the build lock is machine-wide, so it can name a workspace
that is not listed. It is absent when the holder is not known.
Plain status adds the machine to the build line:
build: ios compile on janics-mac-mini (build, 2m10s), 3m05s elapsed
Each workspace also shows its last build per platform:
last build: ios local cache in 12s, android compiled in 7m02s
In --json, every environment carries platforms: string[], ordered
ios, android, macos, web, with [] when no platform is detected.
Expo uses an explicit platforms list from app.json or a literal array in
app.config.js/ts/cjs/mjs; otherwise it defaults to iOS and Android, adding web
when react-native-web is declared or resolves. Bare apps use .xcodeproj or
.xcworkspace entries in ios/, or in the directory ios.projectPath names,
and Gradle project files in android/.
web.url adds web to any app, even when Expo has an explicit platform list; a
bare app has no other route to web. macOS needs Package.swift,
macos.product and macos.infoPlist. Detection never runs project scripts or
executes app config code.
In --json, an environment with a recorded run carries
lastBuilds: { ios?, android? }, each
{ platform, status, cacheHit, cacheSkipped, durationMs, fingerprint, startedAt, finishedAt, errorCode?, missReason?, buildMachine?, builtOn?, offloadedTo?, offloadFallback?, diagnostics? }.
status is ok or failed, and cacheHit is local, remote, or false
when the run compiled or failed before finding an app. buildMachine records the
selected auto, local, or machine name. builtOn records here or the worker
name when a build ran; it is absent on a cache hit or a refusal before building.
Invalid, unlisted or unpaired selections refuse at setup without a build record.
offloadedTo names the
remote machine that compiled the app; offloadFallback is why a run that
considered offloading built here instead, such as
janics-mac-mini: busy (load at or above 2/core; load 8.2/core, 2 builds). A failed run whose
compiler reported errors carries diagnostics: up to five
{ file, line, column, message }, with null for a position the compiler
did not give. Every failed run carries cause: { key, file, line }: its first
diagnostic with a file and a line, keyed <file>:<line>, else its errorCode
(or failed). Failed runs in a row with the same key failed the same way.
A run that did not install a cached app (it compiled, or failed before finding
one) carries missReason: why the cache had no app for it.
{
"kind": "changed",
"summary": "native dependency added: expo-clipboard",
"changes": [
{ "source": "node_modules/expo-clipboard/ios", "change": "added", "category": "native-dependency" },
{ "source": "expoAutolinkingConfig:ios", "change": "changed", "category": "autolinking" }
],
"changeCount": 2,
"baseline": { "fingerprint": "5f9c79...", "from": "workspace" },
"rekeyedBy": []
}
kind is changed, no-baseline, same-sources, cache-skipped, or
fingerprint-error. changes lists at most 20 of the changeCount changed
fingerprint sources. baseline is the cached build Stim compared with: this
workspace's last build of the platform, or else the newest build of the same
project in another worktree. rekeyedBy names prebuild or pod install
when those steps moved the cache key. The build prints the same summary on
stderr as cache miss: <summary>. To predict the next
build instead, use --plan.
The same environment also carries builds: { ios?, android? }, each
platform's last 10 runs, newest first. Each entry has the fields of
lastBuilds plus { result, slot, configuration, cacheKey, phases }.
result is succeeded, failed, cancelled (Stim stopped the run after an
interrupt or stim stop), or interrupted: the run's process ended without
recording a result, such as after a kill or a second interrupt, so the next run in the workspace recorded it without a duration or
cache facts. configuration is the iOS configuration or Android variant the run built
(Debug or debug by default), and
phases gives the milliseconds the run spent in each build phase it entered,
such as pods, compile and install. Only runs that record a last build are
listed: a run that stopped before looking up a build, such as one with a bad
flag, is not. A run that builds or reuses an app and then fails at
the device, install or launch step is listed with failed and its error code.
Each booted simulator and detected emulator also shows who is using it:
ios [duo]: stim-app-duo (iPhone Duo 27.1) booted (owned) -- driven by agent-device for 12m
android: stim-app (emulator) detected (emulator-5554) (owned) -- idle 3h
In --json, those devices carry
activity: { state, driver?, lastActivityAt?, basis }. state is driven
when a tool holds the device now, active when it had activity in the last 10
minutes, idle otherwise, and unknown when a claim or driver check could not
be read. Stim counts as drivers a live agent-device session (its recorded
processes must still be alive with their recorded start times, so a stale or
reused pid does not count), an unexpired stim device lock, a host process
that names the device (Argent, xcodebuild test runners, idb, Maestro, Appium,
simctl io|spawn), and on Android a uiautomator, androidx.test or Argent
helper process.
lastActivityAt is the newest of the device's app log records, changes to the
workspace's client.ndjson and device.ndjson logs, the platform's Metro
bundle requests, the workspace's last Stim run, and, while agent-device
drives the device, the agent's last recorded action, rounded down to the
minute; recent gives the newest time of each of those kinds of evidence, so a
reader can tell agent actions and reloads from app log records. Stim reads
agent-device state without changing it. stim guide facts status lists every
field.
An owned booted simulator or detected emulator also reports whether the workspace's app is running on it now:
ios: stim-app (iPhone 18 Pro 27.0) booted (owned) -- idle 3h -- com.example.app not running
In --json, those devices carry app: { id, state }. id is the bundle
identifier or package Stim checked, and state is running, stopped (the
app crashed, was killed, or never launched), or unknown when the process list
or the app's Info.plist could not be read. app is absent when the device is
not owned or Stim knows no app id for it. Each of those device records also
carries appPresence: none when the device runs, its platform's latest run
failed and builds carries the platform with no succeeded run, closed when it runs and the app is
stopped, else null. This is the app's current process state, not a record of
the last launch. Stim reads it from one host ps for every simulator and the
same adb shell ps it reads for activity, so status --watch notices an app
that exits within 30 seconds. Run stim ios or stim android to launch it
again.
Try it with an agent:
Run `stim ios` in this worktree. While it builds, follow
`stim status --watch --json` and tell me each phase change and the time left for
this workspace.
stats
stim stats [--json]
Shows how many ios and android runs this project and this machine have
recorded, how many hit the build cache, the mean cold run and hit run, and an
estimate of the time the cache saved. The aggregates are kept in
$STIM_HOME/stats.json, and every worktree of a repository counts into the
same project bucket. The same file keeps the last 10 successful runs per
project, platform and cache outcome (and the last 10 that created, adopted or
cold-booted their device), with their phase durations, for the
estimates stim status shows; stats does not print them. Outside a project only the
machine section prints. There is no reset flag: delete that file to start over.
--json prints one line:
{
"version": 1,
"project": { "key": "/path/to/app", "ios": {}, "android": null },
"machine": { "ios": {}, "android": null },
"offload": {
"today": { "here": 3, "offloaded": 0, "fellBack": 0 },
"machines": {},
"placements": []
}
}
project is null outside a project, and a platform with no run yet is
null. A bucket carries runs, failed, hits, misses, coldRuns,
coldRunMs, hitRuns, hitRunMs, timeSavedMs, firstRunAt and
lastRunAt, plus lastColdBuildMs and lastPodsMs once the project has
compiled or installed pods; those two size the progress line a long build
prints (build still compiling (1m00s of ~3m10s)). The saved figure is
an estimate: each cache hit is credited this project's mean cold run at that
moment, minus its own duration, floored at zero.
Every run that compiles also records its placement: where it built (here,
offloaded, or fell-back when it tried a machine and built here), the
reason the run printed on its placement: line, the build time and this
project's last cold build here to compare it with. The placements list under
offload in the stats payload holds the last 100 from the last 7 days, newest
first, and its machines map gives each
remote machine's offloaded builds, offloaded time, estimated time saved and
fallbacks, for today (this machine's calendar day) and in total. The plain output
adds a build placement section with the same counts and the last 5
placements. Placements include slotWaitMs and deviceSlotWaitMs as whole milliseconds waiting for build and owned-device slots, omitted when zero. stim guide facts stats has every field.
Owned-device waits print the count, holder workspace names and elapsed time
at once and about every 10 seconds on stderr. Status JSON includes optional
build.waitingFor: { kind: "build-slot" | "device-slot", inUse, max, since }
without changing the build phase. Overlapping waits show the one that started
first, then the remaining wait.
The optional top-level JSON capacityWaits lists each owned-device slot wait:
{ at, kind: "device-wait", platform, ms, max, workspace, reclaimed? }. ms is whole
elapsed milliseconds, including waits that time out or fail. The list keeps
the last 50 events from 7 days, newest first, and is omitted when empty.
reclaimed is the whole count of devices shut down for that waiting run,
omitted when zero. Plain stats reports waits with positive reclaim counts.
Recording is best effort.
The optional top-level JSON capacityRefusals lists the last 50 device
refusals from the last 7 days, newest first, and is omitted when empty. Each
entry carries at (ISO timestamp), kind: "device", platform (ios or
android), max (concurrency.maxDevices), and workspace (the workspace
id used by status and Desktop). Only STIM_AT_CAPACITY device refusals are
recorded; build caps wait. Recording is best effort and does not change the
refusal or the plain stats output. Desktop uses events within 6 hours for
its device-limit suggestion, including runs from an agent's terminal.
The top-level JSON agentDevice reports agent-device disk usage: version: 1,
measuredAt, total known bytes, complete, stateDir, runnerBuilds,
workspaces, and hosted. Unknown byte fields are null. Runner entries
include last use, agent-device and Xcode versions, and lease/lock/unreadable
in-use flags. Plain output adds an agent-device block when state exists.
$STIM_HOME/agent-device-usage.json caches the result for 10 minutes when roots
match. Only stats and unscoped gc measure it; server and phone stats read
the cache. Stim never trims or deletes the shared runner builds, sessions, logs and other state or the hosted driver dir; a workspace's own agent-device dir goes only with its workspace.
The top-level swiftpmCache reports the user-level SwiftPM cache with
version: 1, measuredAt, dir, present, bytes, and complete, or null
before a cached measurement exists. The directory is
~/Library/Caches/org.swift.swiftpm on macOS, or org.swift.swiftpm under
XDG_CACHE_HOME (default ~/.cache) elsewhere. Missing directories have
present: false, bytes: 0; failed measurements have bytes: null,
complete: false. Plain output adds a SwiftPM cache block after agent-device
only when present. This cache is shared by every SwiftPM build on the machine;
Stim reports it and never deletes it. $STIM_HOME/swiftpm-cache-usage.json
caches du -sk measurements for 10 minutes, keyed on the resolved directory,
with a 20-second timeout. Only stats and unscoped gc measure it; server and
phone stats read the cache. status, start, ios, android and the budget
gate never measure it.
Try it with an agent:
Run `stim stats --json` and tell me whether any build was offloaded to a build
machine today, and why each of the last few builds stayed on this machine.
worktree warm
stim worktree warm [--refresh]
Copies missing ignored entries from the repository's source checkout into the current linked worktree. It accepts a current subdirectory. The source checkout must be available in the same Git repository; running warm in the source checkout refuses.
--refresh updates the source checkout before the copy: it checks the upstream, fetches changes when needed,
fast-forwards whatever branch is checked out there, and installs dependencies or
Pods when the new commits moved a lockfile (or patches/ in an npm project), when nothing is installed, or when
ios/Pods does not match ios/Podfile.lock. See
worktree isolation.
The branch, tracked files, and existing destination entries stay untouched.
Existing directories, including node_modules, are skipped whole. Eligible
ignored .env and local configuration files are included. The source
checkout's nonempty .worktreeexclude replaces its resolved worktree.exclude
setting. See worktree isolation for exclusions.
Wait for warm to finish before any other process writes to the destination. Concurrent writes are unsafe: files created after the initial existence check can be overwritten or removed. This includes edits, installs, builds, Metro, and another warm invocation.
stdout stays empty. stderr reports copied, kept, and failed entries. Failures
exit 1; inspect failed paths before retrying, since partially copied
entries remain and existing directories are skipped. Warm does not install
dependencies or build. It prints the install command when carried
node_modules or Pods do not match this worktree's lockfiles.
worktree remove
stim worktree remove [target] [--force]
Reclaims the target environment, build output, port, and owned device. It then
removes any linked worktree when safe, warmed or not, without requiring a
Stim registry entry. Git-created branches stay. An existing Stim ownership
record permits deleting a branch only when it has no unique commits. On the
source checkout it only reclaims the environment: from the checkout root, every
registered project under it; from a subfolder, only the project at or above that
folder (with the projects nested under it), and it refuses when none is registered there. A bare repository directory
is refused because it is not a worktree. --force permits removal
with uncommitted, untracked, or unpushed work or initialized submodules. A
worktree locked with git worktree lock is refused until you unlock it.
gc
stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces|recordings|parked|archived|archived:<id>|archived-logs|archived-recordings|archived-agent|archived-logs:<id>|archived-recordings:<id>|archived-agent:<id>|watchman|gradle-daemons|gradle-versions>] [--worktrees] [--idle <duration>] [--json]
Reports stale workspace entries, orphaned workspace directories, clean linked
worktrees whose branch is merged or whose pull request was merged or closed,
orphaned owned devices and remote sessions,
stale locks, shared cache sizes, and the memory of the watchman daemon and the
Gradle and Kotlin compile daemons. It does not change anything without
--delete. Without --delete it also runs no git fetch and no gh: it
judges merges from the refs already on disk and the pull request cache that
stim status --watch and the maintenance worktree check write, and shows a
verdict that fresher state could change as not refreshed. --delete fetches
and asks GitHub first. See
removing finished worktrees in bulk
for how gc decides that a branch is merged.
An orphaned device is one this Stim home created that no workspace references.
gc shows the size of an orphaned or stale owned device: for a simulator, the
data size simctl reports; for an AVD, the size of its directory. Other
devices whose names start with stim- appear under "Unrecognized stim-*
devices" with the command that deletes them; gc never deletes them. See
owned devices.
A workspace directory is orphaned when the project root its workspace.json
records is gone from a mounted volume and no registry entry names it. Deleting
a worktree with git worktree remove or rm -rf leaves one behind. A
directory without a readable workspace.json is reported and never deleted.
Nothing deletes a workspace that is in use: a running dev server, a stim ios
or stim android run, a live build, or a held tunnel or remote lock.
stim status caches each folder's size and each worktree's pull request under
$STIM_HOME/disk-usage and $STIM_HOME/pull-requests. worktree remove drops
the entries of the worktree it removes, but not the size entries of its owned
devices' data folders. gc lists the entries of a folder or
worktree that is gone from a mounted volume under "Stale status cache entries",
and --delete removes them. An entry that records no readable path, or whose
volume is not mounted, is kept and counted under "Skipped".
gc also lists recognized leftovers directly in the user's TMPDIR under
"TMPDIR leftovers", with their count, size and a remedy: Stim temp directories
(stim-claim-*, stim-host-*, stim-fakehome-*, stim-hosted-macos-*,
stim-server-*, stim-ws-*), xctrace recordings (instruments*.ktrace),
SwiftPM temp directories (TemporaryDirectory.*, left by swift and xcodebuild) and
Jest's cache (jest_<uid in base 36>). --delete removes a Stim temp directory, recording or SwiftPM
temp directory only when nothing in it changed for 24 hours. It keeps a directory that contains or sits
inside a registered project, holds a live or unverifiable ownership claim or
a .lock directory with no claim set, or cannot be listed, and keeps a recording that
cannot be read, and keeps every recording while xctrace or Instruments runs and every
SwiftPM temp directory while a swift or xcodebuild process runs, or when the
process list cannot be read. Stim's own SwiftPM macOS builds run swift with the
ambient TMPDIR, because SwiftPM keeps the locks for its shared caches there, so
their temp directories land here too. Jest's cache is never deleted. gc never
follows a symlink or acts outside TMPDIR, and with STIM_HOME set it skips
them. Automatic maintenance runs the same deletion at disk pressure level 2.
gc lists each entry of $STIM_HOME/trash under "Worktree trash" with its
size and the worktree it came from: the ignored directories worktree remove
moved out of a worktree before git removed it. --delete deletes an entry
only when it is a remove-* directory with a valid trash.json written by
worktree remove, its ownership claim is free and the worktree it came from is
gone. It keeps one a removal or its background rm still holds, one whose
worktree still exists (a removal killed before git ran; run stim worktree remove on it to finish), and any other directory under trash. A trash entry
that worktree warm wrote (its trash.json has "kind": "warm") holds paths
removed from a copy of a live worktree, so --delete deletes it once its claim
is free, whether or not the worktree exists. See remove a worktree.
--delete also lightens every workspace that is not building and ran no
stim ios, stim android or stim macos in the last 15 minutes, even with
its dev server running or its simulator booted. It clears the build outputs
(derived-data/, gradle-build/, android-cas/, cache-provider/,
macos/build, staged macos/<Product>.app and interrupted-build
macos/staging-* directories) and the project's git-ignored
android/app/build, android/build, android/.gradle and .build, and
prints the bytes freed per workspace. A running, building, unverified or hosted
macOS app keeps its workspace untouched. The workspace keeps its state, logs,
devices, ports, macos/runtime.lock and its claim set. See
workspace build outputs.
gc reports the size of each workspace's logs. The Metro, client and device
logs rotate at about 8 MiB, but a file written by a Stim version before that cap
can be hundreds of MB. --delete trims each of those files that is over 16 MiB
to its newest 8 MiB, whatever --older-than says, in every workspace that is not in use and has no device log collector recorded.
Build transcripts and other files under logs/ are never trimmed.
The Memory section lists long-lived helpers that grow while they run: the
shared watchman daemon with its watched roots, Gradle daemons with their Gradle
version and Gradle home, and Kotlin compile daemons. Each shows its pid,
physical footprint, uptime and whether it is idle, and the section ends with
the memory that can be reclaimed. A plain --delete and --cache all never
stop them; each kind has its own --cache value:
stim gc --delete --cache watchmanremoves the stale roots (a directory that is gone, or a linked worktree git pruned) that no subscription or trigger uses. It then shuts watchman down only when no other client is connected and no root has a trigger; otherwise it keeps the daemon and names each client, such as the Stim workspace whose Metro uses it. Removing roots does not shrink watchman; only a restart does, and the next client that needs watchman starts it again.stim gc --delete --cache gradle-daemonsstops each Gradle daemon that its owngradle --statusreports idle, then each Kotlin compile daemon with no client connection once no Gradle daemon is busy. Nothing stops while a Stim Android build runs. While stim-server runs, it manages the daemons of builds offloaded to this machine, and gc leaves them alone.stim gc --delete --cache gradle-versionsdeletes the version caches (caches/<version>) and wrapper distributions (wrapper/dists/gradle-<version>-*) underGRADLE_USER_HOMEfor each Gradle version no registered project pins in the wrapper of its Android Gradle root or root. Nothing is deleted while a process that may be Gradle using this home runs: a Gradle main class, a Gradle wrapper or distribution jar, or a java process naming a path under the home (one is skipped only when it names no such path and everygradle.user.homeor-git names is an existing Gradle home elsewhere), whilepsfails, while no Stim config exists, while a registered project is missing or its pin cannot be read, while a Gradle directory cannot be listed, or whilecaches,wrapperorwrapper/distsis a symlink or resolves outside the home. A version is also kept when one of its paths is not a plain directory inside the home, when an entry under it cannot be listed or stat'd, when it holds more than 500,000 entries, or, with--older-than <days>, when anything under it changed within that many days. Right before removal gc checks processes and pins again, renames each directory to a.stim-delete-*sibling and deletes it; a latergc --delete --cache gradle-versionsfinishes a removal that failed, even while versions are blocked (not while the home is outside a scopedSTIM_HOMEor its layout is unsafe). Plaingc --deleteand automatic maintenance never touch those directories.modules-2,build-cache-1,jdksand other shared Gradle caches are never deleted, and--cache allnever includes these versions. WithSTIM_HOMEset, gc deletes nothing in a Gradle home outside that directory. At disk pressure level 3, automatic maintenance first stops the idle Gradle and Kotlin daemonsstim gc --delete --cache gradle-daemonswould stop, only when every Gradle process that keeps a version is such a daemon, and waits up to 3 seconds for them to exit, then does the same for versions unmodified formaintenance.gradleVersionIdleDaysdays (7; 0 turns both off); a daemon that did not exit still keeps the versions. The manual command never stops daemons.
Anything gc cannot prove idle is kept, with the reason. With STIM_HOME set,
gc skips these machine-global processes. stim doctor notes a watchman
footprint over 2 GiB.
Unscoped gc also reports agent-device state, including runner builds per
platform and entry, last use and versions, sessions, logs, other state,
workspace directories and stim-server hosted state. JSON sections.agentDevice
contains the same payload as stats.agentDevice; it is null with any
--cache scope, including all. This state never appears under caches and
never changes actionable. Stim never trims or deletes the shared runner builds, sessions, logs and other state or the hosted driver dir; a workspace's own agent-device dir goes only with its workspace.
A lease flag means a live owner or runner matched by start time. A lock flag
means a live or unknown lock owner. Unreadable lock owners and unreadable or
unknown leases conservatively mark entries in use; dead lock owners and leases do not. Clear unused state with
agent-device's own tooling or by removing the directories yourself.
Unscoped gc reports the shared user-level SwiftPM cache after agent-device.
JSON sections.swiftpmCache carries the same payload as stats.swiftpmCache.
Any --cache scope, including all, reports null without measuring it.
It never appears in caches or changes actionable; --delete and
--older-than leave it untouched. Stim never deletes this cache.
Unscoped gc also reports other tools' machine-wide caches with each size and
the owning tool's remedy, as JSON sections.machineCaches:
- Gradle version caches and wrapper dists for versions no registered project
pins in the Gradle wrapper of its Android Gradle root (
android.gradleRoot, defaultandroid, relative to the app and inside the repository; a custom root must holdsettings.gradleand the module build script) or of its root, withstim gc --delete --cache gradle-versionsas the remedy, and themodules-2dependency cache. A registered project that is missing, whose settings cannot be resolved, or whose wrapper cannot be read or names no Gradle version is named, since it may still pin a listed version; every version a readable wrapper of it pins still counts as pinned. A directory gc cannot list adds a notice and marks the section incomplete. - iOS simulator runtimes with their simulator count. Only a deletable runtime
no simulator uses gets
xcrun simctl runtime delete <id>. - Xcode device support entries, which Xcode copies again when the device next connects.
- agent-device runner builds grouped by agent-device and Xcode version, compared with the installed ones. agent-device has no prune command, so the remedy is to remove an entry that is not in use.
The sizes, runtimes and tool versions are cached for 10 minutes. Any --cache
scope reports null without measuring. This section never appears in
caches or changes actionable; --delete and --older-than leave it
untouched. Stim never deletes these caches.
While it works, gc prints each slow step on stderr as it starts, such as
daemons watchman pid 49040: checking 12 roots, so a long run shows what it
is waiting on. The report stays on stdout, and --json prints only its payload
there.
--older-than <days>also selects devices and workspace build outputs of workspaces no Stim command has used for that many days, and unused cache entries. It limits the parked simulators and emulators--deleteclears to those parked at least that many days. A cache directory it cannot list (EACCES, EPERM or ELOOP, for example) counts as a failed trim, not an empty one: gc prints how many entries could not be read or removed, records the cache as failed and exits 1. A directory that is missing or is not a directory is not a failure.--cache <name|all|workspaces|recordings|parked>with--deleteempties the caches whose name or directory carries<name>whole, or every cache and the workspace build outputs withall.workspacesclears only the workspace build outputs, and skips the workspace directories that plain--deleteremoves whole (those of dead projects and orphaned directories). A plaingc --jsondry run carriesscopedEmptyon each cache, which says whether--delete --cachewithout--older-thanwould empty it or leave it alone, and why. When several caches share a name, as the Metro transform cache of each project does, gc names each one<name>: <directory>using its directory's last component, so the report and--cachetell them apart.parkederases the parked simulators and emulators and keeps them parked;allleaves them alone.watchmanandgradle-daemonsact on those helpers alone (see above), and--older-thanwith them is refused. Other devices and project entries are not inspected, so a scoped run empties caches and reaps nothing.--worktreesalso selects every clean, idle linked worktree that has a Stim workspace, not only the merged ones plaingcselects. With--deletegc runsstim worktree removewithout--forceon each of them. Idle means unused for--older-thandays, or 7 days without that option. A worktree with no commits of its own beyond the default branch and no recorded Stim use is idle when its git and Stim activity is that old. It cannot be combined with--cache. See removing finished worktrees in bulk.--idle <duration>shuts down owned simulators and emulators whosestatusactivity has been idle for at least<duration>, such as30m,2hor1d. It acts without--delete, never deletes, and leaves each device assigned to its workspace, likestim stop. It skips a device that is driven, whose activity is unknown, or whose workspace has a build in progress, and re-checks each device before shutting it down. Without--idle,gclists idle devices and how long they have been idle. Physical and remote devices are out of scope. It cannot be combined with--cache.--jsonprints the report as one object on stdout and every other line on stderr. Agents use it to show you whatgc --deletewould remove before they ask to run it.
On a hosting Mac, unscoped stim gc also lists parked hosted iOS and Android
devices; stim gc --delete deletes each through its session home's stop worker
under a session claim. Held claims and unverified ownership keep the device,
and an unreadable journal keeps all hosted devices. Android deletion also
requires the AVD to be visible from this shell's Android environment; otherwise
run gc with the server's ANDROID_AVD_HOME/HOME. Records no longer listed in
their session ledger are already removed, skipped and excluded from actionable.
--older-than filters by
parked time; cache scopes omit hosted sessions. JSON adds
sections.parkedHostedDevices entries with session, client (id), platform,
id (UDID or AVD name), name, parkedAt, and
listed (session-home ledger ownership, null when unreadable). Deletion results
use kind: "parkedHostedDevice". The server clears the parked marker when it
reconciles the emptied ledger.
Unscoped stim gc also lists stale entries of the machine EAS session ledger
(~/.stim/machine/eas/sessions.json): entries whose id is not a UUID and whose
absolute workspace root is missing on a mounted volume, such as ids written by a
test fixture. A valid id, an existing root, or a root on an unmounted volume
keeps the entry. stim gc --delete re-checks each entry and removes it under
the ledger lock without contacting EAS. JSON adds sections.staleEasLedgerEntries
entries with id and workspaceRoot.
--json prints one line. Each key under sections is one section of the text
report, in the same order, and is always present. stim gc --worktrees --json
prints, for example:
{
"mode": "dry-run",
"idle": null,
"cacheScope": null,
"olderThan": null,
"worktreeSweep": { "olderThan": 7, "defaulted": true },
"actionable": true,
"failures": null,
"results": [],
"sections": {
"deadProjects": [{ "path": "/path/to/removed-app" }],
"orphanedWorkspaces": [{ "dir": "~/.stim/workspaces/old--1a2b", "projectRoot": "/path/to/old", "bytes": 52428800 }],
"linkedWorktrees": [
{
"path": "/path/to/feature",
"idleDays": 12,
"mergedInto": null,
"pullRequest": null,
"pullRequestUnknown": null,
"willRemove": true,
"reason": null,
"detail": "idle 12d",
"eligibleAt": null,
"buildState": { "bytes": 0, "dirs": [] }
},
{
"path": "/path/to/shipped",
"idleDays": 0,
"mergedInto": "origin/main",
"pullRequest": null,
"pullRequestUnknown": null,
"willRemove": true,
"reason": null,
"detail": "merged into origin/main",
"eligibleAt": null,
"buildState": { "bytes": 0, "dirs": [] }
},
{
"path": "/path/to/just-merged",
"idleDays": 0,
"mergedInto": "origin/main",
"pullRequest": null,
"pullRequestUnknown": null,
"willRemove": false,
"reason": "recent-activity",
"detail": "recent activity: merged into origin/main 12m ago; removable after 2026-09-25T15:48:00.000Z",
"eligibleAt": "2026-09-25T15:48:00.000Z",
"buildState": { "bytes": 0, "dirs": [] }
},
{
"path": "/path/to/wip",
"idleDays": 20,
"mergedInto": null,
"pullRequest": {
"number": 123,
"state": "merged",
"url": "https://github.com/acme/app/pull/123",
"containsHead": true
},
"pullRequestUnknown": null,
"willRemove": false,
"reason": "dirty",
"detail": "dirty: 2 uncommitted or untracked files (notes.txt, wip.ts)",
"eligibleAt": null,
"buildState": {
"bytes": 2147483648,
"dirs": [
{ "path": "node_modules", "bytes": 1610612736 },
{ "path": "ios/Pods", "bytes": 536870912 }
]
}
}
],
"unlistedWorktreeDirectories": [
{
"path": "/path/to/repo/.worktrees/lost",
"repository": "/path/to/repo",
"bytes": 847249408,
"remedy": "rm -rf '/path/to/repo/.worktrees/lost'"
}
],
"workspaceLogs": [
{
"dir": "~/.stim/workspaces/old-feature--9e8f",
"projectRoot": "/path/to/old-feature",
"bytes": 778043392,
"trimBytes": 761266176,
"willTrim": true,
"reason": null,
"detail": null
}
],
"workspaceBuildOutputs": [
{
"dir": "~/.stim/workspaces/app--3c4d",
"projectRoot": "/path/to/app",
"bytes": 1073741824,
"idleDays": 0,
"willClear": false,
"reason": "in-use",
"detail": "in use: its dev server supervisor (pid 4242) is running"
}
],
"caches": [],
"agentDevice": {
"version": 1,
"measuredAt": "2026-10-05T17:44:35.000Z",
"bytes": 8192,
"complete": true,
"stateDir": {
"dir": "/Users/example/.agent-device",
"present": true,
"bytes": 8192,
"sessions": { "dir": "/Users/example/.agent-device/sessions", "bytes": 4096, "count": 1 },
"logs": { "dir": "/Users/example/.agent-device/logs", "bytes": 4096 },
"other": { "bytes": 0, "largest": [] }
},
"runnerBuilds": {
"dir": "/Users/example/.agent-device/apple-runner",
"present": false,
"bytes": 0,
"sharedBytes": 0,
"platforms": []
},
"workspaces": [],
"hosted": null
},
"swiftpmCache": {
"version": 1,
"measuredAt": "2026-10-05T17:44:35.000Z",
"dir": "/Users/example/Library/Caches/org.swift.swiftpm",
"present": true,
"bytes": 1048576,
"complete": true
}
}
}
The example omits the empty sections and machineCaches. agentDevice,
swiftpmCache and machineCaches are null only with a --cache scope, including all. memory lists each helper process as
{ kind, cacheKind, pid, startedAt, bytes, measure, version, gradleHome, offloadClient, state, reclaimable, reason, detail },
where reclaimable marks the ones gc --delete --cache <cacheKind> would
stop, and watchmanRoots lists each root with its stale reason and whether
it is removable. reason is null for an entry --delete
acts on and otherwise a stable code; detail is the text the report prints.
A linked worktree's eligibleAt is the time a recent-activity worktree
becomes removable, and otherwise null. pullRequest is the pull request of
the worktree's branch whose head is or contains HEAD, found with gh, with
state "open", "merged" or "closed"; pullRequestUnknown says why gh
could not answer, such as "gh is not installed". buildState sizes the
worktree's in-tree build and dependency directories, the ones automatic
maintenance clears plus a root .pnpm-store, largest first; du counts APFS
clones in full, so the sum can exceed what deleting them frees. gc reuses a
size for up to an hour while no entry was added to or removed from the
directory, measures the rest with one du per directory, 4 at a time with a
10-second limit each, and reports a directory it could not measure in time as
null.
unlistedWorktreeDirectories lists directories directly under a repository's
.worktrees/ that git worktree list does not show and that have no .git,
or a .git file whose gitdir is gone; a worktree of another repository placed
there is not listed. gc never removes them,
because it cannot check them for uncommitted work; remedy is the command to
run after you have looked through one.
bytes is null when the size is unknown. worktreeSweep is null without
--worktrees, which still reports merged worktrees. With --delete, mode is "delete", the sections list what
the run acted on, results lists each outcome, and failures counts the entries it could not delete. A
nonzero count exits with status 1. Run stim gc --json again to see what is
left. idle is the --idle duration in milliseconds or null, and with
--idle failures also counts devices it could not shut down. A --cache
name that no cache carries, or --cache together with --worktrees or
--idle, exits with status 1 and prints
{ "code": "STIM_BAD_ARG", "message": "...", "remedy": "..." }.
Ctrl+C stops pending GC actions and drains verified owned children before exiting 130. Completed actions remain applied. Unverifiable cleanup retains its ownership
claims and prints STIM_CLAIM_UNAVAILABLE with a recovery remedy. In JSON mode,
cancellation prints one STIM_CANCELLED payload, or the cleanup refusal when
cleanup cannot be verified.
Each results entry is { kind, status, label, id, bytes, detail }. status
is "done", "kept" or "failed", and detail says why an entry was kept or
failed. For example, a deleted simulator reads
{ "kind": "device", "status": "done", "label": "stim-app (iPhone 17 26.5)", "id": "9C1F...", "bytes": null, "detail": null }.
results is empty on a dry run.
A dry run without --cache or --idle also carries inventory, which is
null otherwise. It lists every simulator and AVD on the machine with its
runtime or system image, last use and owner: workspace (with project and
slot), parked, orphaned, otherStimHome for a stim-* device this Stim
home did not create, or user. It also lists the iOS simulator runtimes and
Android system images with how many devices use each (a runtime that
simctl runtime list does not show has no size), and the
xcrun simctl runtime delete or sdkmanager --uninstall command that removes
it. inventory.notices says why a listing is missing or partial, such as an
AVD or system image folder Stim cannot read. When macOS privacy protection
blocks the read, for example for AVDs on an external disk, the notice names the
Privacy & Security setting that allows the app running Stim.
The inventory is report only: Stim never runs those commands and never
acts on a device through it.
stim guide facts gc lists every section, field and reason code.
Try it with an agent:
Run `stim gc` and show me which owned simulators and emulators are idle and for
how long. Then run `stim gc --idle 2h` to shut down the ones idle that long.
Run `stim gc` and show me how much memory watchman and the Gradle and Kotlin
daemons use and what is reclaimable. If watchman has no clients, run
`stim gc --delete --cache watchman`; then run
`stim gc --delete --cache gradle-daemons` to stop the idle daemons.
guide
stim guide [topic] [section]
Prints version-matched reference text. Topics are agent, facts, metro, ports,
logs, errors, lifecycle, cleanup, and settings. Those topics also cover caches,
remote devices, and release builds. The errors, lifecycle, facts, and cleanup
topics have sections: called bare they print a section index, and a named
section prints on its own. stim guide errors lists every refusal code and
stim guide errors <CODE> prints one.
The bare index and the agent topic open with a STATUS block when something is
due: doctor for a platform that never ran in this app, ran more than seven
days ago, or ran under another Stim version (outside a React Native or Expo app
there is no doctor line); and a newer Stim release, checked
against the npm registry at most once a day and skipped when
STIM_NO_UPDATE_CHECK is set. The block is omitted when nothing is due.
Structured output and exit codes
Use plain output for an agent workflow. It streams progress and includes all
facts needed for the next step. Use --json when a script must parse the result.
Commands exit with code 0 on success. Build, launch, ownership, or input errors
exit with a nonzero code and print an error code, message, and remedy. An empty
logs result exits with code 0.
When start, ios, or android reclaimed disk or memory before it started,
its --json payload, on success or failure, carries a reclaimed array with
one { step, targets, failures, freedMb } entry per step that acted. A machine
still below budget.hardFloorDiskGb after reclaiming refuses with
STIM_LOW_DISK, naming the largest uses of disk. Run stim guide errors STIM_LOW_DISK for the remedies.
Archive cache selectors
Archives are excluded from normal gc, --cache all, and unscoped --older-than.
--cache archived lists archives with id, kinds, bytes and expiry. Add
--delete to remove them. --cache archived:<id> selects one archive; an
unknown id refuses with STIM_BAD_ARG and lists known ids. archived-logs,
archived-recordings, and archived-agent select only that kind and preserve
the record. Add :<id> (for example archived-logs:<id>) to clear one kind of
one archive; an unknown id refuses the same way. --older-than filters by
removal age for whole and per-kind selection; an explicit id ignores it. Archive selection reports abandoned
staging and keeps live or unresolved claims, naming the claim removal command.
- Global
- npx
stim gc --cache archived
npx stim gc --cache archived
- Global
- npx
stim gc --delete --cache archived-logs --older-than 14
npx stim gc --delete --cache archived-logs --older-than 14
status --json adds archived and archivedUsage, and stats prints an
archive usage line. See archived workspaces.
stim-server setup
A person on the worker machine runs stim-server setup to set it up and approve
build and/or device-host access for the machine holding one setup ticket, until
one expiry. The requesting machine makes the ticket: Stim Desktop's Add Machine
flow shows the command, and stim doctor --fix prints it on a machine without
Desktop. Each grant asks Y/n in a terminal (Enter approves); --yes is
required to approve without a terminal. Agents never run setup
or approve requests.
Setup reuses Desktop's server when it already answers and has a tailnet route.
Without a terminal or --yes, it refuses before installing anything unless
every requested capability already has a matching approval. A typed N, Ctrl-C
or SIGTERM exits 1; an interrupt completes the journal and releases the setup
claim. Setup never enables Funnel,
and never changes macOS permission settings. An SSH-driven run is not offered.
Setup prints one line per step; --verbose adds install, service and route
details and the long summary. --json is unchanged.
- Global
- npx
STIM_SETUP_TICKET=<ticket> stim-server setup --expires <ISO-time> --build --device-host
STIM_SETUP_TICKET=<ticket> npx --yes --package @stim-cli/server@<version> stim-server setup --expires <ISO-time> --build --device-host
See the server command reference for flags, permissions, journals, exit codes and undo commands. The remote machines guide walks through Desktop and manual setup.