Purpose
Manipulate and convert raster images using sharp. Two
tiers: reach for the quick path (sharp-cli via npx) for single conversions and simple
transforms, and the script path (global sharp + a Node script) for multi-step
pipelines, compositing, batch work, and reading image info. Always confirm the result by
inspecting the output dimensions/format, not by assuming the command worked.
Routing
- Single convert, resize, crop, rotate, or one simple effect on one file -> sharp-cli
(no install, runs from the npx cache).
- Multi-step pipeline, watermark/overlay, opacity, many files, or reading metadata ->
Node script against the global sharp install (the bundled scripts cover the common
cases; write an ad-hoc
.mjs for anything else).
Quick path: sharp-cli
sharp-cli needs no install; npx -y runs it from cache. General form:
npx -y sharp-cli -i <input> -o <output-dir> [-f <format>] [-q <1-100>] <command> [args] -- <command2> [args]
npx -y sharp-cli -i photo.png -o ./out -f jpeg # convert png -> ./out/photo.jpg
npx -y sharp-cli -i photo.jpg -o ./out resize 800 # width 800, height auto
npx -y sharp-cli -i photo.jpg -o ./out -f webp -q 80 rotate 90 -- resize 400 # chained
-o is an output directory, not a filename: sharp-cli writes <basename>.<ext> into
it (and jpeg lands as .jpg). Chain operations in one pass by separating them with --.
For the full command surface and per-task detail, read the reference for the task at hand:
- Converting formats, quality/compression, metadata -> read
references/convert.md
- Resizing, thumbnails, cropping, rotating, flipping -> read
references/resize.md
- Watermarks, overlays, grayscale/tint/blur effects -> read
references/composite.md
Script path: global sharp + Node
For anything sharp-cli cannot express in one pass, install sharp once and run a Node
script that uses its chaining API.
Install once (idempotent; skip if npm ls -g sharp already resolves):
npm i -g sharp
Run a bundled script (or your own .mjs) with plain node:
node ${CLAUDE_SKILL_DIR}/scripts/inspect.mjs <image> # print format/size/metadata as JSON
node ${CLAUDE_SKILL_DIR}/scripts/watermark.mjs <base> <overlay> <out> [--gravity southeast] [--opacity 0.5] [--scale 0.2]
node ${CLAUDE_SKILL_DIR}/scripts/batch.mjs <input-dir> <output-dir> [--format webp] [--quality 80] [--width N] [--height N]
inspect.mjs is how you READ image info; sharp-cli cannot print metadata.
watermark.mjs composites an overlay at a gravity, with optional opacity and scale.
batch.mjs converts/resizes every image in a directory, keeping base names.
Writing an ad-hoc script
A global install is not on Node's ESM resolution path, and Node ignores NODE_PATH
for import. Resolve sharp by basing a require at the global node_modules root, so the
script runs from any directory with no env setup:
import { createRequire } from 'node:module';
import { execSync } from 'node:child_process';
const sharp = createRequire(execSync('npm root -g').toString().trim() + '/anchor.js')('sharp');
await sharp('in.png')
.resize({ width: 1200, withoutEnlargement: true })
.jpeg({ quality: 82, mozjpeg: true })
.toFile('out.jpg');
The bundled scripts use exactly this header; copy one as a starting template.
Gotchas
-o is a directory, not a filename. sharp-cli writes <basename>.<ext> into it.
To rename, move the output afterward, or write a Node script that calls .toFile(path).
NODE_PATH does nothing for ESM. Use the createRequire-from-global-root header
above; do not prefix node with NODE_PATH.
- sharp-cli cannot read metadata. Its
--metadata is an output flag (it keeps EXIF on
the result). Use inspect.mjs to read dimensions/format/EXIF.
- Metadata is stripped by default. sharp drops EXIF (including orientation) and ICC on
output unless you call
.keepMetadata(). Good for shrinking, surprising if you needed it.
extract arg order differs. sharp-cli is extract <top> <left> <w> <h>; the library
object is { left, top, width, height }.
- RGBA -> JPEG turns transparency black. JPEG has no alpha;
.flatten({ background })
onto a solid colour before encoding.
quality is for lossy formats. It is ignored by png; use compressionLevel/palette
for png size instead.
Examples
Convert a folder of PNG screenshots to optimized WebP thumbnails (max 400px wide):
npm i -g sharp
node ${CLAUDE_SKILL_DIR}/scripts/batch.mjs ./screenshots ./thumbs --format webp --quality 80 --width 400
node ${CLAUDE_SKILL_DIR}/scripts/inspect.mjs ./thumbs/first.webp # confirm format + dimensions
1---2name: edit-image3description: This skill should be used when manipulating or converting raster images with sharp, including "convert this png to jpg", "resize this image", "make a thumbnail", "compress this photo", "crop/rotate/flip this image", "strip EXIF metadata", "add a watermark", "overlay my logo", "batch convert these images", or "what are this image's dimensions". Covers jpeg, png, webp, avif, gif, tiff. It should not be used for creating diagrams (use create-diagram), charts or plots from data (use visualize-data or analyze-data), editing PDFs (use edit-pdf), or vector/SVG authoring.4---56## Purpose78Manipulate and convert raster images using [sharp](https://sharp.pixelplumbing.com). Two9tiers: reach for the quick path (`sharp-cli` via `npx`) for single conversions and simple10transforms, and the script path (global `sharp` + a Node script) for multi-step11pipelines, compositing, batch work, and reading image info. Always confirm the result by12inspecting the output dimensions/format, not by assuming the command worked.1314## Routing1516- Single convert, resize, crop, rotate, or one simple effect on one file -> **sharp-cli**17 (no install, runs from the npx cache).18- Multi-step pipeline, watermark/overlay, opacity, many files, or reading metadata ->19 **Node script** against the global sharp install (the bundled scripts cover the common20 cases; write an ad-hoc `.mjs` for anything else).2122## Quick path: sharp-cli2324`sharp-cli` needs no install; `npx -y` runs it from cache. General form:2526```bash27npx -y sharp-cli -i <input> -o <output-dir> [-f <format>] [-q <1-100>] <command> [args] -- <command2> [args]28```2930```bash31npx -y sharp-cli -i photo.png -o ./out -f jpeg # convert png -> ./out/photo.jpg32npx -y sharp-cli -i photo.jpg -o ./out resize 800 # width 800, height auto33npx -y sharp-cli -i photo.jpg -o ./out -f webp -q 80 rotate 90 -- resize 400 # chained34```3536`-o` is an output **directory**, not a filename: sharp-cli writes `<basename>.<ext>` into37it (and `jpeg` lands as `.jpg`). Chain operations in one pass by separating them with `--`.3839For the full command surface and per-task detail, read the reference for the task at hand:4041- Converting formats, quality/compression, metadata -> read `references/convert.md`42- Resizing, thumbnails, cropping, rotating, flipping -> read `references/resize.md`43- Watermarks, overlays, grayscale/tint/blur effects -> read `references/composite.md`4445## Script path: global sharp + Node4647For anything sharp-cli cannot express in one pass, install sharp once and run a Node48script that uses its chaining API.49501. Install once (idempotent; skip if `npm ls -g sharp` already resolves):5152 ```bash53 npm i -g sharp54 ```55562. Run a bundled script (or your own `.mjs`) with plain `node`:5758 ```bash59 node ${CLAUDE_SKILL_DIR}/scripts/inspect.mjs <image> # print format/size/metadata as JSON60 node ${CLAUDE_SKILL_DIR}/scripts/watermark.mjs <base> <overlay> <out> [--gravity southeast] [--opacity 0.5] [--scale 0.2]61 node ${CLAUDE_SKILL_DIR}/scripts/batch.mjs <input-dir> <output-dir> [--format webp] [--quality 80] [--width N] [--height N]62 ```6364 - `inspect.mjs` is how you READ image info; sharp-cli cannot print metadata.65 - `watermark.mjs` composites an overlay at a gravity, with optional opacity and scale.66 - `batch.mjs` converts/resizes every image in a directory, keeping base names.6768### Writing an ad-hoc script6970A global install is **not** on Node's ESM resolution path, and Node ignores `NODE_PATH`71for `import`. Resolve sharp by basing a `require` at the global `node_modules` root, so the72script runs from any directory with no env setup:7374```js75import { createRequire } from 'node:module';76import { execSync } from 'node:child_process';77const sharp = createRequire(execSync('npm root -g').toString().trim() + '/anchor.js')('sharp');7879await sharp('in.png')80 .resize({ width: 1200, withoutEnlargement: true })81 .jpeg({ quality: 82, mozjpeg: true })82 .toFile('out.jpg');83```8485The bundled scripts use exactly this header; copy one as a starting template.8687## Gotchas8889- **`-o` is a directory, not a filename.** sharp-cli writes `<basename>.<ext>` into it.90 To rename, move the output afterward, or write a Node script that calls `.toFile(path)`.91- **`NODE_PATH` does nothing for ESM.** Use the `createRequire`-from-global-root header92 above; do not prefix `node` with `NODE_PATH`.93- **sharp-cli cannot read metadata.** Its `--metadata` is an output flag (it keeps EXIF on94 the result). Use `inspect.mjs` to read dimensions/format/EXIF.95- **Metadata is stripped by default.** sharp drops EXIF (including orientation) and ICC on96 output unless you call `.keepMetadata()`. Good for shrinking, surprising if you needed it.97- **`extract` arg order differs.** sharp-cli is `extract <top> <left> <w> <h>`; the library98 object is `{ left, top, width, height }`.99- **RGBA -> JPEG turns transparency black.** JPEG has no alpha; `.flatten({ background })`100 onto a solid colour before encoding.101- **`quality` is for lossy formats.** It is ignored by png; use `compressionLevel`/`palette`102 for png size instead.103104## Examples105106Convert a folder of PNG screenshots to optimized WebP thumbnails (max 400px wide):107108```bash109npm i -g sharp110node ${CLAUDE_SKILL_DIR}/scripts/batch.mjs ./screenshots ./thumbs --format webp --quality 80 --width 400111node ${CLAUDE_SKILL_DIR}/scripts/inspect.mjs ./thumbs/first.webp # confirm format + dimensions112```