#2224·openclaude

`sharp` moved to devDependencies in 0.23.0 — all image support silently broken for installed users

Author: locoholyCreated Sep 11, 2026Updated Sep 11, 2026

Summary

sharp is a runtime dependency but has been declared only in devDependencies since 0.23.0. It is therefore never installed for anyone who installs the published package, and every image code path fails. The failure is silent and reported as a wrong, misleading message.

This is a regression of the fix in #308 (which closed #303 "can't paste image").

Evidence

Straight from the registry:

$ npm view @gitlawb/openclaude@<v> dependencies.sharp optionalDependencies.sharp devDependencies.sharp

0.21.0  dep=^0.34.5  optional=—  dev=—
0.22.0  dep=^0.34.5  optional=—  dev=—
0.23.0  dep=—        optional=—  dev=^0.34.5   <-- moved here
0.24.0  dep=—        optional=—  dev=^0.34.5
0.30.0  dep=—        optional=—  dev=^0.34.5

allowScripts still carries "[email protected]": true, so the native install step is still expected — only the declaration that would trigger it is gone.

Why it is not caught in development

In a checkout, devDependencies are installed, so sharp resolves and everything works. Only a clean install of the published tarball reproduces it.

Mechanism

getImageProcessor() has two sources:

  1. the native image-processor-napi module — guarded by isInBundledMode(), which requires typeof Bun !== "undefined" and a non-empty Bun.embeddedFiles. bin/openclaude is #!/usr/bin/env node, so under a normal npm install this branch never runs. (In the published bundle that module is a proxy stub with __stub = true anyway.)
  2. await import("sharp") — throws ERR_MODULE_NOT_FOUND, so getImageProcessor() throws ImageProcessorUnavailableError.

maybeResizeAndDownsampleImageBuffer() calls getImageProcessor() as its first statement, unconditionally — before any size or dimension check. So it throws for every image, including ones small enough to need no processing at all.

getImageFromClipboard() wraps the whole body in catch { return null } (src/utils/imagePaste.ts), which swallows that error. PromptInput sees null and shows:

No image found in clipboard. Use ctrl+v to paste images.

The clipboard read itself succeeded. osascript … «class PNGf» exits 0 and writes a valid PNG to /tmp/claude_cli_latest_screenshot.png (verified: 2064×1096, 817 KB). The failure is entirely in the processing step, and the message points the user at the wrong thing.

Affected paths

  • clipboard image paste (getImageFromClipboard)
  • drag-and-drop / pasted image paths (tryReadImageFromPath)
  • reading image files with the file-read tool
  • BMP→PNG conversion for WSL2

Reproduce

bash
npm install -g @gitlawb/[email protected]
node -e 'import("sharp").catch(e => console.log(e.code))'   # ERR_MODULE_NOT_FOUND
# copy any image, start openclaude, press ctrl+v
# -> "No image found in clipboard"

Environment

macOS 15 (Darwin 25.2.0), Node v22.17.0 via nvm, @gitlawb/[email protected] installed globally. Not platform-specific — the manifest is.

Workaround

bash
npm install -g sharp

-g matters: the CLI lives at <prefix>/lib/node_modules/@gitlawb/openclaude/dist/cli.mjs, and Node's resolution walks up to <prefix>/lib<prefix>/lib/node_modules/sharp. No restart needed — getImageProcessor() caches only on success, so the next ctrl+v retries the import. Confirmed working.

Suggested fix

  1. Move sharp back to dependencies (or optionalDependencies + peerDependenciesMeta if optional image support is the intent).
  2. Add it to the clean-install check — scripts/verify-clean-install.ts already exists and would have caught this.
  3. Stop swallowing ImageProcessorUnavailableError in getImageFromClipboard. It has a precise, actionable message ("Image support is not installed. Install sharp …"); surfacing it instead of null would have made this self-diagnosing. The current catch { return null } converts a dependency error into a false statement about the user's clipboard.
  4. Consider having maybeResizeAndDownsampleImageBuffer check dimensions and size before reaching for the processor, so images already under the limits pass through untouched.

Point 3 is the one that cost the most time here. The bug was findable in minutes; the wrong error message sent the search to the clipboard instead of the manifest.

Possibly related: #2198 (image paste broken after 0.21.0). The dependency move lands at 0.23.0, so it does not explain a 0.22.0 failure on its own, but any 0.23.0+ report of broken image paste is explained by this.