An IOS Simulator Skill for ClaudeCode. Use it to optimise Claude's ability to build, run and interact with your apps. I mainly use it as an xcodebuild wrapper to save context.
SaferSkills independently audited iOS Simulator Skill (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build, test, and automate iOS applications using accessibility-driven navigation and structured data instead of pixel coordinates.
# 1. Check environment
bash scripts/sim_health_check.sh
# 2. Launch app
python scripts/app_launcher.py --launch com.example.app
# 3. Map screen to see elements
python scripts/screen_mapper.py
# 4. Tap button
python scripts/navigator.py --find-text "Login" --tap
# 5. Enter text
python scripts/navigator.py --find-type TextField --enter-text "[email protected]"All scripts support --help for detailed options and --json for machine-readable output.
Always prefer the accessibility tree over screenshots for navigation. The accessibility tree gives you element types, labels, frames, and tap targets — structured data that's cheaper and more reliable than image analysis.
Use this priority:
screen_mapper.py → structured element list (5-7 lines, ~10 tokens)navigator.py --find-text/--find-type/--find-id → semantic interactionScreenshots cost 1,600–6,300 tokens depending on size. The accessibility tree costs 10–50 tokens in default mode.
--project, --scheme, --clean, --test, --verbose, --json--app, --severity, --follow, --duration, --output, --jsonxcrun simctl ui--bundle-id--theme, --text-size, --locale, --region, --reset, --bundle-id, --udid, --json, --verbose--lat/--lng or pick a city with --city--gpx <scenario>--waypoints and --speed--clear; list available scenarios with --list-scenarios--lat, --lng, --city, --gpx, --waypoints, --speed, --clear, --list-scenarios, --udid, --json, --verbose--verbose, --hints, --json--find-text, --find-type, --find-id, --tap, --enter-text, --json--swipe, --scroll, --pinch, --long-press, --refresh, --json--type, --key, --button, --slow, --clear, --dismiss, --json--args) and environment variables (--env KEY=VALUE, injected as SIMCTL_CHILD_*) to the app on launch/restart--launch, --terminate, --restart, --install, --uninstall, --open-url, --list, --state, --args, --env, --wait-for-debugger--verbose, --output, --json--threshold, --output, --details, --json--test-name, --output, --verbose, --json--app-bundle-id, --output, --log-lines, --json--raw ModelName)--project-path, --core-data-only, --swiftdata-only, --show-versions, --raw, --verbose, --json--ls--cat (large files cached)--userdefaults.sqlite / .sqlite-wal / .sqlite-shm stores via --core-data-path--export--ls, --cat, --userdefaults, --core-data-path, --export, --udid, --json, --verbose--start → returns a session ID; detached worker normalises + thresholds events on the fly--stop SESSION_ID → emits ~80–120 token L1 summary (header + top-N clusters + drill hint)--get-details SESSION_ID [--cluster N | --raw] → L2 full clusters or L3 per-event detail--list-sessions / --clear-sessions [--older-than 24h] / --diff A B (cross-session regression report)common/hang_pipeline.py)--budget-tokens N picks the densest level (L0/L1/L2) that fits; --terse forces L0--auto-sample captures a main-thread stack on first event per cluster (soft dependency: main_thread_sampler.py #62; graceful no-op if absent)raw.ndjson--start --raw-capture [--max-size-mb 10] [--no-gzip] — spawns log stream --style ndjson--max-size-mb, default 10) — worker stops cleanly on cap; extras.truncated=true--stop gzips raw.ndjson → raw.ndjson.gz (~15–19× compression; --no-gzip opts out)--get-details SESSION_ID on a raw session prints the path with a zcat | jq ... hintstream_died event then a bounded restart with 2s backoff. After IOS_SIM_HANG_MAX_RESTARTS (default 3) the session is marked crashed, never left in stale running state. --list-sessions shows capture=Xs and restarts=N.IOS_SIM_HANG_SESSION_TTL_HOURS, default 24h) + aggregate cap (IOS_SIM_HANG_TOTAL_CAP_MB, default 100 MB, oldest-first eviction) both run on every --start.--watch [--duration N] (live stream) and --since 5m (historical)--bundle-id (post-parse — hang capture stays simulator-global so RunningBoard/SpringBoard events are kept), --predicate (also via IOS_SIM_HANG_PREDICATE)--json; session storage at ~/.ios-simulator-skill/sessions/<id>/{meta.json,events.jsonl,summary.json,raw.ndjson.gz}Quick start (summarised mode):
SID=$(python scripts/hang_watcher.py --start --min-hang-ms 200)
# ... interact with the simulator (open sheets, scroll, navigate) ...
python scripts/hang_watcher.py --stop $SID # token-tight L1 summary
python scripts/hang_watcher.py --get-details $SID --cluster 1 # drill into cluster 1
python scripts/hang_watcher.py --diff $SID_BASELINE $SID # cross-session regressionQuick start (raw capture + `jq` exploration):
SID=$(python scripts/hang_watcher.py --start --raw-capture --max-size-mb 5)
# ... interact with the simulator ...
python scripts/hang_watcher.py --stop $SID
# → "Session ...: raw mode, 737 lines, 0.96 MB → 0.05 MB gzipped"
# Top processes by event count:
zcat ~/.ios-simulator-skill/sessions/$SID/raw.ndjson.gz \
| jq -s 'group_by(.processImagePath) | map({proc: (.[0].processImagePath | split("/") | last), n: length}) | sort_by(-.n) | .[:5]'
# All RunningBoard assertion invalidations:
zcat .../raw.ndjson.gz | jq -c 'select(.subsystem == "com.apple.runningboard" and (.eventMessage | startswith("Invalidating")))'
# Hangs per minute:
zcat .../raw.ndjson.gz | jq -r '.timestamp[:16]' | sort | uniq -cneeds_review/new keys per locale in .xcstrings catalogsString(localized:) / NSLocalizedString) via --source%d, %@, %s, %lld) across locales.strings and .stringsdict support via plistlib--strict exits 2 on any finding--catalog, --source, --locale, --strict, --json, --verbose--copy, --test-name, --expected, --json--preset, --time, --data-network, --battery-level, --clear, --json--bundle-id, --title, --body, --badge, --payload, --json--bundle-id, --grant, --revoke, --reset, --list, --json--suggestsimctl list (57k → 2k tokens)--get-details, --suggest, --device-type, --jsonconfig.json), latest iOS, common test models, and boot status--list--boot--suggest, --list, --boot, --json--udid, --name, --wait-ready, --timeout, --all, --type, --json--udid, --name, --verify, --timeout, --all, --type, --json--device, --runtime, --name, --list-devices, --list-runtimes, --json--udid, --name, --yes, --all, --type, --old, --json--udid, --name, --verify, --timeout, --all, --type, --booted, --jsonAuto-UDID Detection: Most scripts auto-detect the booted simulator if --udid is not provided.
Device Name Resolution: Use device names (e.g., "iPhone 16 Pro") instead of UDIDs - scripts resolve automatically.
Batch Operations: Many scripts support --all for all simulators or --type iPhone for device type filtering.
Output Formats: Default is concise human-readable output. Use --json for machine-readable output in CI/CD.
Help: All scripts support --help for detailed options and examples.
Screenshot Sizing: Screenshots are resized to save tokens. Presets: full (3-4 tiles, ~5K tokens), half (1 tile, ~1.6K tokens, default), quarter (1 tile, ~800 tokens, less detail). Use quarter for quick visual checks, half for readable UI, full only when pixel-level detail matters. Scripts that capture screenshots (app_state_capture.py, test_recorder.py) default to half.
bash scripts/sim_health_check.shpython scripts/app_launcher.py --launch com.example.apppython scripts/screen_mapper.pypython scripts/navigator.py --find-text "Button" --tappython scripts/accessibility_audit.pypython scripts/app_state_capture.py --app-bundle-id com.example.appMost operational limits can be tuned via environment variables. Defaults work for typical local development; raise them for slow CI runners, large monorepo builds, or accessibility audits on complex screens.
| Variable | Default | Controls |
|---|---|---|
IOS_SIM_A11Y_LABEL_MAX | 80 | Max chars of AXLabel retained in accessibility audit output |
IOS_SIM_A11Y_TOP_ISSUES | 10 | Top accessibility issues surfaced per audit |
IOS_SIM_APPS_PREVIEW | 30 | App entries listed by app_launcher.py before truncation |
IOS_SIM_BOOT_SUBPROCESS_TIMEOUT | 60 | Timeout for the simctl boot subprocess itself (seconds) |
IOS_SIM_BOOT_TIMEOUT | 300 | Wait-for-ready timeout after boot (seconds) |
IOS_SIM_BUILD_JSON_CAP | 50 | Max build errors / failed tests in JSON output |
IOS_SIM_BUILD_LOG_PREVIEW | 4000 | Chars of build log preview in default output |
IOS_SIM_BUILD_TIMEOUT | 1800 | Max seconds for an xcodebuild build invocation before kill |
IOS_SIM_INTROSPECT_TIMEOUT | 60 | Timeout for xcodebuild -list and simctl list lookups (seconds) |
IOS_SIM_TEST_TIMEOUT | 2700 | Max seconds for an xcodebuild test invocation before kill |
IOS_SIM_BUILD_SUMMARY_CAP | 15 | Errors/failures in default build summary |
IOS_SIM_BUILD_VERBOSE_CAP | 100 | Errors/warnings in verbose build output |
IOS_SIM_CACHE_MAX_ENTRIES | 500 | Max entries in progressive disclosure cache (LRU eviction) |
IOS_SIM_CACHE_TTL_HOURS | 1 | Cache entry expiration |
IOS_SIM_ERASE_TIMEOUT | 90 | Wait-for-erase timeout (seconds) |
IOS_SIM_HANG_PREDICATE | _(default)_ | Override the os_log predicate used by hang_watcher.py (default catches RunningBoard kills + "Hang detected" + main-thread hangs). Hang events originate from system daemons (RunningBoard, SpringBoard) so the predicate stays simulator-global — --bundle-id is applied post-parse, not ANDed in. |
IOS_SIM_HANG_MIN_MS | 250 | HangBuster threshold — events below this duration never reach disk (smaller = more sensitive, larger summaries) |
IOS_SIM_HANG_SESSION_TTL_HOURS | 24 | HangBuster session prune age; pruning runs on every --start |
IOS_SIM_HANG_DEFAULT_TOP_N | 3 | Default top-N clusters in --stop L1 output |
IOS_SIM_HANG_BUDGET_TOKENS | _(unset)_ | Default token budget for --stop (picks L0/L1/L2 to fit) |
IOS_SIM_HANG_MAX_RESTARTS | 3 | HangBuster worker: max log stream respawn attempts on EOF/subprocess death before the session is marked crashed |
IOS_SIM_HANG_TOTAL_CAP_MB | 100 | HangBuster aggregate disk cap. When total session-state exceeds this on --start, oldest sessions are dropped first. Set to 0 to disable. |
IOS_SIM_LOG_JSON_CAP | 100 | Max errors/warnings in log_monitor.py JSON output |
IOS_SIM_LOG_LINE_MAX | 300 | Per-line truncation in log summaries |
IOS_SIM_LOG_TAIL | 200 | Lines of log tail in verbose / sample output |
IOS_SIM_LOG_TEXT_SUMMARY | 15 | Errors/warnings shown in text-mode log summary |
IOS_SIM_MAX_ELEMENTS | 25 | Tappable elements listed by navigator.py |
IOS_SIM_POLL_INTERVAL | 0.5 | Boot/erase state polling interval (seconds) |
IOS_SIM_RELAUNCH_DELAY_MS | 1000 | Delay between terminate and re-launch in app_launcher.py |
IOS_SIM_SCREEN_BUTTONS_PREVIEW | 15 | Button names listed by screen_mapper.py |
IOS_SIM_SCREEN_SECTION_ITEMS | 10 | Items per section shown by screen_mapper.py |
IOS_SIM_STATE_SUBPROCESS_TIMEOUT | 15 | Subprocess timeout in app_state_capture.py (seconds) |
IOS_SIM_TAP_SETTLE_MS | 500 | Post-tap settle delay in navigator.py |
Example:
# Slow GitHub Actions runner: give boot 10 minutes
IOS_SIM_BOOT_TIMEOUT=600 python scripts/simctl_boot.py --wait-readySemantic Navigation: Find elements by meaning (text, type, ID) not pixel coordinates. Survives UI changes.
Token Efficiency: Concise default output (3-5 lines) with optional verbose and JSON modes for detailed results.
Accessibility-First: Built on standard accessibility APIs for reliability and compatibility.
Zero Configuration: Works immediately on any macOS with Xcode. No setup required.
Structured Data: Scripts output JSON or formatted text, not raw logs. Easy to parse and integrate.
Auto-Learning: Build system remembers your device preference. Configuration stored per-project.
Use these scripts directly or let Claude Code invoke them automatically when your request matches the skill description.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.