Codemux

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 dev as 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 x11vnc

Enable it per-workspace via .codemux/config.json:

{
  "sandbox": {
    "virtual_display": true,
    "watch_vnc": true
  }
}

Or globally across every workspace:

export CODEMUX_VIRTUAL_DISPLAY=1
codemux

Now 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:

PersonaDefault policyWhat it means
Humanallow_desktop_gui: trueHost DISPLAY/WAYLAND_DISPLAY inherited. Plain Terminal tabs, the Shell preset, setup scripts, and custom presets without the agent flag.
Agentallow_desktop_gui: falseDesktop 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-open can'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 — makes xdg-open <url> run /usr/bin/true instead of launching a browser
  • MOZ_NO_REMOTE=1 — stops Firefox from handing URLs to an already-running host instance
  • DBUS_SESSION_BUS_ADDRESS=unix:path=/dev/null — fails DBus session-bus connections cleanly without triggering $XDG_RUNTIME_DIR/bus autolaunch
  • XDG_CURRENT_DESKTOP=X-Generic, DE=generic — routes xdg-open onto the generic mimeapps path
  • GTK_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:

  1. Finds a free X display number starting at :1000 (avoids collisions with host session :0-:2 and CI convention :99).
  2. Generates a random MIT-MAGIC-COOKIE-1 and writes an Xauthority file to $XDG_RUNTIME_DIR/codemux/vd-<N>/Xauthority (mode 0600). Only processes that Codemux spawns with the cookie can connect; nothing else on your machine — even under your user account — can peek.
  3. Spawns Xvfb :<N> -screen 0 1920x1080x24 -dpi 96 -noreset -nolisten tcp +extension GLX +extension RANDR -auth <cookie>.
  4. Injects DISPLAY=:<N> and XAUTHORITY=<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:5910

A Codemux-native "watch the agent" pane — noVNC in a browser surface — is on the roadmap.

Configuration reference

.codemux/config.jsonsandbox

FieldTypeDefaultEffect
allow_desktop_guiboolpersona defaultForce-overrides persona. true = host DISPLAY even for agents; false = strip even from human panes. Scoped to one workspace.
virtual_displayboolfalseSpawn a hidden Xvfb for agent panes in this workspace. Requires Xvfb on PATH. Ignored for human panes.
watch_vncboolfalseAlso 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

VariableValuesEffect
CODEMUX_ALLOW_DESKTOP_GUI1 / true / yesForce-allow. Every pane, including agents, inherits host DISPLAY. Useful when you explicitly trust your agents.
0 / false / noForce-deny. Every pane, including plain shells, gets the strip. Useful for kiosk / CI / shared-host setups.
unset / other tokensFall through to the per-session persona default.
CODEMUX_VIRTUAL_DISPLAY1 / true / yesEnables 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/WAYLAND like 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

PlatformEnv-strip (persona-driven)Virtual displayWatch-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-sandbox to the Chromium launch flags. Safe here because bwrap is already sandboxing.
  • Or disable CODEMUX_VIRTUAL_DISPLAY and 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 DISPLAY across the Windows↔WSL2 boundary via WSLENV.
  • macOS via Lima vz VM: per-workspace headless Ubuntu VM, same Linux stack inside.
  • Computer-use MCP tools: take_screenshot, click_at(x, y), type_text driven 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, enable virtual_display: true (the agent runs headless and cannot reach the host display at all) plus bubblewrap installed (wraps the agent in a namespace with tmpfs-shadowed sockets).
  • The MIT-MAGIC-COOKIE-1 xauth cookie is 128 bits of OS entropy (/dev/urandom via rand::rngs::OsRng). Only processes spawned by Codemux with XAUTHORITY pointing 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 (mode 0600), consumed by x11vnc -passwdfile read:<path>.
  • x11vnc is always bound to 127.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 DISPLAY and XAUTHORITY only; 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>-lock files whose PIDs are dead OR whose PIDs point to a process whose /proc/<pid>/comm is not Xvfb. This closes the PID-reuse race where a recycled PID could leave a stale lock forever.