Display Isolation
Agent sessions cannot pop windows on your desktop; plain terminals work like any other terminal. Optional virtual display lets agents test GUI apps headlessly.
Display Isolation
Codemux splits display access by persona — who is driving the keystrokes in a pane:
- Human panes — the plain Terminal tab, the built-in Shell preset, and any custom preset not flagged as an agent. You typed the command; you expect it to work exactly like kitty, alacritty, or ghostty.
DISPLAY,WAYLAND_DISPLAY, DBus, and all desktop env vars pass through.npm run tauri dev,firefox,xdg-open ./README.md— all work. - Agent panes — the built-in Claude Code / Codex / OpenCode / Gemini / Pi presets, any pane spawned by the OpenFlow orchestrator, and any custom preset you've flagged as an agent. Desktop env is stripped; xdg-open, DBus auto-launch, and single-instance Firefox handoff are neutralized. If the agent runs
npm run tauri devas a tool call, the GTK init fails cleanly — the window does not pop on your real desktop.
Setup scripts (the Run button, teardown scripts, worktree-includes) are user-initiated and inherit your full environment — docker compose up, notify-send, xdg-open docs, etc. all work.
Quick start
If you just want to stop agent popups — nothing to do. Built-in agent presets carry persona: Agent by default, so Claude / Codex / OpenCode / Gemini / Pi panes already strip display env.
If you want to run GUI apps from your own terminal — nothing to do. Plain Terminal tabs and the Shell preset carry persona: Human and keep the host display. Just open a Terminal and run your command.
If you want agents to run headed GUI apps invisibly (virtual display — opt-in):
Install the tooling:
# Arch / Omarchy
sudo pacman -S xorg-server-xvfb xorg-xauth x11vnc
# Debian / Ubuntu
sudo apt install xvfb xauth x11vnc
# Fedora
sudo dnf install xorg-x11-server-Xvfb xorg-x11-xauth x11vncEnable it per-workspace via .codemux/config.json:
{
"sandbox": {
"virtual_display": true,
"watch_vnc": true
}
}Or globally across every workspace:
export CODEMUX_VIRTUAL_DISPLAY=1
codemuxNow when an agent runs electron ., chromium --headed, or npx playwright test --headed, the windows open on a hidden display the user never sees. Human panes ignore virtual_display — your real desktop is already there.
If you want a custom preset to behave like an agent — open Settings → Presets, pick the preset, and set "Keystroke driver" to Agent (no desktop GUI). Built-in presets have their persona locked (Claude etc. are always Agent; Shell is always Human); custom presets are editable.
The persona model
Every terminal session carries a persona field. The value determines which ExecutionPolicy the PTY gets spawned with:
| Persona | Default policy | What it means |
|---|---|---|
Human | allow_desktop_gui: true | Host DISPLAY/WAYLAND_DISPLAY inherited. Plain Terminal tabs, the Shell preset, setup scripts, and custom presets without the agent flag. |
Agent | allow_desktop_gui: false | Desktop env stripped + neutralizers injected. Built-in AI CLI presets (Claude, Codex, OpenCode, Gemini, Pi), OpenFlow orchestrator panes, custom presets flagged as agent. |
Why a split? Agents driving a PTY can type npm run tauri dev as a tool call — the command inherits the PTY env, so if the pane has DISPLAY, the agent's GUI window pops on your real desktop. That's what we want to stop. A human in a Terminal tab, running the same command, explicitly wanted that window.
What gets stripped (Agent persona only):
- X11 / Wayland:
DISPLAY,WAYLAND_DISPLAY,WAYLAND_SOCKET,XAUTHORITY - DE detection (so
xdg-opencan't route through DBus portals):XDG_CURRENT_DESKTOP,XDG_SESSION_TYPE,XDG_SESSION_DESKTOP,DESKTOP_SESSION,GNOME_DESKTOP_SESSION_ID - Compositor reach-back:
HYPRLAND_INSTANCE_SIGNATURE,HYPRCURSOR_THEME,HYPRCURSOR_SIZE,AQ_DRM_DEVICES,SWAYSOCK - DBus:
DBUS_SESSION_BUS_ADDRESS - Toolkit selectors:
GDK_BACKEND,QT_QPA_PLATFORM,QT_QPA_PLATFORMTHEME,CLUTTER_BACKEND,SDL_VIDEODRIVER,NIXOS_OZONE_WL,MOZ_ENABLE_WAYLAND,MOZ_X11_EGL - Portal bridges:
GTK_USE_PORTAL - Startup notification:
DESKTOP_STARTUP_ID
What gets injected (Agent persona only, to defeat escape hatches):
BROWSER=true— makesxdg-open <url>run/usr/bin/trueinstead of launching a browserMOZ_NO_REMOTE=1— stops Firefox from handing URLs to an already-running host instanceDBUS_SESSION_BUS_ADDRESS=unix:path=/dev/null— fails DBus session-bus connections cleanly without triggering$XDG_RUNTIME_DIR/busautolaunchXDG_CURRENT_DESKTOP=X-Generic,DE=generic— routesxdg-openonto the generic mimeapps pathGTK_USE_PORTAL=0,GIO_USE_VFS=local,NO_AT_BRIDGE=1— blocks GTK portal routing, gvfsd DBus activation, and at-spi2 bus pings
On Linux with Bubblewrap installed (pacman -S bubblewrap / apt install bubblewrap), agent spawns additionally wrap in bwrap with tmpfs-shadowed /tmp/.X11-unix and $XDG_RUNTIME_DIR — even a determined agent that guesses socket paths cannot reach the host display.
Virtual display (optional, agent panes only)
When a workspace sets sandbox.virtual_display: true, Codemux:
- Finds a free X display number starting at
:1000(avoids collisions with host session:0-:2and CI convention:99). - Generates a random MIT-MAGIC-COOKIE-1 and writes an Xauthority file to
$XDG_RUNTIME_DIR/codemux/vd-<N>/Xauthority(mode0600). Only processes that Codemux spawns with the cookie can connect; nothing else on your machine — even under your user account — can peek. - Spawns
Xvfb :<N> -screen 0 1920x1080x24 -dpi 96 -noreset -nolisten tcp +extension GLX +extension RANDR -auth <cookie>. - Injects
DISPLAY=:<N>andXAUTHORITY=<cookie-path>into every agent PTY in that workspace.
The agent runs with what looks like a normal desktop — window manager, compositing, GL, RANDR all work. Windows open into RAM nobody looks at. Human panes in the same workspace still see the host display; virtual display only activates for Agent-persona sessions.
Watch the agent
When a workspace also sets sandbox.watch_vnc: true, Codemux spawns x11vnc attached to the hidden display, bound to 127.0.0.1:<port> (first free port starting at 5910), with a random per-display password. Connect with any VNC client to see what the agent is doing:
# Find the port
ss -tlnp | grep x11vnc
# Connect
vncviewer localhost:5910A Codemux-native "watch the agent" pane — noVNC in a browser surface — is on the roadmap.
Configuration reference
.codemux/config.json → sandbox
| Field | Type | Default | Effect |
|---|---|---|---|
allow_desktop_gui | bool | persona default | Force-overrides persona. true = host DISPLAY even for agents; false = strip even from human panes. Scoped to one workspace. |
virtual_display | bool | false | Spawn a hidden Xvfb for agent panes in this workspace. Requires Xvfb on PATH. Ignored for human panes. |
watch_vnc | bool | false | Also spawn x11vnc so you can view the hidden display. Requires x11vnc on PATH. |
All fields are optional. Absent fields fall through to the per-session persona default.
Environment variables
| Variable | Values | Effect |
|---|---|---|
CODEMUX_ALLOW_DESKTOP_GUI | 1 / true / yes | Force-allow. Every pane, including agents, inherits host DISPLAY. Useful when you explicitly trust your agents. |
0 / false / no | Force-deny. Every pane, including plain shells, gets the strip. Useful for kiosk / CI / shared-host setups. | |
| unset / other tokens | Fall through to the per-session persona default. | |
CODEMUX_VIRTUAL_DISPLAY | 1 / true / yes | Enables virtual_display for every agent session globally. Per-workspace config still wins. Ignored for human panes. |
Per-preset persona
In Settings → Presets, each preset has a "Keystroke driver" field:
- Human (full desktop access) — pane inherits host
DISPLAY/WAYLANDlike any other terminal. Use for custom shells, build scripts, dev servers, anything you drive yourself. - Agent (no desktop GUI) — pane strips desktop env. Use for any CLI that is itself an AI agent driving keystrokes in the pane.
Built-in presets have fixed personas and the field is read-only — Claude / Codex / OpenCode / Gemini / Pi are always Agent; Shell is always Human. Custom presets default to Human and can be flipped.
Platform support
| Platform | Env-strip (persona-driven) | Virtual display | Watch-via-VNC |
|---|---|---|---|
| Linux (with Xvfb + xauth + x11vnc) | ✅ | ✅ | ✅ |
| Linux (Xvfb only) | ✅ | ✅ (no auth) | — |
| Linux (no X tools) | ✅ | — | — |
| macOS | ✅ (X11 apps) | planned (Lima VM) | planned |
| Windows | ✅ (no-op — host doesn't use these vars) | planned (WSL2 + WSLg) | planned |
Env-strip is safe on every platform. On Windows the GUI key list is a no-op because Windows doesn't use those env vars — infrastructure is in place for when WSL2-based virtual displays ship.
If Xvfb or x11vnc is missing on Linux, Codemux falls back gracefully: agent still spawns, workspace still works, a log line explains what degraded. No crash, no blocked workspace creation.
Troubleshooting
My own npm run tauri dev fails with "Failed to initialize GTK"
You probably have CODEMUX_ALLOW_DESKTOP_GUI=0 set, which force-denies display access for every pane including plain shells. Unset it, or set it to 1 if you want the opposite extreme. Alternatively, check your workspace's .codemux/config.json for sandbox.allow_desktop_gui: false — that scoped override denies everything in that workspace. Remove the line to restore the persona default.
Agent can't launch a GUI app (but I want it to test one)
Enable the virtual display for that workspace:
{
"sandbox": { "virtual_display": true }
}Install xvfb if you haven't. Agent tool calls like npm run tauri dev will now render into the hidden display. Add watch_vnc: true to see what the agent sees.
"Virtual display requested but unavailable" in logs
Xvfb is not installed or not on PATH. Install it (see Quick start). Codemux will fall back to plain env-strip — agent runs headless, popups still blocked.
Playwright tests can't find a display
Ensure virtual_display: true is set for your workspace and Xvfb is installed. Run ss -tlnp | grep 6000 or ls /tmp/.X11-unix/ inside a Codemux terminal to confirm an Xvfb socket is present. If Playwright defaults to headless, you may need to explicitly pass --headed for it to hit the virtual display.
Chromium / Electron errors about --no-sandbox
Chromium's own sandbox uses user namespaces. When Codemux runs Chromium inside bubblewrap (Linux hardened mode), nested namespaces sometimes fail. Workarounds:
- Add
--no-sandboxto the Chromium launch flags. Safe here because bwrap is already sandboxing. - Or disable
CODEMUX_VIRTUAL_DISPLAYand run Chromium directly on the host display from a Human-persona pane.
I see "connection lost" spam from x11vnc on workspace close
This shouldn't happen — Codemux kills x11vnc before Xvfb. If you see it anyway, please file an issue with your x11vnc version (x11vnc --help | head -1).
systemd-tmpfiles cleaned up my xauth file
Shouldn't happen — cookies live under $XDG_RUNTIME_DIR (which is not subject to /tmp ageing). If you're running Codemux in an environment without XDG_RUNTIME_DIR (some CI images, chroot-based containers without pam_systemd), the cookie falls back to /tmp/codemux-vd-<N>/Xauthority, which is subject to systemd-tmpfiles' 10-day default ageing. Either set XDG_RUNTIME_DIR or add an /etc/tmpfiles.d/codemux.conf exclusion.
wl-copy / nvim "+y fails from an agent pane
Wayland clipboard tools need WAYLAND_DISPLAY, which agent panes strip. If you need clipboard access in an agent-driven pane, either change the preset's persona to Human in Settings, or set the workspace's .codemux/config.json sandbox.allow_desktop_gui: true.
Roadmap
- "Watch the agent" pane inside Codemux: noVNC embedded in a browser surface, discoverable from the workspace sidebar. No external viewer needed.
- Windows via WSL2 + WSLg: detect WSL2, spawn Xvfb inside the distro, propagate
DISPLAYacross the Windows↔WSL2 boundary viaWSLENV. - macOS via Lima
vzVM: per-workspace headless Ubuntu VM, same Linux stack inside. - Computer-use MCP tools:
take_screenshot,click_at(x, y),type_textdriven by the agent — agents drive real apps like humans, the user watches via the VNC pane.
The virtual-display infrastructure is the foundation for all of the above. See the sandboxing plan doc for current status.
Security notes
- The persona split is a UX feature with a security side-effect, not a containment boundary for adversarial agents. A determined process inside an Agent pane can probe socket paths directly. Real OS-level sandboxing (seccomp, Landlock, nested user namespaces) is future work.
- Env-strip stops ~95% of accidental GUI popups from agent-driven
npm run dev,electron .,playwright --headed, etc. For real containment, enablevirtual_display: true(the agent runs headless and cannot reach the host display at all) plusbubblewrapinstalled (wraps the agent in a namespace with tmpfs-shadowed sockets). - The MIT-MAGIC-COOKIE-1 xauth cookie is 128 bits of OS entropy (
/dev/urandomviarand::rngs::OsRng). Only processes spawned by Codemux withXAUTHORITYpointing at it can connect. - The VNC password is separate from the xauth cookie — 128 bits of OS entropy, written to
$XDG_RUNTIME_DIR/codemux/vd-<N>/vncpasswd(mode0600), consumed byx11vnc -passwdfile read:<path>. x11vncis always bound to127.0.0.1— never to a network-accessible interface.- Neither cookie nor password ever appears in environment variables passed to the agent. The agent sees
DISPLAYandXAUTHORITYonly; it cannot read the VNC password unless it explicitly reads the file on disk (which requires the same UID, same filesystem view). - Orphan sweep on startup only deletes
.X<n>-lockfiles whose PIDs are dead OR whose PIDs point to a process whose/proc/<pid>/commis notXvfb. This closes the PID-reuse race where a recycled PID could leave a stale lock forever.