Skip to main content

Settings reference

Command examples

Commands use stim. If it is not installed globally, replace stim with npx stim.

Most projects need no settings. Use stim guide settings for descriptions that match the installed version.

Read and change settings​

stim settings lists every setting with its effective value and the layer it comes from. get, set, and unset read or change one layer. The command reference has the full syntax and JSON output.

stim settings
stim settings set ios.deviceType "iPhone 17 Pro" --scope workspace
stim settings set optimizations.android.targetAbiOnly false --scope machine

A copyable prompt for an agent:

Run `stim settings --json` in this app and tell me which settings are not at
their defaults and which layer sets each one. Then set this workspace's iOS
simulator to an iPhone 17 Pro with
`stim settings set ios.deviceType "iPhone 17 Pro" --scope workspace`.

The JSON Schema for every setting ships in the stim package as dist/settings.schema.json. Add it to .stim.json for editor completion and validation; Stim ignores the $schema key:

{
"$schema": "https://unpkg.com/stim/dist/settings.schema.json",
"ios": { "deviceType": "iPhone 17 Pro" }
}

The schema's root describes .stim.json. $defs.machine describes the machine settings. Each setting carries its dotted key, the layers it can be written to, and its environment override under x-stim.

Settings layers​

Stim reads the first value found in this order:

  1. Workspace settings in ~/.stim/config.json, keyed by the app's absolute path (--scope workspace).
  2. Repository settings in the same machine file, keyed by the git common dir (--scope repo).
  3. Committed .stim.json beside the app's package.json (--scope committed).
  4. Machine defaults in ~/.stim/config.json (--scope machine), for the settings whose Layers column below includes machine.
  5. The Stim default.

An environment variable that overrides a setting wins over every layer.

Nested objects merge by key. Arrays replace lower-precedence arrays. Unknown keys produce a warning. Every key below takes one type: a string, an array of strings, a number, a boolean, or an object such as android.avdConfig, cache.options, and the nested optimizations settings. ios.remote, android.remote, macos.remote, metro.tunnel, optimizations.android.compilerCache and optimizations.android.pch take only their listed choices. A value of the wrong type or outside those choices is refused by name on every command that resolves settings, stim ios included, so a wrong shape never falls back to a default silently. stim doctor reports it as a finding instead of refusing. The exception is optimizations.android.casToolchain: an invalid value warns and falls back to ccache, or no compiler cache when compilerCache is none. doctor also reports the invalid setting.

Committed settings​

Each monorepo app reads its own .stim.json; it does not inherit an ancestor's runtime configuration. Single-app repositories still use their root file. When upgrading, move runtime settings to each relevant app and make profile, AVD-fragment and committed-provider paths relative to that app directory. Explicit machine project/repository overrides keep their existing precedence.

These settings can be set for a project. The Layers column lists where each one can live; committed means .stim.json. The build optimizations page explains the optimizations switches.

KeyTypeLayersDefaultEnvironment overrideDescription
optimizations.buildCachebooleanmachine, workspace, repo, committedtrueRead and write native build artifacts
optimizations.remoteBuildCachebooleanmachine, workspace, repo, committedtrueUse remote artifact providers
optimizations.releaseBundleSwapbooleanmachine, workspace, repo, committedtrueSwap current JS into cached release builds
optimizations.metroSharedCachebooleanmachine, workspace, repo, committedtrueAppend Stim's shared Metro cache store
optimizations.metroWarmupbooleanmachine, workspace, repo, committedtruePrefetch the development bundle during ios and android
optimizations.ios.compilationCachebooleanmachine, workspace, repo, committedtrueXcode compilation caching
optimizations.ios.swiftCompilationCachebooleanmachine, workspace, repo, committedunsetSwift compilation caching; unset detects toolchain support
optimizations.ios.prefixMappingbooleanmachine, workspace, repo, committedtrueClang source and DerivedData prefix mapping
optimizations.android.compilerCacheauto, ccache, cas, nonemachine, workspace, repo, committedautoAndroid compiler cache backend
optimizations.android.casToolchainabsolute pathmachine, workspace, repo, committedunsetSTIM_ANDROID_CAS_TOOLCHAINAbsolute path to the Android CAS toolchain manifest
optimizations.android.pchauto, on, offmachine, workspace, repo, committedautoAndroid precompiled headers
optimizations.android.gradleBuildCachebooleanmachine, workspace, repo, committedtrueGradle build cache
optimizations.android.targetAbiOnlybooleanmachine, workspace, repo, committedtrueNarrow Debug and hosted Android builds to the device ABI
ios.deviceTypestringmachine, workspace, repo, committedunsetSimulator model for owned simulators
ios.runtimestringmachine, workspace, repo, committedunsetiOS runtime owned simulators are created on
ios.configurationstringworkspace, repo, committedDebugXcode configuration to build, such as Debug or Release
ios.schemestringworkspace, committedunsetShared Xcode scheme to build when the workspace has several; the --scheme flag overrides it
ios.remotestring: eas, proxy, local, auto, remote, remote-only, or a tailnet machine namemachine, workspace, repo, committedunsetWhere the iOS simulator runs: local here (overriding a lower layer); auto here first, then a remote.machines entry when this machine is full or busy; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine; eas or proxy a remote device backend
ios.projectPathpath relative to the appworkspace, committediosDirectory under the app that holds a bare React Native app's Xcode project and Podfile
ios.simslimon, offmachine, workspace, repo, committedoffSlim every Stim-owned iOS simulator with SimSlim's default categories when no ios.simslimProfile is set; off in a project opts that app out
ios.simslimProfilepath relative to the appworkspace, repo, committedunsetSimSlim JSON profile under the app
ios.signingIdentitystringworkspace, repo, committedunsetKeychain identity used to re-seal a device build
ios.signingIdentitySha1string: a 40-character hex SHA-1 hashworkspace, repo, committedunsetSHA-1 of the signing identity, when two share a name
ios.lanHoststring: a bare address or hostname, never a URL, scheme, or portworkspace, repo, committedunsetAddress a phone uses to reach this workspace's Metro
android.systemImagestringmachine, workspace, repo, committedunsetSDK system image for owned AVDs
android.deviceProfilestring: an avdmanager device id such as pixel_6 or Nexus 5X (avdmanager list device -c)machine, workspace, repo, committedpixel_6avdmanager hardware profile id for new owned AVDs
android.dataPartitionSizeGbinteger 6..16384workspace, repo, committed8Data partition size of a new owned AVD, in GiB
android.avdConfigFilepath relative to the appworkspace, repo, committedunsetAVD config.ini fragment under the app
android.avdConfigobjectworkspace, repo, committedunsetValidated AVD config values
android.gradleRootpath relative to the appworkspace, committedandroidDirectory relative to the app holding gradlew and settings.gradle; may be outside the app but not the repository
android.modulestring: a Gradle project path such as :appworkspace, committed:appThe app's Gradle project path, such as :app
android.variantstringworkspace, repo, committedunsetGradle build variant
android.artifactInputsobjectworkspace, repo, committedunsetComplete repeatable native Gradle inputs for local APK caching, including declared local files and environment names
android.offloadInputsobjectworkspace, repo, committedunsetNative Gradle source sufficiency, exact ignored inputs and generated output directories for build workers
android.keystorepathworkspace, repo, committedunsetKeystore a re-packed release APK is signed with
android.keystorePasswordstringworkspace, repo, committedunsetKeystore password, or an apksigner env: or file: reference
android.remotestring: eas, proxy, local, auto, remote, remote-only, or a tailnet machine namemachine, workspace, repo, committedunsetWhere the Android emulator runs: local here (overriding a lower layer); auto here first, then a remote.machines entry when this machine is full or busy; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine; eas or proxy a remote device backend
metro.tunnelauto, off, expo, cloudflared, ngrok, tailscaleworkspace, repo, committedautoHow a remote device reaches Metro; tailscale is tailnet-only and requires explicit selection
metro.commandstring arrayworkspace, committedunsetCommand that starts this project's dev server, as an argv array run from the app directory; {port} is replaced with the reserved Metro port
metro.ngrokUrlstringworkspace, repo, committedunsetStable ngrok URL for the managed tunnel
metro.publicUrlstringworkspace, repo, committedunsetExisting public Metro URL
metro.portinteger 1024..65535workspace, repo, committedunsetSTIM_METRO_PORTThis workspace's Metro port, reserved instead of one Stim picks
metro.warmupUrl.iosbundle URLworkspace, repo, committedunsetBundle URL stim ios prefetches to warm Metro
metro.warmupUrl.androidbundle URLworkspace, repo, committedunsetBundle URL stim android prefetches to warm Metro
metro.idleStopMinutesinteger >= 0workspace, repo, committed60Minutes without a bundle request, client log or Stim command before the dev server stops; 0 never
devices.idleShutdownMinutesinteger >= 0machine, workspace, repo, committed30Minutes an owned simulator or emulator stays idle before the workspace supervisor, or automatic maintenance when no supervisor runs, shuts it down; 0 never
devices.reclaimIdleMinutesinteger >= 0machine, workspace, repo, committed10Minutes an owned simulator or emulator stays idle before a waiting run reclaims its device slot; 0 never
macos.productstring: a Swift Package executable product nameworkspace, repo, committedunsetThe Swift Package executable product stim macos builds in Debug.
macos.infoPlistpath relative to the appworkspace, repo, committedunsetDevelopment Info.plist relative to the Swift Package directory.
macos.artifactInputsobjectworkspace, repo, committedunsetComplete pinned, repeatable SwiftPM inputs producing a relocatable app for completed artifact caching.
macos.argumentsstring arrayworkspace, repo, committedunsetArguments passed directly to the owned macOS development executable.
macos.remotestring: local, auto, remote, remote-only, or a tailnet machine name (macOS has no eas or proxy backend)machine, workspace, repo, committedunsetWhere stim macos launches the app: local here; auto here unless this machine is busy, then a remote.machines entry; remote on a remote.machines entry in order, here only when none takes it; remote-only the same but never here; a machine name exactly that machine. The --remote flag overrides it
macos.assetCatalogpath relative to the appworkspace, repo, committedunsetOptional .xcassets directory relative to the Swift Package directory.
macos.resourcesobjectworkspace, repo, committedunsetResource destinations under Contents/Resources mapped to sources relative to the Swift Package directory.
web.urlstring: an http:// or https:// URL; {port:<label>} is replaced by a named or the Metro portworkspace, repo, committedunsetPage stim web opens; unset opens the Metro URL for Expo web
web.ignoreCertificateErrorsbooleanworkspace, repo, committedfalseAccept self-signed dev certificates in Stim's owned Chrome profile
web.viewportdesktop, phoneworkspace, repo, committeddesktopViewport of the owned Chrome page: desktop, or phone for a 390x844 touch screen
recording.enabledbooleanmachine, workspace, repotrueSTIM_RECORDINGLet stim-server record device screens for replay; false stops it and deletes the recordings
workspace.hiddenbooleanworkspacefalseHide this workspace in Stim Desktop and the phone app; the next command that uses it unhides it
worktree.excludestring arrayrepo, committed (repository root)unsetIgnored paths worktree warm skips
worktree.defaultBranchstringrepo, committed (repository root)unsetBranch worktree warm --refresh expects the source checkout on
cache.providerstringworkspace, repo, committedunsetSecond-tier cache provider module
cache.optionsobjectworkspace, repo, committedunsetOptions passed to the cache provider
remote.easFallbackbooleanmachine, workspace, repo, committedfalseWhen ios.remote or android.remote is auto or remote, this machine is at its concurrency.maxDevices cap and no remote.machines entry takes the run, run the simulator or emulator on a billed EAS Simulator instead of waiting or refusing, when eas-cli can; remote-only never uses it

metro.port and STIM_METRO_PORT pin Metro for stim start, stim ios, stim android and pages using Metro in stim web. Changing the pin while this workspace's dev server runs is refused until stim stop; an unverified supervisor also blocks the change until you stop it with the tool that started it. You can also unset or restore the pin. A foreign holder is refused before anything starts or the port is reserved. This project's own Metro already on the pin is attached to. With --no-metro-check, iOS and Android use the pin, then the recorded port, then 8081, without probing, reserving or writing the registry. An invalid pin still refuses with STIM_BAD_ARG. Release builds skip Metro. A web.url without {port:metro} ignores the pin.

worktree warm reads repository-wide copy settings from the source checkout's root .stim.json, not individual app files. Keep worktree.exclude and worktree.defaultBranch there. worktree.defaultBranch is read only by worktree warm --refresh, which warns when the source checkout sits on another branch; unset, it uses the branch origin/HEAD names. A nonempty .worktreeexclude in the source checkout replaces its resolved worktree.exclude setting; an empty or absent file uses the setting.

Do not put secrets in a committed .stim.json. Keep secrets in ignored files and carry those files into a worktree. stim settings never prints android.keystorePassword and writes it to .stim.json only as an env: or file: reference.

cache.provider names a module that Stim executes in every worktree of the app. Review a committed value the way you review a build script, and keep provider credentials in the environment or in machine settings. cache.options merges by key from the layer that selects the provider, higher-precedence layers that name no provider, and lower-precedence layers that name the same provider resolved from the same directory. A machine layer can therefore override one option of a committed provider, but options written for a different provider, or added by a lower layer that names no provider, are ignored. Stim reads the module for stim ios and stim android; Metro uses it only when the project's own metro.config.js calls sharedCacheStores() from @stim-cli/metro.

metro.warmupUrl.ios and metro.warmupUrl.android replace the bundle URL that ios and android prefetch while the native build runs. Unset, Stim uses the app's most recent bundle request on that platform from the workspace's metro log (skipped when its entry file no longer exists), then Expo's manifest or the bare React Native default. Give an HTTP(S) URL or a path ending in .bundle with the app's full query, including a matching platform. Stim keeps the path and query but always requests this workspace's Metro port. The setting only changes the prefetch, not the app. Setting optimizations.metroWarmup to false turns the prefetch off. Run stim guide settings for the full rules.

{
"metro": {
"warmupUrl": { "ios": "/src/main.bundle?platform=ios&dev=true&lazy=true" }
}
}

Default simulator model and runtime​

ios.deviceType, ios.runtime, android.systemImage, and android.deviceProfile can all be set at the machine layer, so one machine can default every workspace's owned simulator or emulator without touching each project's .stim.json. A workspace, repo, or committed layer still wins over the machine value, and the --device-type, --runtime, --system-image, and --device-profile flags still win over every layer:

stim settings set ios.runtime 26.2 --scope machine
stim settings set ios.deviceType "iPhone 17 Pro" --scope machine
stim settings set android.systemImage "system-images;android-36;google_apis;arm64-v8a" --scope machine

A model or runtime the machine value names that no installed toolchain offers refuses with STIM_BAD_ARG, the same way an unresolvable per-project or per-invocation value does, and the message names the machine layer so it is clear where to fix it.

SimSlim for every owned simulator​

ios.simslim takes on or off, default off, and can be set at any layer. Set it on the machine layer to slim every Stim-owned iOS simulator on that Mac with SimSlim's default service categories, without adding a profile to each app:

stim settings set ios.simslim on --scope machine

A project ios.simslimProfile still applies in place of the defaults, whatever ios.simslim says. A workspace, repo, or committed ios.simslim of off opts that app out and restores stock services on its simulator. SimSlim's defaults disable push, StoreKit, iCloud, Safari sync, Mail, Calendar and Contacts services among others, so move an app that needs them to a profile that keeps those categories or to off. Install SimSlim once per Mac with brew install mobai-app/tap/simslim; without it doctor reports the gap and stim ios boots normally with a one-line note.

Android AVD overrides​

New owned AVDs use the Pixel 6 hardware profile (1080 × 2400 pixels at 420 dpi) unless android.deviceProfile names another one, spelled as avdmanager list device -c prints it. Use pixel_tablet for a tablet or pixel_fold for a foldable; avdmanager writes the foldable's hinge and posture keys from the profile. The machine layer can set a default for every workspace:

stim settings set android.deviceProfile pixel_tablet --scope machine

A set profile counts as a request, so a workspace that already owns an AVD of another profile refuses until that AVD is removed, and each run checks the id with avdmanager. An id that avdmanager does not offer refuses with STIM_BAD_ARG and lists the offered ids. A parked AVD is adopted only by a workspace that requests the same profile. Existing AVDs keep their display settings. Parked AVDs created with the old generic profile are not adopted by new workspaces.

android.avdConfigFile reads an Android config.ini file. android.avdConfig provides the same safe keys as JSON. Stim applies these values only when it creates a new owned AVD. It never rewrites an existing AVD or changes generated identity and storage paths, with one exception: every AVD Stim creates gets hw.keyboard=yes unless these settings set hw.keyboard, and a parked AVD created before that default gets it when a workspace adopts it. A hardware keyboard lets Stim Desktop and stim-server type through the emulator's gRPC endpoint; the on-screen keyboard still opens.

The validated keys cover CPU count, RAM, heap size, screen density, graphics, orientation, network conditions, and common hardware switches. On displayless Linux, Stim also launches the emulator with -no-window -noaudio -no-boot-anim. With androidEmulatorApp resolved to "stim-desktop" on macOS, Stim launches it with -no-window -gpu host; see Machine settings. Every owned emulator starts its gRPC endpoint on the console port plus 3000 with token authentication (-grpc <port> -grpc-use-token); Stim Desktop reads emulator frames from it and sends input through it while Take over is on. Run stim guide settings for the complete key and value list.

Machine settings​

~/.stim/config.json also supports:

{
"concurrency": { "maxBuilds": 2, "maxDevices": 3 },
"budget": { "minFreeDiskGb": 20, "hardFloorDiskGb": 5 },
"iosSimulatorApp": "xcode",
"androidEmulatorApp": "emulator",
"tempDir": "/Volumes/SSD/stim-tmp",
"pool": { "iosParkedMax": 3, "androidParkedMax": 3 },
"remote": { "machines": ["janics-mac-mini"], "build": "auto" },
"caches": {
"buildCache": "/Volumes/Cache/stim/build-cache",
"metroCache": "/Volumes/Cache/stim/metro-cache"
}
}

These settings live only in the machine file. The budget, archive and maintenance settings are listed in their own sections below.

KeyTypeLayersDefaultEnvironment overrideDescription
ios.appStoreConnectKeyPathabsolute pathmachineunsetSTIM_ASC_KEY_PATHApp Store Connect API key (.p8) that stim device provision ios authenticates with
ios.appStoreConnectKeyIdstring: the key ID App Store Connect showsmachineunsetSTIM_ASC_KEY_IDKey ID of ios.appStoreConnectKeyPath
ios.appStoreConnectIssuerIdstring: the issuer ID App Store Connect showsmachineunsetSTIM_ASC_ISSUER_IDIssuer ID of ios.appStoreConnectKeyPath
iosSimulatorAppxcode, siniulator, stim-desktopmachinexcode; stim-desktop with Stim Desktop installedApp that displays an owned iOS simulator; stim-desktop by default when Stim Desktop is installed
androidEmulatorAppemulator, stim-desktopmachineemulator; stim-desktop with Stim Desktop installedApp that displays an owned Android emulator Stim boots on macOS; stim-desktop by default when Stim Desktop is installed
concurrency.maxBuildsinteger >= 0machine0STIM_MAX_BUILDSConcurrent native builds; 0 means no limit
concurrency.maxDevicesinteger >= 0machine0STIM_MAX_DEVICESBooted owned devices; 0 means no limit
gc.worktreeGraceMinutesinteger >= 0machine120STIM_GC_WORKTREE_GRACE_MINUTESMinutes after a linked worktree last changed, or its branch or pull request finished, before gc removes it; 0 removes it at once
pool.iosParkedMaxinteger >= 0machine3; 0 under STIM_HOMESTIM_POOL_IOS_PARKED_MAXParked simulators kept for adoption; 0 turns parking off
pool.androidParkedMaxinteger >= 0machine3; 0 under STIM_HOMESTIM_POOL_ANDROID_PARKED_MAXParked emulators kept for adoption; 0 turns parking off
debug.logsbooleanmachinefalseSTIM_DEBUGDebug-level logging. While on, the CLI records its commands, child processes and requests to other machines, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets
hosting.agentDrivernone, agent-devicemachinenoneTool this Mac starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client; none starts nothing
server.buildsPausedbooleanmachinefalseTemporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available
server.devicesPausedbooleanmachinefalseTemporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available
server.acceptClientBuildsbooleanmachinefalseWhether a machine approved for builds or device hosting here may update this machine's stim-server service to that machine's own stim-server build; releases from npm need no setting
remote.machinesstring arraymachineunsetTailscale names of the remote machines this machine may build on and host owned simulators, emulators and macOS apps on, each optionally with :<port> of its tailscale serve route (default 7443). The order is the placement priority: each workload goes to the first machine with room and a load under its own limit. What each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately
remote.buildPoolDisabledstring arraymachine[]Members excluded from this requester's automatic build placement: local or exact remote.machines entries. Explicit placement and running builds are unaffected; at least one local or configured, approved remote member must stay enabled
remote.devicePoolDisabledstring arraymachine[]Members excluded from this requester's automatic simulator and emulator placement: local or exact remote.machines entries. Explicit placement and existing sessions are unaffected; at least one local or configured, approved remote member must stay enabled
remote.easMaxDurationMinutesinteger >= 1machine30Maximum minutes of each EAS Simulator session Stim creates, after which EAS stops it and stops billing; stim stop and gc --delete end a session sooner. EAS rejects a value above the account plan's session limit
remote.buildstring: local, auto, remote, remote-only, or a tailnet machine namemachineautoSTIM_REMOTE_BUILDWhere iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: local here; auto here while it has a free concurrency.maxBuilds slot and load under server.maxLoadPerCore, otherwise on a remote.machines entry; remote on a remote.machines entry in order, here only when none takes the build; remote-only the same but never here; a machine name exactly that machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override
server.maxLoadPerCorenumber >= 0.1machine2Load per core (5-minute load average divided by the CPU count) at which this machine counts as saturated: it declines builds other machines send it, reports the limit in its device offers so other machines skip it like a full one, stops preferring itself for automatic placement, and automatic iOS, Android and macOS placement treats it as busy
server.workerRootabsolute pathmachineunsetDirectory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker
server.gradleDaemonIdleMinutesinteger 0..35791machine30Minutes the Gradle daemon of an Android build another machine sent here stays warm after the build; 0 stops it when the build ends
caches.buildCacheabsolute pathmachineunsetSTIM_BUILD_CACHEAbsolute path of the shared build cache
caches.metroCacheabsolute pathmachineunsetSTIM_METRO_CACHEAbsolute parent path of the Metro transform caches
tempDirabsolute pathmachineunsetSTIM_TMPDIRAbsolute directory for large temporary copies

concurrency.maxDevices limits booted or booting owned devices. At the cap, new devices wait in FIFO order across the Stim home for 600 seconds by default. Use --wait <seconds> to change the bound, or --no-wait / --wait 0 to refuse immediately. A workspace's own live device bypasses the queue. Waiting prints holder names and elapsed time, followed by the queue position, head workspace name and $STIM_HOME/device-waits path. Workspaces that share a project name show the directory that tells them apart, as in mobile (qa-b-p2). Timeout names the same head and path. stim stop cancels a queued run and releases its ticket without booting its device. Status adds build.waitingFor, and stats records capacityWaits and positive placement deviceSlotWaitMs. 0 or an unset cap means unlimited devices.

With a cap set, queue reclaim uses the waiting workspace's effective setting: devices.reclaimIdleMinutes defaults to 10. The queue head shuts down the longest-idle eligible owned device in another workspace of this Stim home, one at a time, at most once every 15 seconds, rechecking capacity before another. 0 disables it. The same idle checks as supervisor idle shutdown protect drivers, locks, builds, viewers and recent activity. Reclaim never deletes and excludes the waiting workspace, physical, hosted, remote, parked and other homes' devices. Failed locks or shutdowns are reported once per device and skipped for the rest of the wait. The supervisor's own idle shutdown defaults to 30 minutes. Positive reclaimed counts are included in wait stats.

Any workspace of this Stim home with a device idle for devices.reclaimIdleMinutes is eligible, even with no supervisor and even when its own devices.idleShutdownMinutes is 0. Stim Desktop's simulator view and manual input in Simulator.app do not count as activity. Opt out machine-wide with the command below; the waiter's effective setting decides.

stim settings set devices.reclaimIdleMinutes 0 --scope machine

iosSimulatorApp chooses the macOS app that displays an owned iOS simulator after Stim boots it. It defaults to "stim-desktop" while Stim Desktop is installed and to "xcode" otherwise. "xcode" opens the selected Xcode's Device Hub on Xcode 27 or Simulator on older Xcode. On Xcode 27 (confirmed on 27A266a), quitting Device Hub by default shuts down every booted simulator on the machine, including ones it never opened a window for and ones other workspaces or agents are using; never quit it to free memory or clean up. Set "siniulator" to use an installed Siniulator instead. Set "stim-desktop" to open no simulator window and show a launch card in Stim Desktop. Clicking Show opens the workspace that owns the device. This is a machine-wide preference, not a project setting; Stim still creates, boots, and owns the simulator. An invalid value refuses before boot. Opening the chosen app is best effort, so install Siniulator or Stim Desktop before selecting it.

Override it for one launch with stim ios --simulator-app siniulator, stim ios --simulator-app stim-desktop, or stim ios --simulator-app xcode. The flag also opens an already running owned simulator without rebooting it and leaves the saved preference unchanged. It only applies to local simulators.

androidEmulatorApp chooses how an owned Android emulator that Stim boots on macOS is displayed. It defaults to "stim-desktop" while Stim Desktop is installed and to "emulator" otherwise. "emulator" opens the emulator's own window. "stim-desktop" boots it with -no-window -gpu host and shows a launch card in Stim Desktop. Clicking Show opens the emulator view. Desktop renders frames and sends input through the emulator's gRPC endpoint. It applies only when Stim boots the emulator: an emulator that is already running keeps its current display until it next boots, and physical devices are unaffected. It has no effect on Linux or Windows. An invalid value refuses before boot. There is no per-run flag.

Stim finds Stim Desktop by its bundle id, dev.stim.desktop, in Launch Services. A command Stim Desktop runs skips that lookup: Desktop sets STIM_DESKTOP_APP to its app path for every command it starts. A value you set always wins over that default. stim settings shows such a default as (default: Stim Desktop installed), and --json adds "defaultReason": "Stim Desktop installed" to the entry.

pool.iosParkedMax bounds the simulators worktree remove parks for a later workspace to adopt. Absent means 3; 0 turns parking and adoption off. When STIM_HOME is set, parking is off unless STIM_POOL_IOS_PARKED_MAX is set too. pool.androidParkedMax and STIM_POOL_ANDROID_PARKED_MAX apply the same rules to Android emulators. The hosting Mac also uses these bounds for hosted devices on stop, separately from its local pool and across all clients per platform. Hosted adoption is limited to the same client, clears app data and removes other apps. Parked hosted devices persist across a clean server restart; the oldest excess devices, unusable adoption candidates and a revoked client's devices are retired. See hosted parking and owned devices for adoption cleanup.

remote.buildPoolDisabled and remote.devicePoolDisabled list members excluded from this requester's automatic build and simulator/emulator placement. Both default to []; use local or exact remote.machines entries, including case and port. Names are not trimmed; unmatched entries exclude nothing. The settings preserve pairing, running work and explicit placement, and refuse disabling or removing the last configured, approved member of either pool. A remote that Tailscale reports as running an OS other than macOS never counts for the device pool. Excluding local prevents local fallback. See automatic machine pools.

remote.easMaxDurationMinutes (default 30) is the number of minutes after which EAS stops each EAS Simulator session Stim creates, for --remote eas and remote.easFallback. It bounds the cost of a session that nobody ended, such as one whose workspace was deleted by hand. stim stop and stim gc --delete still end a session sooner, and a later run replaces an expired one. EAS rejects a value above the account plan's session limit. A run reuses a live session without checking how much of the cap is left.

remote.machines names the remote machines this machine may build on and host owned simulator sessions on, by MagicDNS name and optional serve port (default 7443). Name each node and port once. Set it with stim settings set remote.machines '["janics-mac-mini"]', then run stim doctor --fix in an app directory. Build and device-host approvals are separate, and a remote machine is used only for what it approved; a listed machine that never granted one is not used for it, and doctor reports it. Doctor makes a setup ticket and prints the stim-server setup command a person runs on the remote machine to approve this one; one ticket covers builds and hosting. Hosting approval grants no read, control or build access. stim macos --remote <placement> or macos.remote runs a macOS app on an approved machine. iOS placement uses stim ios|android --remote <placement> or ios.remote / android.remote and the same approval. auto places on an approved Mac when this machine is full or busy; remote and remote-only try the approved Macs first. To view or control a hosted macOS app, a person on that Mac approves Screen & System Audio Recording and Device Control and Data Access (Accessibility on macOS 26 and earlier) for Stim Host, the app stim-server service install runs the server under.

Credentials stay private in $STIM_HOME/device-host-machines.json. Doctor reports approval under deviceHosts in JSON. The first connection after approval pins the remote machine's stim-server key, and every later connection must prove that key; a different key reports key-changed and refuses access. Uncertain replies or unreadable credentials preserve the pin. To approve a replaced machine, remove the name, run stim doctor --fix to forget it, then re-add it and run --fix again. Only --fix makes a setup ticket, asks again after a revoked or lapsed request, or forgets names removed from the setting. A pending request reports its ticket's expiresAt. A concurrent approval inspection reports busy instead of replacing a pending ticket.

On a remote Mac that hosts, hosting.agentDriver names the tool it starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client. The default, none, starts nothing. For macOS, agent-device starts its shared daemon only when it can lease a single app (its macos-app lease backend); otherwise agent control reports none with a notice, and no client is handed the Mac's desktop. STIM_AGENT_DEVICE_BIN in stim-server's environment names an agent-device binary to use instead of ~/.local/bin/agent-device. Hosted iOS uses one daemon per session, pinned to its simulator UDID by the daemon policy. Both machines need agent-device 0.21.20 or later. Hosted Android uses one daemon per session pinned to the emulator serial, with agent-device 0.21.22 or later on both machines. doctor counts installed hosted iOS, Android and macOS apps with no driver. Argent and other automation tools work with the devices Stim runs on the same Mac, but are not hosted drivers: Argent changes every booted simulator on its Mac and cannot be confined to one session's device, so a remote client gets agent-device or nothing. See hosted iOS and hosted Android.

For a setup walkthrough covering build and hosting approvals, see Remote machines.

For builds, the same remote.machines list names the machines on your tailnet that may build for this one, optionally with the port of their tailscale serve route (janics-mac-mini:7444; default 7443). stim doctor --fix in any app directory makes a setup ticket for each named machine and prints the stim-server setup command a person runs there to approve this one. The next doctor run pins that machine's stim-server key, and Stim connects only to a server that proves it holds that key. Doctor reports each remote machine's build pairing state under remoteMachines.

The order of remote.machines is the priority order for every workload. A build, simulator, emulator or macOS app goes to the first listed machine that has approval, a free slot and a 5-minute load per core under that machine's own server.maxLoadPerCore; a machine at its caps, over that limit, unreachable or without approval is skipped and the next one in order is tried. Load does not rank the machines that qualify, so the first one fills before the next is used. stim status shows this Mac's load per core against its own server.maxLoadPerCore, as capacity.load in --json. Put the fastest machine first and set the order with stim settings set remote.machines '["fast-mini","slow-mini"]'; stim doctor prints it as placement order, and its JSON gives each machine a priority. Desktop's Remote Macs list shows the order and moves a machine up or down.

remote.build is a machine setting, defaulting to auto. On ios, android and macos, --remote-build <local|auto|remote|remote-only|name> overrides STIM_REMOTE_BUILD, which overrides the setting. A blank environment value is unset, and stim doctor flags an invalid value in the machine config or in STIM_REMOTE_BUILD. Trimmed placement words are case-insensitive; names match configured names case-insensitively with port 7443 when omitted. Reports use the configured entry:

  • local keeps the build on this machine for this run.
  • auto builds here while this machine has capacity and otherwise on the first accepting remote machine in order, within enabled pool members, with local fallback only while local is enabled.
  • remote builds on the first remote machine in order that accepts the build, and here only when none does.
  • remote-only is remote without the local build: it refuses with STIM_OFFLOAD_REFUSED when no remote machine accepts the build, or the build cannot leave this machine.
  • A tailnet name requires a matching entry in remote.machines, already paired and approved. It ignores this machine's load and slots.

remote.buildMode and STIM_REMOTE_BUILD_MODE were removed and are no longer read. stim doctor and stim settings report any left in a config file or the environment. Use remote.build instead: auto stays auto, force becomes remote and off becomes local. stim settings unset remote.buildMode removes the leftover key from every layer that has it, and --scope removes it from one.

A named selection never prompts for pairing, uses another machine or falls back locally. Missing configuration/pairing, denied or pending approval, unreachability or a changed server key, incompatible toolchain/CPU/runtime, low disk, busy workers, sync/build failures, artifact fetch/store failures and a checkout changing during the build fail with STIM_OFFLOAD_REFUSED. The message names the worker and the concrete reason. Run stim doctor --fix to ask for build access if not paired. Check stim settings get remote.machines; a person on the worker approves this machine by running the stim-server setup command doctor printed. To change placement, rerun with --remote-build auto, remote or local.

Device, Release/non-Debug, --remote, Android CAS compiler, build-cache-off and unknown simulator runtime builds refuse a named worker on a cache miss. Invalid values, unlisted names and unpaired names refuse at setup before the cache is consulted, without a build record or failed-run stats. A listed paired name with a cache hit needs no build and contacts no worker. Prebuild and pod install still run on this machine before a named offload; for stim ios the worker's toolchain is checked first. stim status and its JSON record buildMachine (the selected value) and builtOn (here or the worker name, absent when no build ran). Ctrl-C cancels the worker build and starts no local xcodebuild, Gradle or SwiftPM compile.

Copy this prompt to your agent:

Run stim ios --remote-build janics-mac-mini. If it refuses, report the
STIM_OFFLOAD_REFUSED reason and remedy; do not retry with another placement.

A build that leaves this machine goes to the first machine in remote.machines order that accepts it: a free slot (concurrency.maxBuilds), at least 10 GB free, a matching toolchain and a load per core under its own server.maxLoadPerCore. A machine that already holds the exact completed artifact goes ahead of the order. Where stim ios compiles a simulator Debug build, stim android an emulator debug build and stim macos a SwiftPM Debug build depends on the selection:

  • auto (default) builds here while this machine has capacity: a free concurrency.maxBuilds slot (always, with no build limit) and a load per core under server.maxLoadPerCore. Otherwise it builds on the first accepting remote machine in order. A remote machine too old to report its load is used only while every slot here is busy.
  • remote builds on the first accepting remote machine in order, whatever the load here, and builds here when none accepts.
  • remote-only does the same and refuses when none accepts.

Load per core is the 5-minute load average divided by the CPU count. server.maxLoadPerCore (default 2) is also the busy threshold for automatic iOS, Android and macOS placement. It is the load per core at which a machine counts as saturated: each machine applies its own value, a remote machine declines offloaded builds past it, and automatic placement skips it.

Device, Release and --remote eas|proxy builds, Android builds with the Apple Clang CAS compiler cache, and iOS/Android runs with the build cache off, always build here. Hosted iOS Debug builds can use a separate --remote-build, targeting the hosting Mac's architecture and runtime.

In auto, an offloaded iOS/Android build runs prebuild (and pod install for iOS) here, then asks every paired machine what it can build. Stim picks one whose Stim build compatibility version matches and, for iOS and macOS, whose CPU architecture matches the target simulator, with at least 10 GB free, that does not decline, taking the first in remote.machines order. When a machine that offered fails the sync or refuses to start the build, for example because it got busy meanwhile, Stim tries the next one in that order:

  • For iOS, its Xcode and simulator SDK must match, and it needs an iPhone simulator on the target runtime. A missing or different global CocoaPods keeps auto, remote and remote-only placement from choosing the machine for a project that does not pin it, unless the machine already holds the exact artifact. The placement line says why and stim doctor shows the install command. A named --remote-build still sends the build; a pod install that fails there reports "pod install failed" with the fix on that machine. When the project's Gemfile.lock, in ios/ or the project root, pins CocoaPods, both machines run that version through Bundler, so the machine needs Bundler on its stim-server PATH and installs the pinned gems itself on the first build. This machine's side of the comparison runs pod --version in the directory pod install runs in. The comparison uses .ruby-version when installed, otherwise rvm's or rbenv's default Ruby, and only when neither exists the login shell's GEM_HOME, GEM_PATH and Ruby-related PATH entries (unless the caller already sets GEM_HOME), and the same UTF-8 locale defaults as pod install, which runs in the same environment, including the Bundler run.
  • For macOS, its Xcode and macOS SDK must match, unless --remote-build and --remote name the same Mac, which builds and runs the app and then needs only the same Stim build compatibility version (see macOS development). The worker needs network access to fetch SwiftPM dependencies the first time and keeps a dependency cache per client. The returned app is verified and launched locally. Completed caching requires macos.artifactInputs and a locally verified compile; remote output is not published under an unverified client key. See macOS development.
  • For Android, its CPU architecture may differ, so an x86_64 Linux or Windows machine running stim-server can build for an Apple silicon Mac. Its JDK major version must match (the vendor may differ), and its Android SDK must hold the NDK, build-tools and compile platform that the project's React Native version names in gradle/libs.versions.toml, or have its licenses accepted so Gradle downloads what is missing on the first build (unless the project sets android.builder.sdkDownload=false). Gradle and the Android Gradle plugin come from the synced project. It builds with this machine's variant, target ABI, Gradle build cache, PCH and compiler cache choices, so the APK matches the ABI-narrowed cache key.

Stim sends the files git ls-files -co --exclude-standard lists, sending only those the machine lacks. That includes untracked files that are not ignored, such as an unignored .env. The machine builds with Stim's own code, refuses unless its fingerprint equals the one here, and sends back the .app or APK. Stim checks the archive's sha256 and fingerprints the checkout again before it stores and installs the app the usual way; an APK is still compared with the installed one before Stim skips an install. The build output shows placement: <machine> or placement: here (<reason>). With auto, when no machine takes the build, one line gives each machine's reason, such as janics-mac-mini: busy (load at or above 2/core; load 8.2/core, 2 builds) -> building here, and a failure after a machine took it prints offload failed: <reason> -> building here; either way it compiles here. A dropped connection is not a failure by itself: Stim pings the machine every 15 seconds, treats a minute of silence while it waits for the build as a drop, then reconnects to the same machine for up to 3 minutes and takes the running build back. The machine keeps a build running for 5 minutes without a connection before it cancels it. A drop while the app is fetched still builds here. Ctrl-C cancels the build on the machine; a run killed another way, or interrupted while it reconnects, leaves it running until those 5 minutes pass. An offloaded app lands only in this machine's build cache, not in a remote cache provider. A project whose xcodebuild changes its own fingerprinted inputs builds on the machine, fails the fingerprint check there; auto builds here and a named selection refuses, so set remote.build to local for it. The --json payload and lastBuilds carry offloadedTo, or offloadFallback with the reason it built here, stim status shows the machine and its step while a build runs there, and stim stats counts offloaded runs apart from cold runs and keeps where each compiling build ran and why (see stats). On the remote machine, stim-server's machine.details reports the builds it ran for each client machine.

On the remote machine, server.workerRoot (an absolute path; default $STIM_HOME/build-worker) holds each client's checkouts, dependencies, DerivedData, compilation cache, ccache and Gradle home, with a separate Stim home per client and repository and one Gradle home per client. On a Windows machine, the checkouts and Gradle homes themselves live in short directories under C:\sw instead (see Windows worker). Put it on a large volume. It runs one offloaded build at a time and boots or installs nothing. It declines a build while that volume has less than 10 GB free, while its own Stim builds and the offloaded one fill its concurrency.maxBuilds, or while its load per core is at or above its server.maxLoadPerCore. While an offloaded build runs, it holds one of the remote machine's concurrency.maxBuilds slots, so a local stim ios or stim android there waits for the slot and counts the machine as busy. The slot is freed when the build ends, is cancelled, or its process is gone. Delete a client's directory there to reclaim its space.

For Android builds, the remote machine builds on an installed JDK of the same major version as the client (found in the system JVM directories, Homebrew, Android Studio and SDKMAN) even when its default JDK is another one; a machine with none gets the install command as the fix. Start stim-server with JAVA_HOME at such a JDK only to choose it for every build, and with ANDROID_HOME at its Android SDK (default ~/Library/Android/sdk, or ~/Android/Sdk on Linux). After an Android build, the client's Gradle daemon stays warm for server.gradleDaemonIdleMinutes (default 30; 0 stops it when each build ends), so the client's next build skips a JVM start of about 15 seconds. A new value applies from the next daemon. The daemon never holds a build slot. While none of the client's builds runs, stim-server stops it sooner when a build of the client is cancelled, when the client is revoked, and when the remote machine has less than 2 GB of memory available. Deleting the client's directory under the worker root stops it too.

To keep stim-server running on a remote machine across logins, run stim-server service install --serve there. It installs a per-user LaunchAgent that starts at login on macOS, a systemd user service on Linux (with sudo loginctl enable-linger $USER so it starts at boot), or a scheduled task on Windows that starts at logon, and restarts the server if it exits. --serve adds the tailnet-only tailscale serve route (port 7443, or the next free one). --path-prepend <dir> and --env KEY=VALUE pin a PATH entry or a variable, such as a private CocoaPods install, that stim-server's toolchain discovery does not find. The service runs under the Stim Host app, which install builds in ~/Applications with Xcode Command Line Tools. stim-server service status reports the process, its health, the route, Stim Host's permissions and whether its Stim build matches the stim on PATH. Doctor points to this command when a named machine does not answer.

Offload needs Stim releases with the same build compatibility version on both machines. It changes only in a release that changes what a worker builds, how it builds, or the build messages, so two machines on different Stim builds usually offload to each other. A stim-server too old to report its compatibility version needs the same Stim build as this machine. To move the remote machine's service to another build, run one of these there:

stim-server service update --release 1.14.0 # an exact release from npm
stim-server service update --from ./packed # pnpm pack output of a checkout
stim-server service rollback # back to the previous server

--release installs that exact version from the public npm registry, and the update stops unless npm verifies each package's integrity and registry signature. --from installs the .tgz packages in the directory, checks the registry signature of every dependency they bring from npm, and does not look up Stim's own packages on the registry, so an unpublished version installs. Either way the new server is installed beside the running one and must start before the switch. The update waits up to 30 minutes for offloaded builds and hosted sessions to finish, restarts the job, and switches back to the previous server when the new job exits or does not answer within 90 seconds. It never changes pairings, approvals, Stim Host, the pinned --env and --path-prepend values or the serve route.

A client machine this machine approved for builds or device hosting can request the same update over its tailnet connection, so the remote machine keeps up without ssh. It can always ask for an npm release. It can send its own packed build only while server.acceptClientBuilds is true on the remote machine; it is false by default:

stim settings set server.acceptClientBuilds true # on the remote machine

Replacing a loaded service waits up to 45 seconds for its existing listeners to release the port. Uninstall does not wait for the port. If stopping the old job or starting the new one fails, install restores the previous plist and attempts to restart a previously loaded LaunchAgent; if it cannot, it prints the launchctl bootout/bootstrap remedy. A running LaunchAgent does not prove server readiness: check its health in stim-server service status and the reported log when readiness is unavailable. The server listens before it touches the Stim home. A read-only child process reads the Stim home, server and recording directories; until it returns, /health answers 503 with startup.state pending. A read that does not return within 10 seconds, plus up to one second to stop the child, leaves the server listening and degraded: /health and stim-server service status name the reason, and no status followers, watchers, native helpers or recorder start. The server reads again every 30 seconds and serves as soon as a read returns, without changing ownership claims or recordings. It does not restore an inaccessible volume or grant filesystem access; the underlying access problem still needs to be resolved.

gc.worktreeGraceMinutes is how long stim gc --delete waits before it removes a merged or idle linked worktree, counted from the worktree's latest git or Stim activity, the merge of its branch, or when its pull request was merged or closed. Absent means 120; 0 removes a finished worktree at once. STIM_GC_WORKTREE_GRACE_MINUTES overrides it. See removing finished worktrees in bulk.

budget keeps parallel agents from filling the disk or memory. It is on by default. Before stim start, stim ios, or stim android builds or boots anything, Stim checks free disk on the volumes that hold the app and $STIM_HOME, and the estimated memory of active environments:

KeyTypeLayersDefaultEnvironment overrideDescription
budget.minFreeDiskGbnumber >= 0machine20; 0 under STIM_HOMESTIM_BUDGET_MIN_FREE_DISK_GBFree disk, in GB, ios, android and start reclaim toward; 0 reclaims only below the hard floor
budget.hardFloorDiskGbnumber >= 0machine5; 0 under STIM_HOMESTIM_BUDGET_HARD_FLOOR_DISK_GBFree disk, in GB, below which ios, android and start refuse with STIM_LOW_DISK; 0 never refuses
budget.maxCommittedMemoryGbnumber >= 0machineunset; 0 under STIM_HOMESTIM_BUDGET_MAX_COMMITTED_MEMORY_GBEstimated memory, in GB, of active environments before idle ones are reclaimed; unset is 60% of RAM, 0 is off
budget.maxLiveWorkspacesinteger >= 0machineunset; 0 under STIM_HOMESTIM_BUDGET_MAX_LIVE_WORKSPACESActive workspaces before idle ones are reclaimed; unset or 0 means no limit

Stim reclaims in order and stops once it is back under budget: it shuts down idle owned devices in other workspaces, stops idle dev servers in other workspaces, clears the build outputs of workspaces idle for 10 minutes (least recently used first), and trims shared cache entries unused for 14 days. The last two steps run only for disk. Each step prints a budget line on stderr, and --json output lists them under reclaimed. A cache trim that removed nothing is not repeated for 5 minutes while the same caches exist and free disk has not fallen by more than 1 GB; the skipped run prints one budget line saying so. stim gc and automatic maintenance never skip. The current workspace, a device someone is driving or has locked, and a workspace with a build in progress are never reclaimed. A memory or workspace limit never refuses a command. 0 turns a check off. When STIM_HOME is set, the budget is off unless its environment variable is set. stim doctor reports the free disk and committed memory against the budget, and what the next command would reclaim.

stim settings set budget.minFreeDiskGb 40

Without waiting for a budget, devices.idleShutdownMinutes shuts down a workspace's owned simulators and emulators once they have been idle that long, through the workspace's dev server supervisor or, with no supervisor, through automatic maintenance every maintenance.idleCheckMinutes (5). It is on by default at 30 minutes; see idle shutdown.

Try it with an agent:

Run `stim doctor` and tell me how much free disk and committed memory this
machine has against its Stim budget, and what the next `stim ios` would
reclaim first.

caches.buildCache and caches.metroCache move the shared build cache and the Metro transform cache to other absolute paths. caches is a machine-file key only: a caches key in .stim.json is not read and produces the unknown-key warning.

tempDir moves the large temporary copies Stim makes for iOS app preparation, release JavaScript and APK swaps, and the doctor fingerprint checkout. Unset, Stim picks a writable directory on the same volume as the files it copies. The value must be an absolute directory outside Git working trees; a missing directory is created. STIM_TMPDIR overrides it. stim doctor reports cross-volume copy costs and invalid values.

Use a top-level optimizations object in this file to control build optimizations on this machine without changing project files.

Device recordings​

stim-server records the screens of owned simulators, emulators and the Stim-owned Chrome page, so the phone app can scrub back through what an agent did while you were not watching. It records a device only while an agent or automation tool drives it, or while the phone app watches it. It keeps the last 15 minutes of footage per device. Recordings stay on your machine, under $STIM_HOME/workspaces/<id>/recordings/, and stim-server serves them only to paired devices.

Turn recording off for the whole machine, a repository, or one workspace:

stim settings set recording.enabled false --scope machine

recording.enabled is true by default. The workspace layer wins over the repo layer, which wins over the machine layer. STIM_RECORDING=0 overrides every layer; stim-server reads it when it starts. Turning recording off deletes the existing recordings of every workspace it turns off, and stim-server stops recording them within seconds. See Inspect and clean caches for how recordings are otherwise cleaned up.

Hidden workspaces​

Mark a workspace hidden so the Stim apps can list it under Hidden. Run this from the workspace:

stim settings set workspace.hidden true

workspace.hidden is false by default and lives only in the workspace layer, so it takes no --scope. stim status --json reports hidden: true on that environment. The next start, ios, android, web, macos, reload or worktree warm in the workspace clears it, so a workspace in use is never hidden. stim settings unset workspace.hidden shows it again, and worktree remove drops it with the workspace.

Environment variables​

These variables override a setting and win over every layer:

VariableOverridesDescription
STIM_ANDROID_CAS_TOOLCHAINoptimizations.android.casToolchainAbsolute path to the Android CAS toolchain manifest
STIM_ARCHIVE_AGENT_ACTIONS_MAX_AGE_DAYSarchive.agentActions.maxAgeDaysDays to keep archived agent-device sessions; 0 keeps none
STIM_ARCHIVE_ENABLEDarchive.enabledKeep workspace history on removal, and archive a dirty finished worktree's changes before gc removes it; false keeps such worktrees, and is the default under STIM_HOME unless explicitly enabled
STIM_ARCHIVE_LOGS_MAX_AGE_DAYSarchive.logs.maxAgeDaysDays to keep archived logs; 0 keeps none
STIM_ARCHIVE_LOGS_MAX_MB_PER_WORKSPACEarchive.logs.maxMbPerWorkspaceArchived logs per workspace in binary MB; 0 keeps none
STIM_ARCHIVE_MAX_AGE_DAYSarchive.maxAgeDaysDays to keep archived workspace records and worktree change archives; 0 keeps nothing
STIM_ARCHIVE_MAX_COUNTarchive.maxCountArchived workspace count; oldest records go first, 0 keeps nothing
STIM_ARCHIVE_MAX_TOTAL_GBarchive.maxTotalGbTotal archived bytes in binary GB, and separately the worktree change archives; 0 keeps no artifacts
STIM_ARCHIVE_RECORDINGS_MAX_AGE_DAYSarchive.recordings.maxAgeDaysDays to keep archived recordings; 0 keeps none
STIM_ARCHIVE_RECORDINGS_MAX_TOTAL_GBarchive.recordings.maxTotalGbArchived recordings in binary GB; 0 keeps none
STIM_ASC_ISSUER_IDios.appStoreConnectIssuerIdIssuer ID of ios.appStoreConnectKeyPath
STIM_ASC_KEY_IDios.appStoreConnectKeyIdKey ID of ios.appStoreConnectKeyPath
STIM_ASC_KEY_PATHios.appStoreConnectKeyPathApp Store Connect API key (.p8) that stim device provision ios authenticates with
STIM_BUDGET_HARD_FLOOR_DISK_GBbudget.hardFloorDiskGbFree disk, in GB, below which ios, android and start refuse with STIM_LOW_DISK; 0 never refuses
STIM_BUDGET_MAX_COMMITTED_MEMORY_GBbudget.maxCommittedMemoryGbEstimated memory, in GB, of active environments before idle ones are reclaimed; unset is 60% of RAM, 0 is off
STIM_BUDGET_MAX_LIVE_WORKSPACESbudget.maxLiveWorkspacesActive workspaces before idle ones are reclaimed; unset or 0 means no limit
STIM_BUDGET_MIN_FREE_DISK_GBbudget.minFreeDiskGbFree disk, in GB, ios, android and start reclaim toward; 0 reclaims only below the hard floor
STIM_BUILD_CACHEcaches.buildCacheAbsolute path of the shared build cache
STIM_CACHES_BUILD_CACHE_MAX_GBcaches.buildCacheMaxGbShared native build-cache cap in GiB; 0 means no cap
STIM_CACHES_CCACHE_MAX_GBcaches.ccacheMaxGbccache size limit in GiB, passed to ccache as CCACHE_MAXSIZE; ccache evicts by itself
STIM_CACHES_METRO_CACHE_MAX_GBcaches.metroCacheMaxGbCombined Metro transform-cache cap in GiB; 0 means no cap
STIM_CACHES_SWIFT_COMPILATION_CACHE_MAX_GBcaches.swiftCompilationCacheMaxGbSwift compilation-cache cap in GiB; planned emptying is whole; 0 means no cap
STIM_DEBUGdebug.logsDebug-level logging. While on, the CLI records its commands, child processes and requests to other machines, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets
STIM_GC_WORKTREE_GRACE_MINUTESgc.worktreeGraceMinutesMinutes after a linked worktree last changed, or its branch or pull request finished, before gc removes it; 0 removes it at once
STIM_MAINTENANCEmaintenance.modeAutomatic resource maintenance. on removes what gc would remove under the caps and floors below and logs every action; report measures, plans and logs what it would do and never deletes anything; off disables it. Off under STIM_HOME or CI unless STIM_MAINTENANCE is set
STIM_MAINTENANCE_CAP_TARGET_PERCENTmaintenance.capTargetPercentPlan toward this percentage of each size cap
STIM_MAINTENANCE_CRITICAL_FREE_DISK_GBmaintenance.criticalFreeDiskGbFree disk, in GB, below which automatic maintenance enters disk pressure level 3, also at or below budget.hardFloorDiskGb; 0 leaves only the hard floor
STIM_MAINTENANCE_DEVICE_IDLE_DAYSmaintenance.deviceIdleDaysDays a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off
STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYSmaintenance.gradleVersionIdleDaysAt disk pressure level 3, stop idle Gradle and Kotlin daemons, then delete the Gradle version caches and wrapper distributions no registered project pins once their directories are unmodified this many days; 0 turns this off
STIM_MAINTENANCE_IDLE_CHECK_MINUTESmaintenance.idleCheckMinutesInterval in minutes of the check that shuts down owned simulators and emulators idle for devices.idleShutdownMinutes, also with no dev server running
STIM_MAINTENANCE_LOG_CHECKSmaintenance.logChecksWrite debug check records and routine skips
STIM_MAINTENANCE_LOG_MAX_MBmaintenance.logMaxMbMachine maintenance and gc log rotation size in MiB
STIM_MAINTENANCE_LOG_RETENTION_DAYSmaintenance.logRetentionDaysRetention of the rotated machine maintenance and gc logs in days
STIM_MAINTENANCE_MAX_LOAD_PER_COREmaintenance.maxLoadPerCoreDefer size scans above this one-minute load per CPU
STIM_MAINTENANCE_MEMORY_PRESSURE_LEVELmaintenance.memoryPressureLevelLowest macOS memory pressure level considered; off disables memory pressure reporting
STIM_MAINTENANCE_MEMORY_WARNING_MINUTESmaintenance.memoryWarningMinutesConsecutive warning minutes before memory counts as pressure
STIM_MAINTENANCE_MIN_AVAILABLE_MEMORY_GBmaintenance.minAvailableMemoryGbAvailable-memory threshold in GiB; unset is 10% of RAM
STIM_MAINTENANCE_OLDER_THAN_DAYSmaintenance.olderThanDaysDays unused before the age sweep removes it: workspace build outputs, shared cache entries, parked simulators and emulators, and, with maintenance.removeIdleWorktrees, clean pushed linked worktrees; 0 turns those age removals off
STIM_MAINTENANCE_PRESSURE_CHECK_MINUTESmaintenance.pressureCheckMinutesDisk and memory check interval in minutes
STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTESmaintenance.pressureProtectMinutesProtection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3, except for workspace build outputs and build directories, which level 2 keeps for 15 minutes after the last ios, android or macos run
STIM_MAINTENANCE_PROTECT_RECENT_HOURSmaintenance.protectRecentHoursCache entries used within this many hours are never evicted
STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREESmaintenance.removeFinishedWorktreesRemove linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, archiving a dirty one's uncommitted files first, and clean, pushed ones with no commits of their own once idle
STIM_MAINTENANCE_REMOVE_IDLE_WORKTREESmaintenance.removeIdleWorktreesIn the age sweep, also remove clean, pushed linked worktrees whose branch is not merged once no Stim command has used them for maintenance.olderThanDays, with their git-ignored files; needs maintenance.removeFinishedWorktrees
STIM_MAINTENANCE_SIZE_CHECK_MINUTESmaintenance.sizeCheckMinutesDirectory size check interval in minutes
STIM_MAINTENANCE_SWEEP_HOURSmaintenance.sweepHoursAge sweep interval in hours; 0 turns the age sweep off
STIM_MAINTENANCE_URGENT_FREE_DISK_GBmaintenance.urgentFreeDiskGbFree disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off
STIM_MAINTENANCE_WORKSPACE_OUTPUTS_MAX_GBmaintenance.workspaceOutputsMaxGbWorkspace build-output cap in GiB; 0 means no cap
STIM_MAINTENANCE_WORKTREE_CHECK_MINUTESmaintenance.worktreeCheckMinutesFinished-worktree check interval in minutes
STIM_MAX_BUILDSconcurrency.maxBuildsConcurrent native builds; 0 means no limit
STIM_MAX_DEVICESconcurrency.maxDevicesBooted owned devices; 0 means no limit
STIM_METRO_CACHEcaches.metroCacheAbsolute parent path of the Metro transform caches
STIM_METRO_PORTmetro.portThis workspace's Metro port, reserved instead of one Stim picks
STIM_POOL_ANDROID_PARKED_MAXpool.androidParkedMaxParked emulators kept for adoption; 0 turns parking off
STIM_POOL_IOS_PARKED_MAXpool.iosParkedMaxParked simulators kept for adoption; 0 turns parking off
STIM_RECORDINGrecording.enabledLet stim-server record device screens for replay; false stops it and deletes the recordings
STIM_REMOTE_BUILDremote.buildWhere iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: local here; auto here while it has a free concurrency.maxBuilds slot and load under server.maxLoadPerCore, otherwise on a remote.machines entry; remote on a remote.machines entry in order, here only when none takes the build; remote-only the same but never here; a machine name exactly that machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override
STIM_TMPDIRtempDirAbsolute directory for large temporary copies

A boolean override such as STIM_RECORDING takes true, false, 1 or 0. STIM_ANDROID_CAS_TOOLCHAIN is described under experimental Android CAS.

Stim also reads these variables, which have no setting:

VariablePurpose
STIM_HOMERuntime state root. Default: ~/.stim
STIM_METRO_PUBLIC_URLPublic Metro URL for remote use
STIM_NO_UPDATE_CHECKSet to disable the daily check for a newer Stim release in stim guide
STIM_RUN_IDId for this stim invocation (letters, digits, ., _, -, at most 64); stamped as runId on every log record it writes and sent to stim-server. Default: generated per run
STIM_TRACEPath of a Chrome trace file the command writes its timeline to; see Timing traces. Default: unset, nothing written

Processes a command starts, such as the Metro supervisor and the collectors, inherit its STIM_RUN_ID, so their records carry the id of the command that started them.

STIM_HOME, STIM_BUILD_CACHE, and STIM_METRO_CACHE must be absolute paths. A relative value would resolve against each process's working directory, so the CLI, Metro, and the Expo build-cache provider would use different stores. Every stim command refuses a relative value with STIM_RELATIVE_PATH. Metro and the Expo build-cache provider cannot refuse without breaking the bundler, so they print a warning and ignore the value, falling back to the config file or the default.

Proxy remote devices also use AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN. Those variables belong to the optional proxy service, not to Stim.

Archive retention​

archive.enabled accepts machine, workspace, repo, and committed layers. The other archive settings are machine settings. Environment overrides win. With STIM_HOME, archiving defaults off unless STIM_ARCHIVE_ENABLED is set. Sizes use binary GB and MB; 0 means keep none.

KeyTypeLayersDefaultEnvironment overrideDescription
archive.enabledbooleanmachine, workspace, repo, committedtrue; false under STIM_HOMESTIM_ARCHIVE_ENABLEDKeep workspace history on removal, and archive a dirty finished worktree's changes before gc removes it; false keeps such worktrees, and is the default under STIM_HOME unless explicitly enabled
archive.maxAgeDaysinteger >= 0machine30STIM_ARCHIVE_MAX_AGE_DAYSDays to keep archived workspace records and worktree change archives; 0 keeps nothing
archive.maxCountinteger >= 0machine200STIM_ARCHIVE_MAX_COUNTArchived workspace count; oldest records go first, 0 keeps nothing
archive.maxTotalGbnumber >= 0machine5STIM_ARCHIVE_MAX_TOTAL_GBTotal archived bytes in binary GB, and separately the worktree change archives; 0 keeps no artifacts
archive.logs.maxAgeDaysinteger >= 0machine14STIM_ARCHIVE_LOGS_MAX_AGE_DAYSDays to keep archived logs; 0 keeps none
archive.logs.maxMbPerWorkspacenumber >= 0machine100STIM_ARCHIVE_LOGS_MAX_MB_PER_WORKSPACEArchived logs per workspace in binary MB; 0 keeps none
archive.recordings.maxAgeDaysinteger >= 0machine3STIM_ARCHIVE_RECORDINGS_MAX_AGE_DAYSDays to keep archived recordings; 0 keeps none
archive.recordings.maxTotalGbnumber >= 0machine2STIM_ARCHIVE_RECORDINGS_MAX_TOTAL_GBArchived recordings in binary GB; 0 keeps none
archive.agentActions.maxAgeDaysinteger >= 0machine7STIM_ARCHIVE_AGENT_ACTIONS_MAX_AGE_DAYSDays to keep archived agent-device sessions; 0 keeps none

Retention runs on every worktree removal, archived or not, once an archive exists. It deletes records past their age or count limit first, then expires recordings, agent actions and logs by age from removal; trims logs by oldest rotated generation, then build logs, then oldest other files; and removes the largest kind of the oldest archive while total caps are exceeded. Artifact expiry keeps records. Turning recording.enabled off at machine scope deletes archived recordings on the next retention pass.

The same archive.enabled, archive.maxAgeDays and archive.maxTotalGb govern the uncommitted changes gc archives before it removes a dirty finished worktree (remove finished worktrees in bulk). Those archives have their own archive.maxTotalGb budget, so all archives together can take twice that, and archive.enabled false keeps such worktrees instead.

stim settings set archive.enabled false --scope repo

See archived workspaces for privacy and deletion.

Automatic maintenance​

maintenance.mode is on by default: a pass removes what stim gc --delete would remove under the caps and floors below and logs every action. report measures, plans and logs and deletes nothing, and off disables passes. STIM_HOME and CI make the mode off unless STIM_MAINTENANCE is set. All of these settings have machine scope except maintenance.keep, a project setting that pins a workspace: automatic passes never clear its build outputs, delete its owned devices or remove its worktree. GB and MB below mean GiB and MiB.

KeyTypeLayersDefaultEnvironment overrideDescription
maintenance.modeoff, report, onmachineon; off under STIM_HOME; off under CISTIM_MAINTENANCEAutomatic resource maintenance. on removes what gc would remove under the caps and floors below and logs every action; report measures, plans and logs what it would do and never deletes anything; off disables it. Off under STIM_HOME or CI unless STIM_MAINTENANCE is set
maintenance.pressureCheckMinutesinteger >= 1machine1STIM_MAINTENANCE_PRESSURE_CHECK_MINUTESDisk and memory check interval in minutes
maintenance.sizeCheckMinutesinteger >= 1machine60STIM_MAINTENANCE_SIZE_CHECK_MINUTESDirectory size check interval in minutes
maintenance.maxLoadPerCorenumber > 0machine4STIM_MAINTENANCE_MAX_LOAD_PER_COREDefer size scans above this one-minute load per CPU
maintenance.logMaxMbnumber > 0machine1STIM_MAINTENANCE_LOG_MAX_MBMachine maintenance and gc log rotation size in MiB
maintenance.logRetentionDaysinteger >= 1machine30STIM_MAINTENANCE_LOG_RETENTION_DAYSRetention of the rotated machine maintenance and gc logs in days
maintenance.logChecksbooleanmachinefalseSTIM_MAINTENANCE_LOG_CHECKSWrite debug check records and routine skips
maintenance.memoryPressureLevelwarning, critical, offmachinewarningSTIM_MAINTENANCE_MEMORY_PRESSURE_LEVELLowest macOS memory pressure level considered; off disables memory pressure reporting
maintenance.memoryWarningMinutesinteger >= 0machine10STIM_MAINTENANCE_MEMORY_WARNING_MINUTESConsecutive warning minutes before memory counts as pressure
maintenance.minAvailableMemoryGbnumber >= 0machineunsetSTIM_MAINTENANCE_MIN_AVAILABLE_MEMORY_GBAvailable-memory threshold in GiB; unset is 10% of RAM
maintenance.capTargetPercentinteger 10..100machine80STIM_MAINTENANCE_CAP_TARGET_PERCENTPlan toward this percentage of each size cap
maintenance.worktreeCheckMinutesinteger >= 1machine15STIM_MAINTENANCE_WORKTREE_CHECK_MINUTESFinished-worktree check interval in minutes
maintenance.idleCheckMinutesinteger >= 1machine5STIM_MAINTENANCE_IDLE_CHECK_MINUTESInterval in minutes of the check that shuts down owned simulators and emulators idle for devices.idleShutdownMinutes, also with no dev server running
maintenance.sweepHoursinteger >= 0machine24STIM_MAINTENANCE_SWEEP_HOURSAge sweep interval in hours; 0 turns the age sweep off
maintenance.olderThanDaysinteger >= 0machine7STIM_MAINTENANCE_OLDER_THAN_DAYSDays unused before the age sweep removes it: workspace build outputs, shared cache entries, parked simulators and emulators, and, with maintenance.removeIdleWorktrees, clean pushed linked worktrees; 0 turns those age removals off
maintenance.deviceIdleDaysinteger >= 0machine3STIM_MAINTENANCE_DEVICE_IDLE_DAYSDays a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off
maintenance.protectRecentHoursinteger >= 0machine2STIM_MAINTENANCE_PROTECT_RECENT_HOURSCache entries used within this many hours are never evicted
maintenance.urgentFreeDiskGbnumber >= 0machine12STIM_MAINTENANCE_URGENT_FREE_DISK_GBFree disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off
maintenance.criticalFreeDiskGbnumber >= 0machine6STIM_MAINTENANCE_CRITICAL_FREE_DISK_GBFree disk, in GB, below which automatic maintenance enters disk pressure level 3, also at or below budget.hardFloorDiskGb; 0 leaves only the hard floor
maintenance.pressureProtectMinutesinteger >= 0machine10STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTESProtection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3, except for workspace build outputs and build directories, which level 2 keeps for 15 minutes after the last ios, android or macos run
maintenance.gradleVersionIdleDaysinteger >= 0machine7STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYSAt disk pressure level 3, stop idle Gradle and Kotlin daemons, then delete the Gradle version caches and wrapper distributions no registered project pins once their directories are unmodified this many days; 0 turns this off
maintenance.removeFinishedWorktreesbooleanmachinetrueSTIM_MAINTENANCE_REMOVE_FINISHED_WORKTREESRemove linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, archiving a dirty one's uncommitted files first, and clean, pushed ones with no commits of their own once idle
maintenance.removeIdleWorktreesbooleanmachinefalseSTIM_MAINTENANCE_REMOVE_IDLE_WORKTREESIn the age sweep, also remove clean, pushed linked worktrees whose branch is not merged once no Stim command has used them for maintenance.olderThanDays, with their git-ignored files; needs maintenance.removeFinishedWorktrees
maintenance.keepbooleanworkspace, repo, committedfalsePin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree
maintenance.workspaceOutputsMaxGbnumber >= 0machine20STIM_MAINTENANCE_WORKSPACE_OUTPUTS_MAX_GBWorkspace build-output cap in GiB; 0 means no cap
caches.buildCacheMaxGbnumber >= 0machine10STIM_CACHES_BUILD_CACHE_MAX_GBShared native build-cache cap in GiB; 0 means no cap
caches.metroCacheMaxGbnumber >= 0machine5STIM_CACHES_METRO_CACHE_MAX_GBCombined Metro transform-cache cap in GiB; 0 means no cap
caches.swiftCompilationCacheMaxGbnumber >= 0machine15STIM_CACHES_SWIFT_COMPILATION_CACHE_MAX_GBSwift compilation-cache cap in GiB; planned emptying is whole; 0 means no cap
caches.ccacheMaxGbnumber > 0machine5STIM_CACHES_CCACHE_MAX_GBccache size limit in GiB, passed to ccache as CCACHE_MAXSIZE; ccache evicts by itself

Size scans defer above maintenance.maxLoadPerCore; pressure checks continue. On macOS the memory signal is the sysctl pressure level, with no signal when sysctl fails. os.freemem() and maintenance.minAvailableMemoryGb apply only on other platforms. Caps act toward maintenance.capTargetPercent of the limit. The Swift compilation cache can only be emptied whole, so a pass empties it only below budget.hardFloorDiskGb or at disk pressure level 3, and never while an iOS build holds its claim on it or that claim is unresolved. maintenance.urgentFreeDiskGb, maintenance.criticalFreeDiskGb and maintenance.pressureProtectMinutes set the disk pressure levels. caches.ccacheMaxGb is passed to ccache, which evicts by itself. maintenance.sweepHours (24) sets how often the age sweep runs. maintenance.olderThanDays (7; 0 turns these off) is how long something stays unused before that sweep removes it: workspace build outputs, shared cache entries, parked simulators and emulators, and clean, pushed linked worktrees (only with maintenance.removeIdleWorktrees, off by default); maintenance.deviceIdleDays sets how long a workspace stays idle before that sweep deletes its owned simulators and emulators (0 turns it off); maintenance.gradleVersionIdleDays (7) sets how long a Gradle version no project pins stays unmodified before level 3 stops the idle Gradle and Kotlin daemons that keep it and deletes it (0 turns that off); maintenance.removeFinishedWorktrees controls the worktree check. Memory pressure is recorded; below disk pressure level 3 a pass never stops a dev server (removing a finished worktree tears down that workspace's own, as gc --delete does). Debug check logs are off by default. See automatic maintenance for reports.

Debug logs​

debug.logs is a machine setting, off by default, with the environment override STIM_DEBUG (1 or 0 for one command). While it is on, the CLI writes debug records to $STIM_HOME/logs/debug/cli.ndjson: each run's start and end, every child process with its program name, duration and exit status, and every request to another machine with its duration and result code. stim-server writes server.ndjson there and logs every request with its timings to its service log. Records never carry arguments, environment values, tokens or tickets, nothing is sent anywhere, and the file rotates at about 8 MiB with one previous generation kept.

stim settings set debug.logs true --scope machine

Try this with your agent:

Turn on debug.logs, rerun the command that stalled, then read
~/.stim/logs/debug/cli.ndjson and tell me which child process or remote request
took the time.

Timing traces​

STIM_TRACE=<path> writes a timeline of one command to <path> as Chrome trace events. Open the file in Perfetto or chrome://tracing to see where worktree warm, start, ios, android or stop spent its time: one span per child process, one per build phase (the phases stats.json and the build ledger record), and spans for the fingerprint and the wait for Metro. Unset, Stim writes nothing. There is no setting for it, and the trace goes only to that file, never to stdout or stderr, so --json output is unchanged.

STIM_TRACE=/tmp/ios.json stim ios

The command you start truncates the file; the processes it starts, such as the Metro supervisor, append to it, so use a new path for each command. The file is a JSON array with one event per line and no closing bracket, which Perfetto and chrome://tracing accept. Argument values of flags that look like secrets, credentials in URLs and common token shapes are replaced with [redacted], as are the credentials of curl -u and security import -P. Other programs' short flags such as -p and -u are kept, because they usually mean a path, a port or a user. A trace still holds full paths and arguments, so treat it like a log.

Try this with your agent:

Run stim ios with STIM_TRACE=/tmp/ios.json, then read the file and tell me which
build phase and which child process took the most time.