codex - 💡(How to fix) Fix tui: env-gated external clipboard reader for headless environments

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

const CODEX_CLIPBOARD_READER_ENV: &str = "CODEX_CLIPBOARD_READER";

pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> { if let Some(cmd) = std::env::var_os(CODEX_CLIPBOARD_READER_ENV) { return paste_image_via_external_reader(&cmd); } // ... existing arboard + WSL fallback, untouched ... }

fn paste_image_via_external_reader( cmd: &std::ffi::OsStr, ) -> Result<(PathBuf, PastedImageInfo), PasteImageError> { let output = std::process::Command::new(cmd) .stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) .output() .map_err(|e| PasteImageError::ClipboardUnavailable( format!("{CODEX_CLIPBOARD_READER_ENV} failed to spawn: {e}")))?; if !output.status.success() || output.stdout.is_empty() { return Err(PasteImageError::NoImage( format!("{CODEX_CLIPBOARD_READER_ENV} reported no image"))); } // write stdout to a tempfile, image::image_dimensions, return (PathBuf, PastedImageInfo) // mirroring how try_wsl_clipboard_fallback returns a path + dimensions }

Root Cause

Failed to paste image: clipboard unavailable: Unknown error while interacting with the clipboard: X11 server connection timed out because it was unreachable

Fix Action

Fix / Workaround

  • The arboard read path (paste_image_as_png) is untouched.
  • try_wsl_clipboard_fallback and is_probably_wsl are untouched.
  • The arboard file_list() "paste a file URI" workaround (clipboard_paste.rs:59-67) stays the same — it already serves the case where arboard can reach a display.
  • No new error variants, no new dependencies, no new config schema.

Code Example

Failed to paste image: clipboard unavailable: Unknown error while interacting with the clipboard: X11 server connection timed out because it was unreachable

---

const CODEX_CLIPBOARD_READER_ENV: &str = "CODEX_CLIPBOARD_READER";

pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
    if let Some(cmd) = std::env::var_os(CODEX_CLIPBOARD_READER_ENV) {
        return paste_image_via_external_reader(&cmd);
    }
    // ... existing arboard + WSL fallback, untouched ...
}

fn paste_image_via_external_reader(
    cmd: &std::ffi::OsStr,
) -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
    let output = std::process::Command::new(cmd)
        .stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped())
        .output()
        .map_err(|e| PasteImageError::ClipboardUnavailable(
            format!("{CODEX_CLIPBOARD_READER_ENV} failed to spawn: {e}")))?;
    if !output.status.success() || output.stdout.is_empty() {
        return Err(PasteImageError::NoImage(
            format!("{CODEX_CLIPBOARD_READER_ENV} reported no image")));
    }
    // write stdout to a tempfile, image::image_dimensions, return (PathBuf, PastedImageInfo)
    // mirroring how try_wsl_clipboard_fallback returns a path + dimensions
}
RAW_BUFFERClick to expand / collapse

Problem

codex-rs/tui/src/clipboard_paste.rs reads the clipboard via arboard, which talks X11 / Wayland directly. In environments without a reachable display server, arboard::Clipboard::new() either fails immediately or, when DISPLAY happens to be inherited and points at an unreachable server, surfaces a confusing X11 timeout:

Failed to paste image: clipboard unavailable: Unknown error while interacting with the clipboard: X11 server connection timed out because it was unreachable

The most common case in practice is running Codex inside a Linux container on a macOS host (Docker Desktop, OrbStack, colima, etc.). The host clipboard is a real NSPasteboard with a real image on it, but arboard inside the Linux container has no X11 / Wayland server to connect to and no library-level path to the host's native pasteboard. The same wall is hit by Codex running in a Linux container on a Windows host outside WSL, by headless Linux runners, by locked-down remote dev sandboxes, and by SSH sessions with no display forwarding.

Existing precedent

Codex already concedes this point for one specific host/container combination: try_wsl_clipboard_fallback (clipboard_paste.rs:159) runs after arboard errors and shells out to powershell.exe Get-Clipboard -Format Image, bypassing arboard to read the actual Windows clipboard via WSL interop. WSL is just the case where Codex happens to ship a built-in escape hatch; the underlying pattern — the container can't see the host's clipboard through arboard, but an out-of-band reader on the host can — is identical for macOS + Docker Desktop, Windows + Docker outside WSL, headless Linux runners, and every other "Codex in a Linux container with no display" deployment. There is currently no way for those deployments to plug in their own reader the way WSL gets PowerShell.

Proposal

Generalise that escape hatch from one hardcoded environment to "any environment, opt-in via env var." If CODEX_CLIPBOARD_READER is set, Codex runs that command and treats its stdout as PNG bytes; if it exits non-zero or stdout is empty, Codex behaves exactly as if no image were on the clipboard. The library path stays the default; the WSL detection stays untouched.

Why opt-in (not auto-detect)

Heuristics for "we are in a container with no display" are fragile (cgroups, /proc/1/cgroup, .dockerenv are all bypassable or inaccurate). The operator setting the env var is the most reliable signal that "the library path won't work; use this command instead."

Sketch

One new branch at the top of paste_image_to_temp_png() (clipboard_paste.rs:121), before the existing arboard attempt:

const CODEX_CLIPBOARD_READER_ENV: &str = "CODEX_CLIPBOARD_READER";

pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
    if let Some(cmd) = std::env::var_os(CODEX_CLIPBOARD_READER_ENV) {
        return paste_image_via_external_reader(&cmd);
    }
    // ... existing arboard + WSL fallback, untouched ...
}

fn paste_image_via_external_reader(
    cmd: &std::ffi::OsStr,
) -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
    let output = std::process::Command::new(cmd)
        .stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped())
        .output()
        .map_err(|e| PasteImageError::ClipboardUnavailable(
            format!("{CODEX_CLIPBOARD_READER_ENV} failed to spawn: {e}")))?;
    if !output.status.success() || output.stdout.is_empty() {
        return Err(PasteImageError::NoImage(
            format!("{CODEX_CLIPBOARD_READER_ENV} reported no image")));
    }
    // write stdout to a tempfile, image::image_dimensions, return (PathBuf, PastedImageInfo)
    // mirroring how try_wsl_clipboard_fallback returns a path + dimensions
}

The command is passed straight to Command::new — no shell parsing, no argument vector. Users who need arguments wrap it in a script. Errors reuse existing PasteImageError variants; no new error cases.

Behaviour matrix

CODEX_CLIPBOARD_READERResult
unsetidentical to today (arboard, with WSL fallback on Linux)
set, exits 0 with PNG bytes on stdoutimage attached
set, exits non-zero"no image on clipboard" (no error spew)
set, command missing / not executable"clipboard unavailable: <spawn error>"

Why stdout PNG (not a path like WSL)

The WSL fallback returns a Windows path the PowerShell side wrote. That works because both sides see the same filesystem via /mnt. In the generic case there's no shared mount we can assume — bytes are the universal contract. The diff stays small because we write stdout to a temp file and reuse image::image_dimensions exactly like the WSL branch does (clipboard_paste.rs:179).

What this doesn't change

  • The arboard read path (paste_image_as_png) is untouched.
  • try_wsl_clipboard_fallback and is_probably_wsl are untouched.
  • The arboard file_list() "paste a file URI" workaround (clipboard_paste.rs:59-67) stays the same — it already serves the case where arboard can reach a display.
  • No new error variants, no new dependencies, no new config schema.

Security

The env var holds a command path passed directly to Command::new. No shell parsing → no quoting / argument-injection surface, same shape as $EDITOR / $PAGER. The reader runs with the calling process's privileges — the operator setting the env var is, by definition, authorised to invoke it. Stdout is treated as opaque PNG bytes; the existing image-decode path rejects malformed input.

Alternatives considered

  • Config-file flag (~/.codex/config.toml). Persistent and discoverable, but introduces a new config schema and doesn't match the existing per-process precedent. Can come later without breaking the env var.
  • Pluggable Clipboard trait. Cleaner architecturally, larger diff; not justified for one extra branch.
  • Auto-detect containerised → shell-out. Heuristics are fragile and the right reader varies by deployment.
  • Drop arboard, always shell out. Non-starter for the majority of users with a working library path.

Questions for maintainers

  1. Appetite for an env-gated branch vs holding out for a config-file approach?
  2. Naming — CODEX_CLIPBOARD_READER matches the existing CODEX_* family; happy to bikeshed.
  3. Position relative to WSL — proposing env-first (skip arboard entirely when set) so deployments that already know the library path won't work don't pay the X11 timeout. Open to running env after arboard if preferred.
  4. Arguments — punting on CODEX_CLIPBOARD_READER_ARGS for now (wrap in a script); fine to add later if asked.
  5. Write direction — out of scope for this PR. Worth a separate issue if anyone cares.

Happy to send a PR if there's interest; can include unit tests covering the success / empty-stdout / non-zero-exit / spawn-error branches and a small repro for the docs.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

codex - 💡(How to fix) Fix tui: env-gated external clipboard reader for headless environments