Skip to content

Image processor native module

This page is an artifact-level reverse-engineering writeup of image-processor.node, the Bun-embedded native addon behind Claude Code’s sharp-compatible image façade. It combines the exact Linux-x64 binary surface with the readable JavaScript resize/re-encode policy. Native scheduling and clipboard implementation details remain opaque unless directly observed.

The .node binary is regenerated locally by scripts/extract-claude-code-final-artifacts.mjs (now in the final-keep allow-list) and is held outside git via *.node in .gitignore. This page works off the linux-x64 build of @anthropic-ai/claude-code@2.1.215. Its SHA-256 is unchanged from the prior 2.1.143 snapshot.

ELF metadata

ItemValue
Pathclaude-code-pkg/image-processor.node
TypeELF 64-bit LSB shared object, x86-64, dynamically linked, stripped
Size1,464,760 bytes (1.4 MiB)
SHA-25637bec7de530676e3dfe963d34a824b49191595809a8072348a2ef4571f1e5f4d
Bun graph path/$bunfs/root/image-processor.node
Defined dynamic symbolnapi_register_module_v1 (single export — standard Node-API entrypoint)

ABI surface

LayerEvidence
Dynamic library deps (readelf -d)libm.so.6, libpthread.so.0, libc.so.6, libdl.so.2, ld-linux-x86-64.so.2no libpng / libjpeg / libwebp / libheif / libvips / libX11 / libwayland-client. All image codecs are statically linked.
Minimum glibcGLIBC_2.17 (driven by clock_gettime); CentOS 7 / Ubuntu 14.04+ compatible.
Rust toolchainSource paths embedded as panic locations carry the rustc commit 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf.
Stack / TLSNon-executable stack (GNU_STACK empty perms), GNU_RELRO set on .data.rel.ro.
N-API surface43 unique undefined napi_* imports: napi_create_threadsafe_function, napi_define_class, napi_create_promise, napi_resolve_deferred, napi_reject_deferred, napi_create_external_buffer, napi_create_buffer, napi_create_function, napi_get_cb_info, full property / reference / typeof / coercion helpers.

Source provenance — statically linked Rust crates

Rust embeds the source file path of each function into panic locations; the binary therefore tells us exactly which crates and versions are linked in. Recovered set:

crateVersionRole
napi2.16.17napi-rs binding layer (Promise / class / threadsafe-fn / external buffer plumbing).
image0.25.10Top-level pixel buffer + format dispatch + metadata; defines the Rgb8 / Rgba8 / L8 / L16 / Rgb16 / Rgba16 / Rgb32F / Rgba32F layouts and a Limits guard.
png0.18.1PNG decoder / encoder; the PNG chunk names bKGD, cHRM, gAMA, iCCP, eXIf, acTL, fcTL, fdAT, mDCV, cLLI, cICP, tRNS, tEXt, zTXt, sBIT are visible in .rodata.
image-webp0.2.4WebP decoder + encoder, including VP8 lossy and the lossless transforms.
zune-jpeg0.5.13JPEG decoder; marker table (SOF, DHT, DAC, RST, SOI, EOI, SOS, DQT, DNL, DRI, APP, COM) embedded.
zune-core0.5.1Shared zune-* utilities.
flate21.1.9High-level DEFLATE façade.
fdeflate0.3.7Fast DEFLATE (PNG zlib hot path).
miniz_oxide0.8.9Pure-Rust DEFLATE fallback.
tokio1.50.0Async-runtime code is linked; the stripped artifact does not establish which image operations it schedules.
once_cell1.21.4Lazy/global storage support; the concrete stored values are not recovered here.

addr2line-0.25.1 / gimli-0.32.3 / hashbrown-0.16.1 / rustc-demangle-0.1.26 are present too but only service Rust panic backtraces; they are not part of the image pipeline.

Implication: the artifact is not dynamically linked to sharp/libvips or external PNG/JPEG/WebP shared libraries. It contains napi-rs and Rust image-codec code in one addon. That dependency inventory does not by itself establish every accepted format. In a safe exact-artifact probe, GIF input rejected with The image format Gif is not supported; PNG/JPEG/WebP are the formats exercised by the readable Claude Code transformation façade.

JavaScript surface

Module exports (registered by napi_register_module_v1)

Recovered from error strings such as Failed to register function \process_image`/`has_clipboard_image`/`read_clipboard_image`andFailed to construct class `ImageProcessor“:

ExportKindLinux behaviour
processImage(input)standalone fnReturned a Promise in an exact-artifact probe. Which native executor performs its work is not established.
hasClipboardImage()standalone fnExport observed. Its Linux return behavior was not exercised in this audit.
readClipboardImage()standalone fnExport observed. Its Linux return/error behavior was not exercised in this audit.
ImageProcessorclass/exportExport observed; the readable façade consumes instances returned by processImage.

ImageProcessor class

class ImageProcessor {
constructor(input)
metadata() // → { width, height, format, … }
resize(width, height, opts) // chainable, mutates state
jpeg(quality) // set output codec
png(opts) // set output codec
webp(quality) // set output codec
toBuffer() -> Promise<Buffer> // consume: execute pipeline, returns encoded bytes
dispose() // idempotent explicit cleanup
}

The binary contains the error string ImageProcessor already consumed (toBuffer/dispose was called), consistent with a one-shot native object. The readable façade independently guarantees that its own metadata() and toBuffer() paths call dispose() in finally.

Call path from cli.renamed.js

The JS-side façade sharp() at cli.renamed.js#L272868 lazily loads the addon, buffers resize / jpeg / png / webp calls into a closure queue, then on toBuffer() awaits processImage(input) and replays the queue. metadata() does not replay transformations. Both terminal operations dispose the returned native object in finally, including when metadata lookup or encoding throws.

The shim that brings the addon into the bundle is the Bun CJS wrapper at claude-code-pkg/image-processor.js; it is the JS half of the require("/$bunfs/root/image-processor.node") bridge.

Concurrency evidence and limit

The addon imports Promise/deferred and threadsafe-function N-API calls and contains Tokio code. processImage() returned a Promise in the exact-artifact probe, and the JS façade awaits both it and toBuffer(). These facts establish an asynchronous JavaScript contract, not the native execution schedule. Without disassembly or tracing, this audit cannot prove:

  • that every decode or encode runs on a Tokio worker;
  • that processImage() and toBuffer() use separate worker jobs;
  • that CPU-heavy work can never execute on the JS thread;
  • how cancellation, panic, or disposal interacts with in-flight native work.

Strings such as Panic in async function, Resolve deferred value failed, and threadsafe-function imports are possible boundary machinery, not a recovered call path.

Source-confirmed transformation policy

The high-level resize policy is readable in oit() around cli.renamed.js:279205:

  1. Reject an empty input and inspect native metadata.
  2. Return the original bytes when raw size and dimensions are already within the caller’s limits.
  3. For an over-byte-limit PNG, first try PNG compression level 9 with palette conversion.
  4. Try JPEG qualities 80, 60, 40, then 20 when re-encoding is needed.
  5. If dimensions exceed the API bounds, scale inside the maximum width/height with withoutEnlargement: true, then repeat PNG/JPEG compression as needed.
  6. As a last encoding attempt, cap width at 1,000 pixels and emit JPEG quality 20.
  7. If native processing fails, preserve the original only when header-derived dimensions and base64 size are still safe; otherwise return a text-facing resize/compression error rather than forwarding an unsafe attachment.

This policy is JavaScript orchestration. It does not reveal native interpolation kernels, codec defaults omitted from calls, memory ownership, or task scheduling.

Defensive limits and error surfaces

GuardSource string (excerpt)Meaning
Dimension capsImage width N greater than width limit M, Image height N greater than height limit M, Image size exceeds limit, width and height must be >= 1 and <= 65535image::Limits is enabled with non-trivial bounds; rejects oversize inputs without allocating.
Memory budgetMemoryLimitExceededDecoder allocates against a Limits.max_alloc and fails fast.
Decompression budgetOut of decompression space. Try with a larger limit.PNG zlib bomb defence.
Stream integrityCorrupt deflate stream, CrcMismatch, BadZlibHeader, DistanceTooFarBack, WrongChecksum, Invalid PNG signature., Invalid WebP signature:Both fdeflate and miniz_oxide reject malformed inputs; the encoders refuse to write with can't write indexed image without palette, the dimension and position go over the frame boundaries, etc.
Metadata capsICC profile too large, Unable to compress text metadata, The text metadata cannot be encoded into valid ISO 8859-1PNG tEXt / iCCP guards.
JPEG feature gatingThe library cannot yet decode images encoded using Extended Sequential Huffman encoding scheme yet., … Lossless Huffman …, … Extended Sequential DCT Arithmetic …, … Progressive DCT Arithmetic …, … Lossless Arithmetic …zune-jpeg is the baseline / progressive Huffman path only — uncommon JPEG variants surface explicit errors instead of crashing.

Clipboard boundary

hasClipboardImage and readClipboardImage are exported by the exact artifact. No libX11 or libwayland-client dynamic dependency was observed, but that absence does not prove the functions are stubs, their return values, or whether another mechanism is used. This audit did not invoke clipboard access because it is environment-affecting. Linux clipboard behavior and other-platform implementations therefore remain open native questions.

Recovery script

Terminal window
# (re)generate the .node files into ./claude-code-pkg/
node scripts/extract-claude-code-final-artifacts.mjs --refresh-package
# inspect
file claude-code-pkg/image-processor.node
readelf -d claude-code-pkg/image-processor.node | grep NEEDED
nm -D --undefined-only claude-code-pkg/image-processor.node | grep -c napi_
strings -n 6 claude-code-pkg/image-processor.node | grep -E "^[a-z][a-z0-9_-]+-[0-9]+\.[0-9]+\.[0-9]+/" | sed 's|/.*||' | sort -u

The third line confirms the unique N-API import count (43); the fourth recovers the full crate set with versions.

Caveats

  • This page works only from the Linux-x64 binary; macOS and Windows builds were not inspected, so export parity and clipboard behavior are unknown.
  • Internal Rust function layout is not recovered. Panic-location paths, imports, exports, and runtime observations identify artifact content but not full native control flow.
  • Crate-version strings and the rustc commit identify linked build inputs; they do not prove every linked codec or async path is reachable.

Created and maintained by Yingting Huang.