openai / openai/codex

macOS: Ctrl+V image paste fails for HDR (16-bit float), CMYK and palette clipboard images — root cause in arboard TIFF decoding, with a verified fix

Open
#43,154 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI TUI
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex CLI is running?

codex-cli 0.153.4 (also reproduced on a build of main at 6af3454)

What platform is your computer?

macOS 27.0 (26A5425a), Apple silicon. Earlier reports in #6080 show the same failure on macOS 15.7 and 26.x, so it is not specific to this release.

What terminal emulator and version are you using?

Ghostty and tmux; the terminal is not involved (see root cause).

What issue are you seeing?

Pressing Ctrl+V with an image on the clipboard fails with:

■ Failed to paste image: no image on clipboard: The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format.

Fresh screenshots paste fine. The failure happens when the clipboard image came from Photos (HDR), Preview or a PDF/design app (CMYK), or was re-copied from a clipboard manager such as Raycast Clipboard History (which is what makes it look random). The second error sentence is arboard::Error::ConversionFailure, which is not "no image on clipboard": the pasteboard has an image, Codex just cannot decode it.

Root cause

codex-rs/tui/src/clipboard_paste.rs calls arboard::Clipboard::get_image(). On macOS, arboard (3.6.1) reads only the public.tiff pasteboard flavor and decodes it with the pure-Rust tiff crate (0.10.3 in Cargo.lock):

// arboard src/platform/osx.rs
let image_data = self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF);
let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff);
reader.decode().map_err(|_| Error::ConversionFailure)

Decoding those TIFFs with the exact image/tiff versions from Cargo.lock gives:

Clipboard TIFF tiff decoder result
8-bit RGB(A), LZW/ZIP, tiled OK
16-bit integer RGB(A) OK
32-bit float OK
16-bit float (HDR) Unhandled TIFF sample format 3 for 16 bits
CMYK does not support the color type Unknown(0)
Palette Photometric interpretation RGBPalette ... is unsupported
YCbCr unsupported (rare on the pasteboard)

macOS itself has no trouble with any of these; every other app pastes them. Codex is the only consumer insisting on decoding the TIFF flavor with a limited decoder.

Steps to reproduce

Put a 16-bit float or CMYK TIFF on the pasteboard, then press Ctrl+V in the Codex composer:

magick some.png -depth 16 -define quantum:format=floating-point hdr.tiff   # or: -colorspace CMYK cmyk.tiff
osascript -e 'set the clipboard to (read (POSIX file "/full/path/hdr.tiff") as TIFF picture)'
codex   # Ctrl+V → "Failed to paste image ... could not be converted to the appropriate format"

For a real-world trigger without ImageMagick: copy an HDR photo from Photos, or copy any image from Raycast/Paste clipboard history.

Expected behavior

The image attaches as [Image #1], as it does for a fresh screenshot.

Proposed fix (verified)

On macOS, read the image from the pasteboard directly: take the public.png flavor when present (screenshots provide one), otherwise let AppKit's NSBitmapImageRep convert the TIFF to PNG. AppKit handles every format macOS can display. Keep arboard as the fallback. This is ~35 lines in clipboard_paste.rs plus objc2-app-kit/objc2-foundation deps that are already in Cargo.lock via arboard.

Tested by driving the TUI in tmux and pressing Ctrl+V:

Clipboard 0.153.4 patched
16-bit float (HDR) error [Image #1]
CMYK error [Image #1]
16-bit integer ok [Image #1]
screenshot ok [Image #1]

cargo clippy -p codex-tui --all-targets -- -D warnings, cargo fmt --check and just test -p codex-tui clipboard_paste pass; just bazel-lock-update produces no lockfile diff.

Patch
--- a/codex-rs/tui/Cargo.toml
+++ b/codex-rs/tui/Cargo.toml
@@ -151,6 +151,23 @@ winsplit = "0.1"
 [target.'cfg(not(target_os = "android"))'.dependencies]
 arboard = { workspace = true }
 
+# Read pasteboard images through AppKit so formats the `tiff` decoder behind
+# `arboard` rejects (16-bit float HDR, CMYK, palette) still paste.
+[target.'cfg(target_os = "macos")'.dependencies]
+objc2 = "0.6"
+objc2-app-kit = { version = "0.3", default-features = false, features = [
+    "std",
+    "NSBitmapImageRep",
+    "NSImageRep",
+    "NSPasteboard",
+] }
+objc2-foundation = { version = "0.3", default-features = false, features = [
+    "std",
+    "NSData",
+    "NSDictionary",
+    "NSString",
+] }
+
--- a/codex-rs/tui/src/clipboard_paste.rs
+++ b/codex-rs/tui/src/clipboard_paste.rs
@@ -71,6 +71,13 @@ pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageErro
             img.height()
         );
         img
+    } else if let Some(img) = pasteboard_image() {
+        tracing::debug!(
+            "clipboard image opened from pasteboard: {}x{}",
+            img.width(),
+            img.height()
+        );
+        img
     } else {
         let _span = tracing::debug_span!("get_image").entered();
         let img = cb
@@ -108,6 +115,49 @@ pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageErro
     ))
 }
 
+/// Read image data from the macOS pasteboard as PNG.
+///
+/// `arboard` reads the TIFF flavor and decodes it with the `tiff` crate, which
+/// rejects 16-bit float (HDR), CMYK and palette images that Preview, Photos and
+/// clipboard managers commonly place on the pasteboard, surfacing as
+/// "no image on clipboard". Prefer the PNG flavor when present (screenshots
+/// provide one) and otherwise let AppKit convert whatever TIFF is there.
+#[cfg(target_os = "macos")]
+fn pasteboard_image() -> Option<image::DynamicImage> {
+    use objc2::AnyThread;
+    use objc2_app_kit::NSBitmapImageFileType;
+    use objc2_app_kit::NSBitmapImageRep;
+    use objc2_app_kit::NSPasteboard;
+    use objc2_app_kit::NSPasteboardTypePNG;
+    use objc2_app_kit::NSPasteboardTypeTIFF;
+    use objc2_foundation::NSDictionary;
+
+    let pasteboard = NSPasteboard::generalPasteboard();
+    // SAFETY: `dataForType` and `representationUsingType_properties` are plain
+    // AppKit getters on objects we own for the duration of the call.
+    let png = match unsafe { pasteboard.dataForType(NSPasteboardTypePNG) } {
+        Some(png) => png.to_vec(),
+        None => {
+            let tiff = unsafe { pasteboard.dataForType(NSPasteboardTypeTIFF) }?;
+            let rep = NSBitmapImageRep::initWithData(NSBitmapImageRep::alloc(), &tiff)?;
+            unsafe {
+                rep.representationUsingType_properties(
+                    NSBitmapImageFileType::PNG,
+                    &NSDictionary::new(),
+                )
+            }?
+            .to_vec()
+        }
+    };
+    image::load_from_memory_with_format(&png, image::ImageFormat::Png).ok()
+}
+
+/// Only macOS needs to bypass `arboard`'s TIFF decoding.
+#[cfg(not(any(target_os = "macos", target_os = "android")))]
+fn pasteboard_image() -> Option<image::DynamicImage> {
+    None
+}
+

Related: #6080 (several macOS reports there, including "works for a fresh screenshot, fails when re-selected from clipboard history"), #4366.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in codex-rs/tui/src/clipboard_paste.rs and review the existing paste_image_as_png flow, then inspect the macOS pasteboard handling described in the issue. Use the listed HDR, CMYK and palette reproduction steps, and run cargo clippy -p codex-tui --all-targets -- -D warnings, cargo fmt --check, and just test -p codex-tui clipboard_paste; done means each clipboard image attaches as [Image #1].

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, rust
Domain
cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.