A tiny, fast image optimizer for the terminal — resize, compress, and rename images while keeping transparency. Output stays PNG for maximum compatibility (Outlook desktop, older webmails, everything), with an optional WebP sibling. Optionally uses vision AI (OpenAI or Claude) to suggest a semantic filename, alt text, optimal parameters, and a ready-to-paste HTML snippet — tuned per context (email signature, web, hero, icon, avatar).
No banding on gradients, no bloated files, no %20 in your URLs.
Most "optimize this image" advice stops at compression. The biggest win is
almost always shipping the image at its display size — a 1000px asset shown
at 200px is wasting 96% of its bytes no matter how well you compress it. squish
does both, keeps the file email/web-safe, and gives it a clean URL-safe name.
One-liner (copies squish to a bin dir on your PATH and checks deps):
curl -fsSL https://raw.githubusercontent.com/heycesar/squish/main/install.sh | bashOr do it by hand. Requires pngquant and oxipng; optional tools unlock extra flags.
macOS (with Homebrew):
brew install pngquant oxipng
brew install webp jq # optional: --webp and --aiLinux (Debian/Ubuntu):
sudo apt install pngquant imagemagick webp jq
# oxipng: cargo install oxipng (or grab a release binary)Then put squish.sh on your PATH:
chmod +x squish.sh
ln -sf "$(pwd)/squish.sh" /opt/homebrew/bin/squish # macOS (Apple Silicon)
# or
ln -sf "$(pwd)/squish.sh" /usr/local/bin/squish # macOS Intel / Linux
squish --help| Tool | Needed for | Required? |
|---|---|---|
pngquant |
PNG palette compression | ✅ |
oxipng |
PNG lossless recompression | ✅ |
imagemagick |
resize on Linux, JPEG, --avif |
recommended |
sips |
resize on macOS (built in) | — |
cwebp |
--webp (or ImageMagick) |
optional |
curl + jq |
--ai (vision analysis) |
optional |
Cross-platform: resizing uses
sipson macOS and ImageMagick everywhere else. Install ImageMagick on Linux and--width, JPEG, and--avifall work.
squish image.png # → image.png (URL-safe slug name)
squish photo.jpg # JPG in, JPG out (progressive, tuned)
squish image.png -w 400 # resize to 400px wide, then compress
squish image.png --retina --display 200 # 2× of a 200px display size → 400px
squish image.png --webp --avif # also write .webp and .avif siblings
squish *.png -w 400 --out-dir dist # batch into ./dist/
squish assets/*.png --dry-run # preview names/sizes, write nothing
squish photo.jpg --ai # AI: suggest name / alt / params / html
squish arc.png --ai --context email-signature --applyThe output shows, per file: before→after dimensions, a savings bar, the byte
sizes, any .webp/.avif siblings, and a final total. Color turns off
automatically when piped or in CI.
| Input | Output | Engine |
|---|---|---|
| PNG | PNG (keeps alpha) | pngquant + oxipng |
| JPG / JPEG | JPG (progressive) | ImageMagick |
| any of the above | .webp sibling (--webp) |
cwebp, or ImageMagick |
| any of the above | .avif sibling (--avif) |
ImageMagick |
| Flag | What it does |
|---|---|
-w, --width N |
Resize to N px wide (keeps aspect ratio). The biggest win — ship at ~2× display size. Never upscales past the source. |
-r, --retina |
With --display, targets 2× that width. Shorthand for --width. |
--display N |
Intended on-screen width (px). With --retina, resizes to 2×N. |
-c, --colors N |
PNG palette size (default 128). Lower = smaller & more banding. 128 ≈ no visible loss; 64 = aggressive. |
--jpeg-quality N |
JPEG output quality 1–100 (default 82). |
--webp |
Also emit a .webp (~40% smaller). Needs a <picture> fallback — Outlook can't read WebP. |
--avif |
Also emit an .avif (~50% smaller). Needs ImageMagick built with AVIF support. |
--dry-run |
Print what would happen (names, sizes) without writing any file. |
-d, --out-dir DIR |
Write all outputs into DIR (created if missing). |
-o, --output F |
Explicit output path (single input; overrides naming). |
--name-as WHAT |
How to name outputs (see below). Default: slug. |
--rename NAME |
Replace the base name entirely (single input; slugified). |
--ai |
Analyze the image with vision AI and print suggestions (see AI section). |
--ai-provider P |
auto (default) / openai / anthropic. auto uses whichever key is set. |
--context WHAT |
What the image is for: auto (default under --ai) / general / email-signature / web / hero / icon / avatar. |
--ai-fields L |
Comma list of fields to request (default name,alt,params,html). |
--apply |
Apply the AI-suggested name automatically (implies --ai); works on batches with collision suffixes. |
--ai-model M |
Model override (default gpt-4o-mini / claude-haiku-4-5). |
--no-cache |
Bypass the AI result cache (~/.cache/squish/). |
--no-color |
Disable color (also respects NO_COLOR). |
-q, --quiet |
Only print the per-file result lines. |
-h, --help |
Help. |
Pass a directory with -R (or --recursive) to optimize every supported image
inside it, at any depth:
squish assets/ -R --webpIt discovers .png, .jpg, and .jpeg files (case-insensitive), skips
everything else and any hidden directory (.git/, .cache/), and does not
follow symlinks. Outputs are written in place, next to each source.
With --out-dir, the source tree is mirrored into the target:
assets/logo.png → dist/logo.png
assets/ui/icon.png → dist/ui/icon.png
squish assets/ -R --out-dir dist/(-r remains --retina; the recursive short flag is the capital -R.)
Keep squish running and it re-optimizes sources as you edit them:
squish assets/ --watch -RIt optimizes everything once, then polls the sources every 2 seconds and
re-optimizes any that change — or any new image you drop into a watched
directory (with -R). Only sources are watched; the files squish generates
(-min, .webp, .avif) never re-trigger it. Press Ctrl-C to stop.
--watch can't be combined with --dry-run. With --ai, note that each change
re-queries the model, so an API key means per-edit API calls.
Drop a .squishrc in a project directory to persist default flags, so you don't
retype them every run. It's a flat key=value file; lines starting with # are
comments. Only ./.squishrc (the current directory) is read.
# .squishrc — defaults for this project
colors=64
webp=1
ai=1
context=email-signature
out_dir=distPrecedence: CLI flags override .squishrc, which overrides built-in defaults.
| Source | Wins over |
|---|---|
| CLI flag | everything |
./.squishrc |
built-in default |
| built-in default | — |
Supported keys: colors, width, webp, avif, jpeg_quality, name_as,
out_dir, ai, context, ai_provider, ai_model, ai_fields, no_cache,
quiet, no_color. Per-invocation actions (--output, --rename, --apply,
--dry-run, and the input files) are CLI-only. Unknown keys and invalid values
are warned and skipped; the file is never executed.
With "forma_derecha 1.png" and -w 400:
| Value | Result | Use |
|---|---|---|
slug (default) |
forma-derecha-1.png |
URL-safe (no spaces/accents/uppercase) |
optimized |
forma_derecha 1.optimized.png |
keeps the original name + suffix |
plain |
forma_derecha 1.png |
original name as-is |
retina |
forma-derecha-1@2x.png |
for srcset / retina |
width |
forma-derecha-1-400w.png |
multiple widths |
Never overwrites the source: if the output name collides with the input in the same folder, it appends
-min(e.g.logo.png→logo-min.png).
--rename replaces the base name completely:
squish "forma_derecha 1.png" --retina --display 200 \
--rename signature-arc-right --name-as retina
# → signature-arc-right@2x.png
slug/retina/widthclean the name for bucket/URL use:Árbol Décor.pngbecomesarbol-decor.png. Recommended whenever the image is served over HTTP — a space becomes%20and causes problems.
With --ai, after optimizing, the image is sent to a vision model that returns
structured JSON and prints it as suggestions:
squish arc.png -w 400 --ai --context email-signature--ai works with or without an API key:
- With a key (OpenAI or Anthropic): full AI analysis — semantic name, alt text, optimal parameters (colors, webp, avif), and a ready-to-paste HTML snippet tuned to context.
- Without a key (local mode): an ImageMagick heuristic detects the image
kind(photo/logo/gradient/icon) and suggests optimal--colorsand--webp/--avifflags. Prints🔍 auto (local).
| Provider | Env var | Default model |
|---|---|---|
| OpenAI | OPENAI_API_KEY |
gpt-4o-mini |
| Anthropic | ANTHROPIC_API_KEY |
claude-haiku-4-5 |
By default (--ai-provider auto) it uses OpenAI if OPENAI_API_KEY is set,
otherwise Anthropic. Force one with --ai-provider openai|anthropic. Both are
cheap for this task (classifying one small image). If no key or tools are
present, --ai warns and skips — the optimization still runs.
squish photo.jpg --ai # no key → local heuristic; with key → full AI
export OPENAI_API_KEY="sk-..." # or ANTHROPIC_API_KEY
squish photo.jpg --ai --apply # apply the suggested name
squish arc.png --ai --context email-signature
squish icon.png --ai --ai-fields name,altUnder --ai, if you don't pass --context, it defaults to auto — the model
infers whether the image is an avatar, hero, icon, web image, email signature,
or general. You can still override with an explicit --context web etc.
AI results are cached by default (in ~/.cache/squish/, keyed by image content
hash + model + context + fields). Subsequent runs on the same image with the
same settings re-use the cache. To bypass: --no-cache.
--apply works on multiple inputs. When output names collide, the script adds
numeric suffixes: name.png, name-2.png, name-3.png, etc.
| Field | What it is |
|---|---|
name |
URL-safe semantic slug. With --apply, becomes the output name. |
alt |
Alt text for the <img> (accessibility). |
params |
Classifies the image (photo/logo/gradient/…) and suggests --colors and whether --webp helps. |
html |
Ready-to-paste HTML snippet, tuned to --context. |
Security: keys are read from the environment, never stored in the script or printed. Don't commit keys — use a git-ignored
.env/.dev.vars, or export them in your shell.
- Export the image at ~2× its on-screen width (shown at 200px →
-w 400). - Put the display width in the HTML:
<img src="..." width="200">— crisp on retina. - Serve it with a long cache:
Cache-Control: public, max-age=31536000, immutable. - Keep the output PNG — Outlook desktop and several webmails don't render WebP.
Real example: a 1016 KB / 1154px asset → 40 KB at -w 400 (−96%).
- Optional resize (
sipson macOS, ImageMagick elsewhere) — never upscales. - Compress by format: PNG via
pngquant(palette, keeps alpha) +oxipng -o max(lossless); JPEG via ImageMagick (progressive, tuned quality). - Optional
.webp/.avifsiblings. - Optional AI pass: sends the resized image to the vision model with a strict JSON schema and prints the suggestions.
Optimize images automatically in pull requests:
- uses: heycesar/squish@v0.2.0
with:
files: "assets/**/*.png"
webp: "true"See docs/USING_THE_ACTION.md for a full workflow.
brew install heycesar/tap/squishSee docs/HOMEBREW.md for tap setup.
Tab-completion for flags, enumerated values, and image files, in zsh and bash.
Homebrew: installed automatically — restart your shell after
brew install heycesar/tap/squish and squish --<TAB> just works.
Manual (curl/clone installs):
zsh — place the script on your fpath as _squish:
mkdir -p ~/.zsh/completions
cp completions/squish.zsh ~/.zsh/completions/_squish
# in ~/.zshrc, before `compinit`:
fpath=(~/.zsh/completions $fpath)bash — source it from ~/.bashrc:
echo "source /path/to/squish/completions/squish.bash" >> ~/.bashrcIssues and PRs welcome — see CONTRIBUTING.md. CI runs ShellCheck and a bats test suite on every push.
MIT © César
