Fold, rotate and touch the iPhone Duo simulator from the command line —
built for scripts and AI coding agents.
duoctl close # fold shut: the cover screen takes over
duoctl open --duration 1.5 # unfold flat, animated: the inner screen takes over
duoctl rotate portrait # rotate the active screen's interface
duoctl tap --label "Continue" # tap — on the inner screen too
duoctl swipe 330 800 330 300 # scroll
duoctl screenshot shot.png # capture whichever screen is showing
duoctl state # hinge angle, active screen, orientation, sizeXcode 27.1 ships an iPhone Duo simulator, and Device Hub can fold it, rotate it and tap either screen — by hand. Nothing scriptable could, which leaves the unfolded inner screen untestable for CI and AI coding agents:
| You want to… | What exists today | duoctl |
|---|---|---|
| Fold or unfold | simctl and devicectl have no hinge command |
duoctl close, open, half, hinge <deg> |
| Rotate | devicectl device orientation set and XCUIDevice.shared.orientation report success and change nothing |
duoctl rotate portrait |
| Tap the inner screen | AXe (built on idb), XcodeBuildMCP and RocketSim deliver every touch to the cover screen, so on an unfolded Duo they report success and nothing happens | duoctl tap, long-press, swipe |
| Screenshot what's showing | simctl io screenshot captures the inner screen even while folded (a black image) |
duoctl screenshot |
The silent failures are the worst part: every one of those calls exits 0, so a script or
an agent carries on as if it worked. duoctl reads the device state back after every fold
and rotation, and fails with an error when it didn't happen.
- macOS with Xcode 27.1 or later and the iOS 27.1 simulator runtime
- A booted iPhone Duo simulator
- Python 3 (ships with the Xcode command line tools)
- Optional: AXe for
--label,--idandelements
For AI agents — install the agent skill with skills.sh or openskills. It bundles the CLI, so nothing else is needed:
npx skills add skarol/duoctl
# or
npx openskills install skarol/duoctlOn your PATH — with Homebrew, e.g. on CI:
brew install skarol/tap/duoctlor from source:
git clone https://github.com/skarol/duoctl.git
ln -s "$PWD/duoctl/bin/duoctl" /usr/local/bin/duoctlThe first run compiles a small helper for the simulator with Xcode's clang and caches it
in ~/.cache/duoctl.
On a macOS runner with Xcode 27.1:
brew install skarol/tap/duoctl
UDID=$(xcrun simctl create ci-duo "iPhone Duo" com.apple.CoreSimulator.SimRuntime.iOS-27-1)
xcrun simctl boot "$UDID" && xcrun simctl bootstatus "$UDID" -b
export DUOCTL_UDID="$UDID"
duoctl open
duoctl rotate portrait
duoctl screenshot unfolded-portrait.pngduoctl [-d <udid|name>] <command>
state print state as JSON
open | close | half [--duration s] set the hinge to 180° / 0° / 90°
hinge <degrees> [--duration s] set any hinge angle, 0 (closed) to 180 (flat)
rotate <orientation> portrait | landscape | portrait-upside-down | landscape-flipped
tap <x> <y> | --label L | --id I tap (interface points of the active screen)
long-press <x> <y> [--duration s] press and hold
swipe <x1> <y1> <x2> <y2> [--duration] drag between two points
screenshot <path> PNG of the screen showing the interface
elements labelled accessibility elements with frames
--duration animates the fold from the current angle instead of jumping, which makes
Device Hub play a smooth fold — useful for recordings.
With one iPhone Duo booted, duoctl uses it. With several, pass -d or set
DUOCTL_UDID — it never guesses.
Orientation names describe the interface, not the device: the inner screen sits a
quarter turn from the cover, so its default unfolded orientation is landscape.
Everything runs through a small helper that duoctl starts inside the simulator with
xcrun simctl spawn.
Hinge and orientation. Device Hub's hinge slider and orientation picker both send a
vendor-defined HID event (usage page 0xFF61, usage 0x5B) whose payload is a
binary-serialized dictionary. locationd turns it into a hinge angle or a device
orientation:
| Control | source |
type |
value |
|---|---|---|---|
| Hinge | hinge-slider-control |
range |
degrees, 0–180 |
| Orientation | orientation-picker-control |
enum |
portrait, pud, landscape-left, landscape-right, faceup, facedown |
Both carry provider = "com.apple.Virtualization". duoctl rotate tries device
orientations until the active screen reports the rotation you asked for, because the
same device orientation rotates the cover and inner screens differently.
Touches. The simulator exposes one CoreDevice touchscreen HID service per display,
each tagged with its display's UUID. Existing tools inject touches through the legacy
simulator HID path, which always lands on the main (cover) touchscreen. duoctl reads the
active display's UUID from devicectl, clones that display's touchscreen as a virtual
HID service, and dispatches digitizer events through the clone, converting your
interface coordinates to the panel's native axes for the current rotation.
State is read back with xcrun devicectl device info displays and
xcrun devicectl device motion hinge-angle.
- Simulator only.
- Relies on private, undocumented interfaces verified with Xcode 27.1 and the iOS 27.1
runtime. A future release may change them;
duoctlchecks state after every change so a break shows up as an error rather than a silent no-op. - Device Hub's hinge slider and rotation indicator don't follow changes made by
duoctl.
The hinge event format was first documented by Artem Novichkov's hinge.
MIT — see LICENSE.
