# tddworks/baguette: headless iOS Simulator control from a Swift CLI

> Baguette drives booted iOS simulators without opening Xcode or Simulator.app: frame streaming, host-side HID input, accessibility inspection and a browser UI. It is Apple Silicon only and links against private Xcode frameworks.

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

- Repository: https://github.com/tddworks/baguette
- Website: https://tddworks.github.io/baguette/
- Stars: 2,140 · Forks: 121
- Language: Swift
- License: Apache-2.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/tddworks-baguette

## What baguette replaces, and for whom

The default way to drive an iOS simulator is to open Simulator.app and click. That is fine for a human and useless for a script. Xcode's command line tools can boot a device (simctl), but the README positions baguette as the layer above that: input injection, screen streaming, accessibility inspection and log tailing, all from one Swift CLI named `baguette` plus a web UI served locally.

The intended user is an engineer automating iOS behaviour: someone writing agent-driven UI tests, building a device wall for demos, or wiring simulator control into a larger pipeline. The README frames it as "Headless iOS Simulator manager + host-side input injection for iOS 26", and the topics list (agent, cli, devicefarm, ios, simulator, simulatorkit, streaming) matches that audience. It is not a testing framework. There is no assertion API in the README; baguette supplies eyes and hands, and you bring the test logic.

## How input reaches the simulator without dylib injection

The interesting design decision is where the input is generated. For touch, baguette calls SimulatorKit's private symbols from the host process using what the README calls "the iOS-26 calling conventions". The streaming-touch and edge-gesture path goes through `IOHIDDigitizerDispatch`, which is why the README can claim that home-indicator swipes, app-switcher drags and Notification Center or Lock Screen pull-downs fire the real iOS recognizers. The README states there is no dylib injection on that path and no `DYLD_INSERT_LIBRARIES` to manage.

That is a genuine architectural split, and it matters because the camera feature takes the opposite route. Camera frames are injected by loading `VirtualCamera.dylib` (vendored from asc-pro/SimCam) into every simulator-launched app via `DYLD_INSERT_LIBRARIES`, with baguette pumping BGRA frames through a shared-memory ring buffer. So the no-injection property holds for input, not for the whole product. If you care about injection-free operation, scope that claim to touch and gestures.

Device orientation is a third mechanism: `baguette orientation --udid <X> portrait` sends a `GSEventTypeDeviceOrientationChanged` mach message at `PurpleWorkspacePort`, deliberately bypassing SimulatorKit's NSView path so the host stays headless. Three features, three different plumbing routes, all documented in the README.

## Installing baguette and booting your first device

Installation is a single Homebrew formula. The README gives no source-build instructions as the primary path; `build.sh` and a Makefile exist in the repository, but the documented route is the tap below.

```bash
brew install baguette
```

Two constraints are stated up front: Apple Silicon only, and Xcode 26 required, because baguette links against private SimulatorKit and CoreSimulator frameworks shipped with Xcode. On an Apple Silicon Mac where `brew` is Intel Homebrew running under Rosetta 2 from /usr/local, the install fails with a message telling you to use native Homebrew instead:

```bash
/opt/homebrew/bin/brew install baguette
```

If that path does not exist, the README points you at https://brew.sh to install native Apple Silicon Homebrew first. Once installed, the first real use is starting the local server and opening the device list.

```bash
baguette serve
open http://localhost:8421/simulators
```

The list page shows every simulator on the machine with Boot and Shutdown buttons. Clicking a booted device opens its focus-mode page: a full-window live stream, a DeviceKit-sourced bezel, and a toolbar with Camera, Accessibility, Logs, Home, Screenshot, Record and App-switcher controls. A second view, `http://localhost:8421/farm`, renders every booted simulator in a wall, grid or list with filtering and sorting, and clicking a tile focuses it through the same pipeline the CLI uses. Port 8421 is the only port the README names.

## Reading the screen: streaming, accessibility and logs

Frame streaming comes in two encodings, MJPEG or H.264/AVCC, over either stdout or WebSocket, with bitrate, fps and scale tunable at runtime. The README claims 60 fps and does not publish a benchmark table, so treat the number as the target the project aims at rather than a measured figure you can rely on for capacity planning. In-browser recording produces MP4 and composites the bezel, screen and gesture overlays into one file, which is convenient for bug reports and less useful if you want a clean, overlay-free capture.

For automation, the more valuable surface is `baguette describe-ui`, which returns the on-screen accessibility tree as JSON with per-node `role`, `label`, `value`, `identifier` and `frame` in device points. Hit-test mode (`--x --y`) returns the topmost node under a coordinate. This is what lets an agent or a script find a target without hardcoding pixel positions. The README notes it is powered by the private `AccessibilityPlatformTranslation` framework with a `bridgeTokenDelegate` the project installs itself, which is another private-API dependency to weigh.

Logs are the third leg. `baguette logs --udid <X>` streams `os_log` output to stdout, and `WS /simulators/:udid/logs` does the same into the browser's Logs panel. Predicate and bundle-id filters are supported. Streaming logs continuously is the point; if you want a bounded capture, you are responsible for cutting the stream yourself.

## Where baguette is the wrong tool

The hard boundary is the platform. Apple Silicon only, Xcode 26 required. Any CI fleet still on Intel Macs, or any containerised runner without a full Xcode install and its private frameworks, cannot run this. That is not a configuration detail; it is the precondition for the whole design.

The second boundary is API stability. Baguette depends on private SimulatorKit, CoreSimulator, `AccessibilityPlatformTranslation` and `IOHIDDigitizerDispatch` symbols, and the README ties the input path specifically to iOS-26 calling conventions. Private symbols move between OS and Xcode releases. The release cadence visible in the repository (v0.1.95 through v0.1.97 across August 2026, with the last push on 2026-09-08) suggests active churn rather than a frozen interface, and a version number still in 0.1.x is a fair signal that the surface is not settled.

The third boundary is what it does not do. There is no assertion layer, no test runner, no device-cloud integration and no documented rollback procedure if an upgrade breaks your pipeline. If your need is "run XCUITest suites", baguette is infrastructure underneath that, not a replacement for it.

## baguette against simctl and XCUITest

The obvious comparison is Apple's own `xcrun simctl`. simctl manages device lifecycle, installs apps and can take screenshots, and it ships with Xcode, so it has no private-framework risk and works wherever Xcode does. What it does not give you, per the README's framing, is host-side input injection, live frame streaming to a browser, an accessibility-tree dump as JSON, or a multi-device farm view. Those are baguette's additions.

XCUITest sits at the other end. It drives the app from inside the test process, with a supported API and a real assertion model, and it is the right answer when you are writing a test suite that must run on a device farm you do not control. Baguette differs in approach rather than degree: it injects input from the host through private SimulatorKit symbols and exposes the result as CLI output and WebSocket streams. That makes it better suited to agents, dashboards and exploratory automation, and worse suited to anything that has to survive an Xcode upgrade unattended.

The camera feature has its own lineage: `VirtualCamera.dylib` is vendored from asc-pro/SimCam, so if you only need webcam frames in the simulator, that upstream project is the more direct dependency.

## Licence, upgrade cost and what the repository shows

Baguette is Apache-2.0, which permits commercial use and modification with the usual notice and patent terms. That covers baguette's own code. It does not change the licensing position of Apple's private frameworks, and it does not automatically cover vendored third-party components; `VirtualCamera.dylib` is described as vendored from asc-pro/SimCam, so check that project's terms separately. This is a description of what the repository states, not legal advice.

Upgrade cost is the real ongoing expense. Because the input path is bound to iOS-26 calling conventions and private symbols, an Xcode or simulator runtime update is a potential breaking event, not a routine dependency bump. The repository ships a CHANGELOG.md and a docs/ directory including docs/features/camera.md, which is where a breaking change would surface, but the README does not document a version compatibility matrix or a rollback procedure. Pinning the Homebrew formula version and reading the changelog before upgrading is the practical posture.

The engineering discipline is visible in the layout: a Domain / Infrastructure / App split, mock-injected ports for Input, Screen, Accessibility, LogStream, Chromes, DeviceHost, Subprocess, CameraCapture, VideoCapture, CameraFrameSink, SimulatorInjection and Cameras, and a Makefile with a `test-web` target running `node --test 'Tests/Web/**/*.test.js'`. The README states that `swift test` requires no simulator at all, which is a deliberate choice and a good sign for contributors, though it also means the test suite cannot validate the private-framework integration that is the product's core.

## Conclusion

Adopt baguette if you are automating iOS UI on an Apple Silicon Mac with Xcode 26 installed and you want input, streaming, accessibility and logs behind one CLI plus a local web UI. Do not adopt it if you need Intel Macs, CI runners without a full Xcode install, or a build that avoids private frameworks; the README is explicit that baguette links against private SimulatorKit and CoreSimulator, and the camera path additionally loads a dylib via DYLD_INSERT_LIBRARIES. Before committing, verify on your own machine that /opt/homebrew/bin/brew install baguette resolves natively as arm64, that Xcode 26 is selected, and that booting one device plus `baguette describe-ui` and `baguette logs --udid <X>` work against your target app, because the README does not document a rollback path or a supported non-Xcode runtime.

## FAQ

### How do I use baguette to control an iOS simulator?

Install it with `brew install baguette`, then run `baguette serve` and open http://localhost:8421/simulators. The list page has Boot and Shutdown buttons, and clicking a booted device opens a focus-mode page with the live stream and input toolbar.

### What are the requirements for installing baguette?

The README states Apple Silicon only, with Xcode 26 required, because baguette links against private SimulatorKit and CoreSimulator frameworks shipped with Xcode. If brew runs under Rosetta 2 from Intel Homebrew in /usr/local, the install fails and you must use /opt/homebrew/bin/brew instead.

### Does baguette work on Intel Macs?

No. The README says Apple Silicon only, and the troubleshooting section explains that Intel Homebrew in /usr/local cannot install baguette because it runs under Rosetta 2. The documented workaround is native Homebrew at /opt/homebrew/bin/brew.

### Can baguette feed a Mac webcam into the iOS simulator?

Yes, as of 0.1.72. You pick a camera in the browser's Camera card and click Start; baguette loads VirtualCamera.dylib into simulator-launched apps via DYLD_INSERT_LIBRARIES and pumps BGRA frames through a shared-memory ring buffer into the simulator's camera APIs.

### Does baguette need a dylib injected into my app?

Not for touch input. The README states the iOS-26 streaming-touch and edge-gesture path uses IOHIDDigitizerDispatch with no dylib injection and no DYLD_INSERT_LIBRARIES to manage. The camera feature is the exception, since it does load VirtualCamera.dylib.

## Sources

- [License: Apache-2.0](https://github.com/tddworks/baguette/blob/main/LICENSE)
- [Project website](https://tddworks.github.io/baguette/)
- [README](https://github.com/tddworks/baguette/blob/main/README.md)
- [Releases](https://github.com/tddworks/baguette/releases)
- [tddworks/baguette on GitHub](https://github.com/tddworks/baguette)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tddworks-baguette
