From xcode-sim-automation
Interactively controls an app through XCUITest via a CLI. Claude reads UI state and screenshots, decides actions, and executes commands. Use for dynamic UI exploration, complex navigation flows, or when pre-scripted navigation isn't feasible.
npx claudepluginhub gestrich/xcode-sim-automation --plugin xcode-sim-automationThis skill uses the workspace's default tool permissions.
Enables Claude to dynamically control an app through XCUITest using a CLI that abstracts the file-based protocol. Unlike pre-scripted tests, this allows Claude to explore the UI, make decisions based on current state, and recover from unexpected situations.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Enables Claude to dynamically control an app through XCUITest using a CLI that abstracts the file-based protocol. Unlike pre-scripted tests, this allows Claude to explore the UI, make decisions based on current state, and recover from unexpected situations.
Invoke this skill when you need to:
The skill will ask for your goal if not specified (e.g., "Navigate to Settings and enable Dark Mode").
Read .xcuitest-config.json from the project root. If it exists, use its values throughout this skill:
$PROJECT = config.xcodeProject (e.g., "MyApp.xcodeproj")$SCHEME = config.scheme (e.g., "MyApp")$DESTINATION = config.destination (e.g., "platform=macOS")$UI_TEST_TARGET = config.uiTestTarget (e.g., "MyAppUITests")$TEST_CLASS = config.testClass (e.g., "InteractiveControlTests")$TEST_METHOD = config.testMethod (e.g., "testInteractiveControl")$CONTAINER = config.containerPath (e.g., "~/Library/Containers/.../Data/tmp")$PROCESS_NAME = config.processName (e.g., "MyApp")If .xcuitest-config.json doesn't exist, ask the user for these values before proceeding.
If config.appSpecificNotes is set, read that file from the project root for app-specific navigation patterns and accessibility identifiers.
The CLI wrapper script (Tools/xcuitest-control) is in the xcode-sim-automation repo. To find it:
Tools/xcuitest-control wrapper script (not the .py file)~/Developer/personal/xcode-sim-automation/Tools/xcuitest-controlgit clone https://github.com/gestrich/xcode-sim-automation.gitThe wrapper auto-builds the Swift CLI binary on first run and whenever source files change — no manual build step needed.
A Python fallback (Tools/xcuitest-control.py) is also available if the Swift toolchain isn't installed.
Set the CLI path variable:
CLI=<path-to-xcuitest-control>
Add the package to your project via SPM:
// In Package.swift or via Xcode:
.package(url: "https://github.com/gestrich/xcode-sim-automation.git", from: "1.0.0")
Create a UI test in your project's UI test target ($UI_TEST_TARGET):
import XCTest
import XCUITestControl
final class InteractiveControlTests: XCTestCase {
@MainActor
func testInteractiveControl() throws {
let app = XCUIApplication()
app.launch()
InteractiveControlLoop().run(app: app)
}
}
Set these variables at the top of every Bash command (shell state does not persist between Bash tool calls):
CLI=<path-to-xcuitest-control>
CT="$CONTAINER"
Where $CONTAINER comes from .xcuitest-config.json or is the sandbox container path for your app's UI test runner.
Kill any app processes from previous runs (stale processes cause "Failed to terminate" errors):
pkill -f "$PROCESS_NAME" 2>/dev/null; sleep 2
$CLI -c "$CT" reset
Always build first (catches errors without hanging), then run. All xcodebuild commands must be run from the directory containing $PROJECT (the .xcodeproj file).
xcodebuild build-for-testing \
-project $PROJECT \
-scheme $SCHEME \
-destination '$DESTINATION'
CRITICAL: The xcodebuild test-without-building command must be run using the Bash tool's run_in_background: true parameter. Do NOT use shell & backgrounding — the process will be killed when the Bash tool call completes.
# Use run_in_background: true on the Bash tool for this command
xcodebuild test-without-building \
-project $PROJECT \
-scheme $SCHEME \
-destination '$DESTINATION' \
-only-testing:"$UI_TEST_TARGET/$TEST_CLASS/$TEST_METHOD"
The test will:
Use the ready command to poll until the test is running:
$CLI -c "$CT" ready --timeout 30
CRITICAL: Always activate the app first to bring it to the foreground. If the app window is behind other windows, scroll/tap commands will fail with "Unable to find hit point".
$CLI -c "$CT" activate
Use the CLI to execute actions:
# Read current UI state (use the Read tool)
# Read $CT/xcuitest-hierarchy.txt
# View screenshot (use the Read tool)
# Read $CT/xcuitest-screenshot.png
# Execute action
$CLI -c "$CT" tap --target settingsButton --target-type button
# Read updated hierarchy and screenshot after action
See cli-reference.md for the full command reference.
When the goal is achieved:
$CLI -c "$CT" done
Note: The done command will report a timeout — this is expected. The test exits before writing a "completed" status. Check the xcodebuild output for "TEST EXECUTE SUCCEEDED" to confirm clean shutdown.
After exit, kill any orphaned app processes:
pkill -f "$PROCESS_NAME" 2>/dev/null
On macOS, Xcode always sandboxes the XCUITest runner. The test runner cannot write to /tmp/. Files are written to the runner's sandbox container instead.
Use the --container (-c) flag on every CLI command to set all file paths from the container directory:
$CLI -c "$CT" screenshot
$CLI -c "$CT" tap --target myButton --target-type button
IMPORTANT: Shell state does not persist between Bash tool calls. You must include CLI=... and CT=... in every Bash command that uses the CLI.
The hierarchy file shows the element tree with types, identifiers, and labels:
Application, pid: 12345, label: 'MyApp'
Window, 0x600000001234
Other, identifier: 'mainView'
Button, identifier: 'settingsButton', label: 'Settings'
StaticText, identifier: 'welcomeLabel', label: 'Welcome!'
Cell, identifier: 'item_1', label: 'First Item'
Slider, identifier: 'volumeSlider', value: '50%'
From this hierarchy:
settingsButton is a Button → --target-type buttonwelcomeLabel is a StaticText → --target-type staticTextitem_1 is a Cell → --target-type cellvolumeSlider is a Slider → --target-type sliderUse --target-type any if unsure — it searches all element types.
When interacting with text fields, the keyboard may appear and affect other UI elements.
Tap on a non-interactive element that's visible:
$CLI tap --target notesLabel --target-type staticText
Tips for dismissing the keyboard:
StaticText elements (labels) that are above the keyboardtype --value "\u{1b}" if neededTap the text field first to focus it:
$CLI tap --target searchBar --target-type any
Then type your text:
$CLI type --value "Hello"
activate after starting the test to bring the app to foreground--container (-c) flag — Set all file paths with one flaganywait command if UI is animatingdone command when finished--target-type any if specific type failsbuild-for-testing first to avoid hangs--target <listIdentifier> --target-type any rather than scrolling the app itself