Your agent gets its own Mac desktop. You keep yours.
offstage gives your coding agent a second macOS account, logged in behind yours. Simulators, Xcode UI tests and the app it just built open on that account's desktop. Your windows, keyboard and mouse stay yours.
claude mcp add offstage -- npx -y --package=@viraatdas/offstage@latest offstage-mcp
# ~/.codex/config.toml
[mcp_servers.offstage]
command = "npx"
args = ["-y", "--package=@viraatdas/offstage@latest", "offstage-mcp"]
// opencode.json
"mcp": {
"offstage": {
"type": "local",
"command": ["npx", "-y", "--package=@viraatdas/offstage@latest", "offstage-mcp"]
}
}
npm i -g @viraatdas/offstage
Free and MIT licensed. For Macs on Apple Silicon with Node 20 or newer. The helper account takes one setup command.
One Mac, two desktops, at the same time
off your screen: yes.
Both captures are real, taken on one Mac on 2026-09-22. The console user was watching a video in a browser the whole time. The moving cursor above illustrates the agent's loop: screenshot, decide, click, screenshot.
Every command goes somewhere that is not your screen
Agents also run tests and browsers. offstage reads each command before it runs and sends it to the cheapest place that keeps it off your display.
| When your agent runs | offstage runs it |
|---|---|
xcodebuild test, xcrun simctl, XCUITests, open -a, osascript, a built .app |
In the second macOS account. It has its own desktop, window server and input. This is why offstage exists. |
--headed, headless: false, cypress open, WebGL and GPU flags |
In a Linux container with a virtual display, if Docker is installed. It's a real display, just not yours. |
npm test, vitest, headless Playwright and Puppeteer |
Right where it is. These never open a window, so wrapping them would only cost time. |
An installer, a .pkg, a .dmg, hdiutil |
Nowhere. Both accounts share one machine, so offstage refuses these, and no flag overrides that. |
Paste this into your agent
Send it as a message in Claude Code, Codex or opencode. The agent
connects offstage itself and keeps GUI work off your screen from
then on. Put it in your project's AGENTS.md or
CLAUDE.md to make it stick.
Use offstage (https://github.com/viraatdas/offstage) for any command that
could open a window or take focus on my Mac. It runs GUI work in a second,
logged-in macOS account, so nothing appears on my screen.
Set it up if the offstage_* MCP tools are not already available:
- Claude Code: claude mcp add offstage -- npx -y --package=@viraatdas/offstage@latest offstage-mcp
- Codex: add to ~/.codex/config.toml
[mcp_servers.offstage]
command = "npx"
args = ["-y", "--package=@viraatdas/offstage@latest", "offstage-mcp"]
- opencode: add to opencode.json
"mcp": { "offstage": { "type": "local",
"command": ["npx", "-y", "--package=@viraatdas/offstage@latest", "offstage-mcp"] } }
The tools appear once I restart you. Until then, or if MCP is not an option,
install the CLI (npm i -g @viraatdas/offstage) and use `offstage route -- <cmd>`
and `offstage run -- <cmd>`; they are the same code path as the tools.
Rules:
1. Never run these directly: Playwright, Puppeteer, Cypress, or WebDriver
with --headed or headless: false; screen or video capture; xcodebuild;
xcrun simctl; XCUITests; simulators; open -a; osascript; or a built .app.
Call offstage_route to see which lane the command gets, then offstage_run
to run it there. Commands that are already headless (npm test, npx
playwright test) run in place at no cost, and that is the right answer.
2. To test an app with a GUI: offstage_session_launch (it waits until the app
registers and returns its pid), then offstage_session_screenshot, decide,
offstage_session_input, and screenshot again to confirm. Coordinates are
points, not pixels: divide a pixel coordinate by the screenshot's scale.
offstage_session_quit closes the app when you are done.
3. status "skipped" means nothing ran anywhere. Show me the fix line from
diagnostics and stop. Never re-run the command outside offstage to get
past it: that puts it on my screen.
4. "Refused" (an installer, .pkg, .dmg, or hdiutil) means offstage will not
run it on any lane, and no flag overrides that. Tell me; running it is my
call, not yours.
5. Two things only I can do, so ask instead of working around them: if a run
errors because the helper account cannot read my repository, I run
`offstage session share <dir>`; if the session lane is not set up, I run
`offstage session setup --create` in a terminal (it needs sudo).
Set up the helper account once
Your agent can't do this part, because it needs sudo
and one click from you.
-
Install the CLI
npm i -g @viraatdas/offstage offstage doctordoctortells you which lanes work on this Mac and how to fix the rest. -
Create the helper account
sudo offstage session setup --createThis creates an ordinary account called
computeruseand builds a small Swift daemon for it. If your terminal has Full Disk Access, it also grants that daemon Screen Recording and Accessibility; if not, it tells you which two toggles to flip. It prints the whole root script before running it. -
Switch to it once, then switch back
offstage session statusUse the user menu in the menu bar. That first login starts the daemon, and the account stays logged in behind yours until the Mac restarts.
statusexits 0 when it's ready. -
Let it read your project
offstage session share ~/code/myappIf a run fails because the helper account can't read your project, share that folder with it. Sharing is read-only, one folder at a time, and
unsharetakes it back.
Questions people ask first
Is this a virtual machine?
No. It's a second user account on the Mac you already have, with no guest OS and nothing to boot. It takes about 3 GB of disk, where the macOS VM image we measured was a 68.8 GB download. The trade-off is that both accounts share the Mac's CPU, memory and disk.
Can the agent ever click on my screen?
The daemon posts input to its own session only, never to the global input stream that feeds your screen. If its session is ever the one on the console, it refuses to send input at all. In testing, the window server's own log showed every synthetic event landing in the helper session and none reaching the console.
What can the helper account read?
It's an ordinary second user on your Mac, so it can't write to
your files. It can read what macOS lets any other local account
read. offstage session share gives it read-only
access to a folder it can't otherwise reach, and each run writes
its output to its own artifacts folder.
Which agents does it work with?
Claude Code, Codex and opencode, through MCP, and Claude Code also
as a plugin. Anything else that can run a shell command can use
the offstage CLI, which runs the same code.