CLI tool
tddworks/baguette avatar
tddworks/baguette

Baguette: Headless iOS Simulator Control Through SimulatorKit's Private Symbols

Headless control for Apple's Simulators — 3D models, taps, swipes, multi-finger gestures, 60 fps streaming, and a multi-device farm

1,777 stars99 forksSwiftApache-2.0

At a glance

What is it?
Baguette is a Swift CLI and web UI that drives booted iOS simulators without opening Xcode or Simulator.app. It works by calling private SimulatorKit and CoreSimulator APIs, which is what makes it useful and also what limits where it can run.
Who is it for?
Adopt baguette if you are on an Apple Silicon Mac with Xcode 26 and you need scripted taps, gestures, screenshots, log tailing or a browser-based multi-device wall without a human at Simulator.app. Do not adopt it if you are on Intel hardware, if you cannot keep Xcode 26 installed, or if you need something that will not break when Apple changes an undocumented framework.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem: driving a simulator without a window

Simulator.app is built for a person. You open it, you click a device, you watch the screen, you click again. Anything scripted on top of that has to fight a GUI that was never designed to be a backend. Baguette takes the opposite position: the simulator is a process you control, and the screen is a frame stream you subscribe to.

The README describes it as a headless iOS Simulator manager plus host-side input injection for iOS 26. The intended audience is narrow and specific. It is for people writing automation that needs to boot a device, send real gestures, read the accessibility tree, capture video, and do all of it from a shell or from a browser tab. Agent tooling is listed in the repository topics next to cli, devicefarm and streaming, which matches the shape of the API: JSON out, commands in, no window required.

How input injection actually reaches the simulator

This is the part worth understanding before you install anything. Baguette does not synthesize events at the window-server level and it does not sit on top of XCUITest. According to the README, taps, swipes, streaming one- and two-finger gestures, pinch, pan, scroll, Mac keyboard input and hardware buttons all go through SimulatorKit's private symbols using what the project calls the iOS-26 calling conventions.

The iOS 26 streaming-touch and edge-gesture path uses IOHIDDigitizerDispatch. That detail matters because it is the difference between a gesture that looks right in a screenshot and a gesture that fires the real iOS recognizer. The README states that home-indicator swipes, app-switcher drags and Notification Center or Lock Screen pull-downs all trigger the actual system recognizers, and that this path requires no dylib injection and no DYLD_INSERT_LIBRARIES to manage.

Orientation takes a different route. The orientation command fires a GSEventTypeDeviceOrientationChanged mach message at PurpleWorkspacePort, which the README says bypasses SimulatorKit's NSView path so the host can stay headless. Two subsystems, two mechanisms, one CLI.

The camera path does use dylib injection

The clean no-injection story applies to touch, not to everything. Camera support, added in 0.1.72, pipes a Mac webcam into the simulator's AVCaptureVideoPreviewLayer, AVCapturePhotoOutput and UIImagePickerController. To do that, baguette loads VirtualCamera.dylib into every simulator-launched app via DYLD_INSERT_LIBRARIES, and pumps BGRA frames through a shared-memory ring buffer. The dylib is vendored from asc-pro/SimCam, and the README points at docs/features/camera.md for the details.

So the honest summary is that baguette has two injection strategies depending on the subsystem. If you are evaluating it for a workflow that must not modify the launched app's environment, the camera feature is the one to exclude, not the input path.

Streaming, the accessibility tree and the log tail

Frame streaming is offered as MJPEG or H.264/AVCC over stdout or WebSocket, with bitrate, fps and scale tunable at runtime. The README claims 60 fps and describes in-browser MP4 recording that composites the bezel, the screen and gesture overlays into a single file. I have not measured the frame rate and the README does not publish a methodology, so treat 60 fps as the project's stated target rather than a number you can plan capacity around.

Accessibility inspection runs through baguette describe-ui, which returns the on-screen tree as JSON with per-node role, label, value, identifier and frame in device points. Hit-test mode takes --x and --y and returns the topmost node under that coordinate. The README says this is powered by the private AccessibilityPlatformTranslation framework with a bridgeTokenDelegate the project installs itself. For anyone building a selector strategy, the identifier and frame fields are the ones that matter, and the fact that they come from a private framework is the risk you are accepting.

Logging is straightforward by comparison: baguette logs --udid <X> streams os_log output to stdout, with predicate and bundle-id filters, and WS /simulators/:udid/logs does the same into the browser's Logs panel.

Getting it running: Homebrew, Xcode 26, and one port

Installation is a single Homebrew formula. The README gives brew install baguette and states the constraints plainly: Apple Silicon only, and Xcode 26 required, because baguette links against private SimulatorKit and CoreSimulator frameworks shipped with Xcode.

The failure mode most people will hit first is not documented as a bug because it is not one. If brew install baguette prints that baguette requires Apple Silicon Homebrew running natively as arm64, your brew is the Intel copy under Rosetta 2 in /usr/local. The fix in the README is explicit: run /opt/homebrew/bin/brew install baguette, and if that path does not exist, install native Homebrew from brew.sh first.

Once installed, the quickstart is two commands and a browser. baguette serve opens http://localhost:8421/simulators, which lists every simulator on the machine with Boot and Shutdown buttons; clicking a booted device opens its focus-mode page with a live stream, a DeviceKit-sourced bezel, and a toolbar carrying Camera, Accessibility, Logs, Home, Screenshot, Record and App-switcher controls. http://localhost:8421/farm renders every booted simulator in a wall, grid or list with filtering and sorting, and clicking a tile focuses it for full-quality streaming and input through the same pipeline the CLI uses.

For embedding, the README shows a small JS SDK under Resources/Web/baguette/ used as const sim = await Baguette.use({...}); sim.mount(container);, with each part (screen, buttons, keyboard) hanging off one Simulator instance.

Where baguette is the wrong tool

The dependency on private frameworks is the central trade-off, and the README does not pretend otherwise. Baguette links against SimulatorKit and CoreSimulator as shipped with Xcode 26, calls private symbols with iOS-26 calling conventions, and reaches into AccessibilityPlatformTranslation for the UI tree. Anything Apple changes in those frameworks can break input injection, orientation or describe-ui between Xcode releases. There is no public API contract underneath this.

That has a second consequence: the Xcode version is not negotiable. If your CI image pins an older Xcode, or if you deliberately track a beta, baguette's assumptions may not hold. The install instructions are written for exactly one configuration, Apple Silicon plus Xcode 26, and nothing in the README describes a fallback path for other setups.

The camera feature is the other place to be careful. Loading VirtualCamera.dylib through DYLD_INSERT_LIBRARIES changes the environment of every simulator-launched app. If you are testing something that inspects its own dynamic libraries, or if you want a test run that is byte-identical to a run without baguette, that path is not for you.

Finally, this is a young project by version number. The recent release list shows 0.1.95, 0.1.96 and 0.1.97 within a few weeks of each other. Rapid patch releases at that cadence usually mean active fixes rather than a frozen surface, so pin a version if you depend on specific behaviour.

Compared with XCUITest and simctl

The obvious comparison is Apple's own tooling, and the difference is architectural rather than a matter of features.

XCUITest drives the simulator from inside the app process, through the accessibility system, using a test bundle that Apple supports publicly. That gives you a stable contract and a debugging story, at the cost of running a test host and living inside the XCTest lifecycle. Baguette sits outside the simulator entirely and injects HID events at the host level. You get a CLI that returns JSON, a WebSocket stream, and no test bundle, but you give up the supported API surface.

simctl is the other reference point. It manages simulator lifecycle well: create, boot, install, launch. What it does not do is dispatch a two-finger gesture, stream the screen at a tunable bitrate, return the accessibility tree as JSON, or render a multi-device wall in a browser. Baguette is not replacing simctl so much as filling the gap between simctl's device management and the input and observation layer that only existed inside Simulator.app.

If your requirement is a supported, long-lived automation API and you can tolerate the XCTest harness, XCUITest is the safer choice. Baguette's value is the case where you cannot or will not run a test bundle, and you need the simulator to behave like a remote device.

Maintenance cost, testing, and the licence

The repository describes a layered Domain, Infrastructure and App split with mock-injected ports for Input, Screen, Accessibility, LogStream, Chromes, DeviceHost, Subprocess, CameraCapture, VideoCapture, CameraFrameSink, SimulatorInjection and Cameras. The README states that swift test requires no simulator at all, which is a meaningful property: you can run the unit suite on a machine that cannot run the product. It also states the suite is written with Swift Testing and backed by auto-generated MockXxx fakes. I have not run it, so I am reporting the claim, not verifying it.

The real maintenance cost is not in the Swift code. It is in tracking Xcode. Every dependency on SimulatorKit, CoreSimulator, IOHIDDigitizerDispatch, PurpleWorkspacePort and AccessibilityPlatformTranslation is an implicit coupling to a specific Xcode 26 build. Budget for re-verifying input, orientation and describe-ui after any Xcode update rather than assuming they carry over.

Licensing is Apache-2.0, which the repository states. That is a permissive licence with an explicit patent grant, and it is worth noting that the vendored VirtualCamera.dylib comes from asc-pro/SimCam, so the camera path may carry its own attribution requirements separate from baguette's own licence. Read the vendored component's terms before shipping anything that depends on the camera feature. This is a description of what the licence identifiers say, not legal advice.

Editorial conclusion

Adopt baguette if you are on an Apple Silicon Mac with Xcode 26 and you need scripted taps, gestures, screenshots, log tailing or a browser-based multi-device wall without a human at Simulator.app. Do not adopt it if you are on Intel hardware, if you cannot keep Xcode 26 installed, or if you need something that will not break when Apple changes an undocumented framework. Before committing, verify three things on your own machine: that /opt/homebrew/bin/brew is the native arm64 Homebrew and not the Rosetta copy in /usr/local, that baguette serve actually binds port 8421 without a conflict, and that describe-ui returns the roles and identifiers your automation depends on for the specific app you intend to drive.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. tddworks/baguette on GitHub
Community notes

Community notes