shuttle acts on your own desktop, with your accounts signed in. The controls below are built on macOS (P1) and Windows (P2). On Linux, with its desk backend, shuttle stop and resume, the script_run switch and the app allowlist are built; its hotkey, indicator and confirm dialog are not yet.
The kill switch, the app allowlist and the confirm mode are guard rails for an agent that means well and reaches for the wrong app or acts at the wrong time. They bound the desk, screenshot and gui_plan tools. They are not a sandbox: with script_run on, which is the default, a script is your own user’s code, and it can resume a stopped daemon, edit config.toml, restart the daemon or start programs, and no check inside shuttle stops it. A service plugin is your own user’s code in the same way. The guard rails hold only with script_run = false under [tools] and no service plugin installed.
shuttle stop stops shuttle at once: it ends the running gui_plan or script_run process with every process it started, ends the desk and model calls in flight, and halts. On macOS and Linux the run’s process group gets SIGTERM, so a script can release the keys it holds, and whatever is left 2 s later gets SIGKILL; on Linux the apps the desk moved into scopes of their own are spared. Its desk calls already fail during those 2 s. On Windows the run’s Job object ends at once, since Windows has no SIGTERM. Every tool call except status then fails with stopped by the user until you resume, with shuttle resume or the indicator’s Resume. The stop lives in the running daemon; a daemon that starts again starts running. Only you stop and resume, from the command line, the hotkey or the indicator’s menu: no MCP tool and no gui_plan step reaches them. A desk action that the driver or the accessibility API has already started ends on its own, so at most one action in flight completes after the stop. Likewise a window list or tree read in flight cannot be cancelled: a call after resume can queue behind it, at most until its own accessibility limit (3 s; 10 s for snapshot, find and press), and then fails with no accessibility answer.
A global hotkey does the same stop, and the indicator’s menu has Stop and Resume. The hotkey is built on macOS and Windows; Linux does not have it yet, and its row is the planned default:
| OS | Default | Registered with |
|---|---|---|
| macOS | ⌃⌥⌘⎋ (Control-Option-Command-Escape) | RegisterEventHotKey, which needs no permission |
| Windows | Ctrl+Alt+Shift+Esc | RegisterHotKey |
| Linux (X11) | Ctrl+Alt+Shift+Esc | XGrabKey |
Set kill_hotkey in config.md to change it, or to off to register no global hotkey (the menu’s Stop and shuttle stop remain). On macOS a system permission prompt in front holds the hotkey until the prompt closes; shuttle stop works regardless. On Wayland a global hotkey depends on the compositor’s GlobalShortcuts portal; where there is none, shuttle stop from a terminal is the kill switch.
On macOS a menu-bar item shows a shuttle icon: outlined when idle, filled while a desk action or a run is in progress, and struck through after a stop. Its tooltip and the first line of its menu name the version and the input mode, and the menu has Stop and Resume. A menu-bar manager such as Ice or Bartender puts a new item in its hidden section, and the notch hides items that do not fit beside it: move the item into the visible section once. indicator = false hides the menu-bar item. cua-driver draws its agent cursor where shuttle points either way, since macOS needs its overlay to track the front window; on Windows [driver] overlay = false in config.md turns the cursor off. On Windows a notification-area icon shows the same three states, as the stock application, information and error icons with the tooltip Shuttle, Shuttle acting or Shuttle stopped; a click opens its Stop and Resume menu. Windows 11 puts a new icon under the ^ overflow until you drag it onto the taskbar. indicator = false hides it. Linux has no tray icon yet.
confirm = "coordinate" asks before every step that can end in a click at a screen point, the least predictable action: click, press, act with click, and drag, which presses at one point and releases at another. confirm = "every" asks before every desk action: press, act, key, type, click, scroll, drag, focus, close and launch. desk computer_act is an action too: coordinate asks before its tap, click, left_click, double_tap, double_click, right_click, triple_click, long_press, swipe and drag, its type at an element or a point, which clicks there first, and a list that holds any of them; every asks before each act. On macOS the question is a dialog with Cancel and Allow: Escape presses Cancel and Return presses neither. A step you cancel fails with declined by the user, and a stop closes an open dialog. While the dialog is open every other desk call fails at once with a confirm dialog is open, so the agent cannot answer the dialog itself. On Windows the question is a message box with OK and Cancel; Cancel is the default, so Return and Escape both refuse. The box opens in front of other windows with the keyboard focus; if Windows refuses it the focus, its taskbar button flashes instead. Linux has no dialog yet, so a step that would ask fails and names confirm = "off". With confirm = "every", a gui_plan app step for an app that is not running asks twice, first to focus it and then to launch it; declining the focus does not end the step, and the launch still asks.
apps = ["TextEdit", "notepad.exe"] under [safety] limits the desk calls to windows of those apps. A name matches the macOS app name, the Windows executable or the Linux executable’s file name (/proc/PID/exe), case-insensitive and with or without .exe; a Linux window whose executable cannot be read is refused. The daemon checks the app on every desk call:
key and type;read, press and act, and of the element at finds;click, press or act, and the topmost element there across every window layer, such as the Dock or the taskbar; on macOS, an open menu item at the point instead.windows lists only the allowed apps’ windows, and launch and a gui_plan app step start only an allowed app. The computer calls of wire.md, which see and click the whole screen, are refused while the list is not empty. A screenshot or crop, also the one a model call gets, is black wherever no allowed app’s window is on top: the desktop, the menu bar, the Dock or taskbar, other apps’ windows, and on macOS also open menus. An empty list allows every app and costs nothing. A check reads the window list once, which costs a few ms to a few hundred ms on macOS depending on the apps on screen.
The check runs before the action, not at the input event: a window that comes to the front in between can take a key or a click. A screenshot and a click each read the window list on their own, so they can see different windows.
The screenshot mask is per window rectangle, not per pixel:
config.toml there is no allowlist.[daemon] socket_group (config.md) lets every member of that group call the daemon as you do:
gui_plan and, with script_run on, script_run and the plugin tools, which run as your user;The daemon reads the user of each connection’s peer and answers safety_stop and safety_resume only from its own user, so a member cannot send them directly. That keeps the stop yours only while script_run is off and no service plugin is installed: a member’s script or service runs as your user, and its calls, the stop and the resume among them, pass as yours. The app allowlist and the confirm mode apply to their calls as to yours. Give the group only to accounts you would let use your desktop; script_run = false keeps them to the desk tools.
script_run = false under [tools] turns off the one tool that runs the agent’s own code, and the plugin tools of wire.md with it: the MCP server lists none, and script_run and every plugin answer how to turn it back on. gui_plan, desk and screenshot keep working, and so do service plugins. While plugins run, their desk calls pass the same app allowlist, confirm and stop as any other.
A service plugin is a program you install in the plugin directory; the daemon runs it as your user and hands it the calls of its tools. Installing it is what turns it on: script_run = false does not turn it off, so that the tools it serves keep working where scripts are off.
shuttle stop refuses new calls of its tools and closes every one in flight. The service itself keeps running, and work it started for a call, such as a shell command, is its own to end when the connection closes; shuttle does not end it.SHUTTLE_SOCKET pass them, as any other desk call.macOS has one pointer and one keyboard focus, shared by you and shuttle:
desk computer_act input at a screen point acts like a human click: it drives the real pointer and may bring the app it clicks to the front. Background input refuses it.On Windows the same order holds with UI Automation patterns and cua-driver input. On Linux AT-SPI actions come first too.
With [desk] input = "background" (config.md), shuttle sends input to its target window through cua-driver without fronting that window or moving your pointer. shuttle sends no fronting call, but an app may still bring itself to the front in reaction to the input, for example when a menu opens. It never falls back to the foreground; an action that needs the foreground fails and names why:
window or pid);focus, select or action on an element refuses, since UI Automation runs them by bringing the element’s window to the front, and a press clicks the element in its window instead, and refuses when it shows no point to click;gui_plan drives the window its app or wait step names and the dialogs that window owns, never the front window, and it refuses on Linux, where background plans are not measured. A plan step refuses when it would need the screen, which shows whatever covers the plan’s window (finding an element by its look, or reading a state from pixels); when it runs a macOS menu-bar entry, which only the front app takes; when another window of the app has the same title, since input names its window by title; when an unnamed close finds a dialog it cannot tie to the plan’s window; when the plan’s window is gone, since another window of the same title is never taken for it; and when input the driver could not confirm is not shown by the step’s own read or by a wait, verify or expect step right after it (typing must add the text to an element of the window). The typing check counts the text in every element of the window, not only the field that took the keys, and on macOS a dialog with no owner the OS names counts as the plan’s when it is the one new dialog of the app after input to the plan’s window. An app that brings itself to the front during a step fails that step with that reason. The check cannot tell the app from you: if you click into the plan’s app yourself while it runs, the step fails the same way.The driver refuses or cannot deliver some input in the background:
/dev/uinput.A delivered action is not always a landed one. When cua-driver cannot confirm that background input took effect, for example keys posted to a Windows XAML or console window, the result carries unverified with the driver’s note (wire.md). Read the effect back before relying on it; the input may have landed, so sending it again can type twice.
On macOS windows also lists, in background mode only, the windows on another Space, a desktop or a full-screen app’s, marked space: "other" and never active, so input can name them; in background mode a snapshot of one fails with “is on another Space”, since accessibility reaches a window only on the current Space. Input to such a window is delivered without a Space switch, but its capture can lag what it shows. When the driver cannot confirm such an input’s effect, the result’s unverified adds that background input cannot read the window back there: after typed text, do not type the text again, and before you submit it with Return or Send, ask the user to confirm that the text is there, unless you can read it back; after a key, click, scroll or drag, do not send the input again, and ask the user to confirm its effect unless you can read it back. A confirmed input carries no note. Background input by a title that two windows of one app share, one on this Space and one on another, is refused and names both, since it cannot tell them apart.
focus is the one call that brings a window to the front in background mode.
With [desk] input = "auto", a desk action that background input refuses before anything is sent runs once more in the foreground, its window brought to the front first; any other error stays an error. These refusals take the foreground:
focus, select or action, and a Windows press with no point to click in its window. A press on macOS refuses without the foreground, since its accessibility action may already have run;gui_plan, a macOS menu-bar entry, an element found on the screen, a state read from pixels and an expect checked by vision bring the plan’s window to the front under auto; after any step of a plan took the foreground, the rest of that plan runs in the foreground. The other plan refusals stay refusals.