tui: env-gated external clipboard reader for headless environments
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_READER | Result |
|---|---|
| unset | identical to today (arboard, with WSL fallback on Linux) |
| set, exits 0 with PNG bytes on stdout | image 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_fallbackandis_probably_wslare 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
Clipboardtrait. 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
- Appetite for an env-gated branch vs holding out for a config-file approach?
- Naming —
CODEX_CLIPBOARD_READERmatches the existingCODEX_*family; happy to bikeshed. - 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.
- Arguments — punting on
CODEX_CLIPBOARD_READER_ARGSfor now (wrap in a script); fine to add later if asked. - 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗