Skip to content

64 critical headless gui

Part VII: Ecosystem, Runtime & Agent-Specific | Challenge §64

64. Headless Display and GUI Launch Blocking

Source: Gemini 07_system_physics.md, Antigravity 05_environment_and_execution.md (RA)

Severity: Critical | Frequency: Common | Detectability: Hard | Token Spend: High | Time: Critical | Context: Low

The Problem

Distinct from §45 (OAuth browser flow), many CLI tools launch GUI applications for operations unrelated to authentication: opening a browser to show documentation, launching a file picker, showing a notification dialog, rendering a chart, or using X11 for visualization. In headless environments (containers, CI, SSH sessions without X11 forwarding), these launches either crash immediately (cannot connect to X server), hang indefinitely (waiting for a window to close), or silently do nothing while the tool waits for user interaction that never comes.

# Tool opens browser to show deployment result
$ tool deploy --env prod --open-browser
# In headless container: xdg-open hangs or crashes with:
# "Failed to open URI: No application is registered as handling this file"
# Tool is waiting for the browser to close — hangs forever

# Tool uses X11 for progress visualization
$ tool analyze --chart
# In SSH session without -X: "Error: cannot open display :0"
# Tool crashes with non-zero exit; agent doesn't know if the analyze completed

# macOS `open` command in Linux container:
$ open https://docs.example.com
# "open: command not found" → exit 127 → agent thinks deploy failed

Impact

  • Agent blocked indefinitely waiting for a GUI window to close
  • Critical operation may have completed successfully but tool crashes during the GUI step
  • Exit code reflects GUI failure, not operation result — agent can't determine if work was done
  • No way for agent to know in advance which commands will attempt GUI launches

Solutions

Detect headless environment and skip GUI operations:

import os, sys
def is_headless():
    return (
        not sys.stdout.isatty() or
        os.environ.get('CI') or
        not os.environ.get('DISPLAY') and not os.environ.get('WAYLAND_DISPLAY')
    )

if is_headless():
    # Skip browser launch; emit URL in JSON instead
    return {"ok": True, "data": {"url": url, "opened": False, "open_hint": f"open {url}"}}

Schema declares GUI operations:

{
  "name": "deploy",
  "gui_operations": ["browser_open"],
  "headless_behavior": "emit_url_in_output"
}

Wrap graphical commands in headless fallback:

# Tool wraps GUI launch:
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
    echo '{"url": "'"$URL"'", "note": "open this URL in your browser"}'
else
    xdg-open "$URL"
fi

For framework design: - Framework MUST detect headless environment on startup and set framework.headless = true - Commands that declare gui_operations MUST implement headless fallbacks; framework raises a registration error if headless_behavior is not declared - In headless mode, browser/GUI launch attempts MUST be replaced with URL/path emission in the JSON response rather than blocking

Evaluation

Score Condition
0 Commands launch GUI/browser in non-TTY mode; deadlock or crash in headless environments; no fallback
1 Headless detected; GUI not launched; operation fails silently or with a prose error; no URL/path emitted
2 headless_behavior: emit_url_in_output in schema; URL returned in JSON response instead of opening browser
3 gui_operations declared in schema; framework-level headless detection on startup; registration error if no headless_behavior declared

Check: Run any command with --open-browser (or equivalent) in a headless environment (DISPLAY= unset) — verify it exits cleanly with the URL in the JSON response and does not hang.


Agent Workaround

Signature: stderr shows cannot open display, Failed to open URI, or open: command not found with exit 127; or process hangs after the operation completes

Tier: C (stateful logic; weak models apply the fallback below) Fallback: `CI=true NO_BROWSER=1 BROWSER=true DISPLAY= timeout 60 tool