Hmbown/CodeWhale · error · ExecError

no X11 ($DISPLAY) or Wayland ($WAYLAND_DISPLAY) session…

Error message

no X11 ($DISPLAY) or Wayland ($WAYLAND_DISPLAY) session visible to this process

What it means

`shotTool` picks a screenshot tool based on the detected session ("wayland" or "x11"). If neither $WAYLAND_DISPLAY nor $DISPLAY is set/visible to this process, no screenshot mechanism can work, so it throws instead of guessing. This is an environment-detection failure, not a tool failure.

Solutions

  1. Run the backend from within a graphical session shell so DISPLAY or WAYLAND_DISPLAY is set.
  2. For SSH, use `ssh -X` or run the automation on the machine with the GUI session.
  3. In containers/systemd, pass through the display env and socket (e.g. -e WAYLAND_DISPLAY -v $XDG_RUNTIME_DIR:/tmp/xdg).
  4. Verify with `echo $DISPLAY $WAYLAND_DISPLAY` in the same environment the backend runs in.

Example fix

// before
// CI container: neither DISPLAY nor WAYLAND_DISPLAY set -> throws
// after
// docker run -e WAYLAND_DISPLAY=$WAYLAND_DISPLAY -v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/tmp/$WAYLAND_DISPLAY ...
await backend.screenshot({ file: "/tmp/s.png" });
Defensive patterns

Strategy: validation

Validate before calling

if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
  throw new Error("no graphical session: set DISPLAY or WAYLAND_DISPLAY");

Type guard

const hasGraphicalSession = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);

Try / catch

try { await backend.screenshot({ file }); }
catch (e) {
  if (String(e.message).includes("no X11") && String(e.message).includes("Wayland"))
    throw new Error("Run inside a graphical session or export DISPLAY/WAYLAND_DISPLAY");
  throw e;
}

Prevention

When it happens

Trigger: Calling any screenshot/capture API on the Linux backend when the process has no DISPLAY or WAYLAND_DISPLAY environment variable (e.g. running via SSH without X forwarding, a systemd service, a cron job, or a container without the socket mounted) (backends/linux.mjs:124).

Common situations: Running the agent from a headless shell/CI; SSH session without `ssh -X`; Wayland user session but process launched before the session env is imported; container without passing through the display socket and env.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/4e93682db4d454cf. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:124

    }
    tools.pyatspi = tools.python3 && (await run("python3", ["-c", "import pyatspi"], { timeoutMs: 10_000 })).code === 0;
    throwIfAborted();
    probed = true;
    return session;
  }

  function need(tool, purpose) {
    if (!tools[tool]) throw new ExecError(`linux backend needs "${tool}" for ${purpose} — install it and retry`);
  }

  async function shotTool() {
    if (session === "wayland") { need("grim", "screenshots on Wayland"); return { cmd: "grim", base: [] }; }
    if (session === "x11") {
      if (tools.scrot) return { cmd: "scrot", base: ["-z"] };
      need("import", "screenshots on X11 (imagemagick)");
      return { cmd: "import", base: ["-window", "root"] };
    }
    throw new ExecError("no X11 ($DISPLAY) or Wayland ($WAYLAND_DISPLAY) session visible to this process");
  }

  /** Capture a PNG to `file`, optionally cropped to region [x,y,w,h] points. */
  async function takeShot(file, region) {
    outputPath(file);
    const { cmd, base } = await shotTool();
    let args = [...base];
    if (cmd === "grim") {
      if (region) args.push("-g", `${Math.round(region[0])},${Math.round(region[1])} ${Math.round(region[2])}x${Math.round(region[3])}`);
      args.push(file);
    } else if (cmd === "scrot") {
      if (region) args.push("-a", `${Math.round(region[0])},${Math.round(region[1])},${Math.round(region[2])},${Math.round(region[3])}`);
      args.push(file);
    } else {
      if (region) args.push("-crop", `${Math.round(region[2])}x${Math.round(region[3])}+${Math.round(region[0])}+${Math.round(region[1])}`);
      args.push(file);
    }
    const r = await run(cmd, args, { timeoutMs: 10_000 });

View on GitHub (pinned to 73e0f67d83)