Compress images with one command.
The same engine as the website, in your terminal, your build and your agent. Node 20 or newer, no telemetry, images deleted the moment they’re compressed.
Let your agent install it
Send this to Claude Code, Codex or any coding agent — it reads this doc and sets itself up.
Install the pipic CLI for me based on this doc, then tell me how to sign in: https://pipic.cc/cliWhy the CLI
Agent-native output
NDJSON and stable exit codes an agent can branch on — not prose it has to guess at.
Whole folder, one command
In place or into a fresh directory — local batches and build output alike.
Sign in once
Scripts, CI (via PIPIC_TOKEN) and agents run unattended after that — nothing to approve again.
Safe by default
Nothing is touched without an explicit --replace or -o, writes are atomic, and files that wouldn’t shrink are left alone.
Quick start
agents · install the skillInstall the skill once and your agent finds pipic on its own — you never have to name the tool or repeat the flags again.
Install the CLI
Node 20 or newerInstall the skill
Claude Code, Codex, Cursor and 70-odd othersApprove the sign-in
One approval per machineThen just ask
No tool name, no flags- Never run pipic login itself — hand the approval to the user.
- skipped is not a failure — it carries the reason, e.g. already small enough.
- After a partial failure, re-run only the failed files — a full re-run spends quota twice.
One-off task, or an agent without skills — hand it the command directly. The full contract lives one section down:
Use the pipic CLI to compress the images in ./assets, replacing them in place: pipic ./assets --replace --json Run `pipic -h` for more.
Agent running in a container or sandbox that can’t see your ~/.config? Create a token on your account page and set PIPIC_TOKEN in that environment instead.
Running it yourself
humans · scripts · CIInstall
Node 20 or newerNothing installed
npxNo browser — SSH, containers
device codeCI and sandboxes
PIPIC_TOKENCommands
pipic <paths…>Compress files or a directory. This is the default command.pipic login [--no-browser]Authorize this machine through the browser, or with a device code.pipic logoutSign out and revoke this machine’s token.pipic whoamiShow the signed-in account and its plan.pipic quotaShow how much of this month’s quota is left.pipic token listList your tokens with their prefix and last use.pipic token createPrints a link to create one on the website — for security, creating a token always happens there, never in the CLI.pipic token revoke <id>Revoke a token you own, usually effective within a minute.login, logout, whoami, quota and token are matched as subcommands before pipic treats them as paths. To compress a folder that happens to share one of those names, write ./login or login/ — anything with a path separator goes to the compress command instead.
Compress options
--replaceOverwrite the originals in place — atomic replace, permissions kept, symlinks resolved.
-o, --out <dir>Write results to this directory instead.
--jsonNDJSON output, one line per file — for scripts and agents.
--concurrency <n>Parallel uploads. Default 4.
You must give exactly one of --replace or -o — they’re mutually exclusive, and neither is a default. Without one, pipic exits with a usage error (code 2).
Good to know
- Files are only written back when the result is actually smaller — already-optimized images are skipped, with the reason reported. Under -o the original is copied across anyway, so the output directory is always a complete set.
- --replace writes to a temp file and renames it, so a Ctrl-C never leaves a half-written file behind.
- Files over 8 MB are skipped with a clear reason, never silently dropped.
- JPG, PNG, WebP and AVIF are supported. No telemetry is ever sent.
- With -o, two files that share a name — from different subfolders, say — collide: pipic reports an error for both rather than guessing which one to keep.
JSON Output Contract
--json prints one JSON object per line, one per file, with six fixed fields:
fileThe path you passed in, unchanged.status"ok", "skipped", or "error" — no other values.beforeOriginal size in bytes.afterCompressed size in bytes — 0 on skipped/error rows.savedBytes saved (before minus after) — never a percentage.errorPresent on skipped and error rows only — a human-readable reason, not a code.Exit codes
This is a stable contract: branch scripts and agents on status and the exit code — never on the text of error.
HTTP API
No Node? Call the same compression engine directly over HTTP — one image per request, no SDK, no multipart forms.
AuthorizationBearer <token> — required on every request.Content-Typeimage/png, image/jpeg, image/webp or image/avif — required, and must match the bytes you send.X-Original-NameOptional. URL-encode the filename first — only encodeURIComponent-safe characters are echoed back.BodyRaw image bytes, not multipart/form-data. One image per request, up to 8 MB.Response
200 OK returns the compressed image as the response body — same format in, same format out. Content-Type reflects the compressed format; Content-Length and X-Original-Name come back too when available.
Get a token
The API uses the same Personal Access Tokens as the CLI. Sign in with GitHub or Google, then create one from your account page — the plain token is shown once, so save it somewhere safe.
Errors
Anything other than 200 responds with a JSON envelope. Branch your code on the code field, not on the message text — here’s a real one:
400INVALID_TYPE — missing or non-image Content-Type; NO_FILE — empty body401UNAUTHORIZED, TOKEN_EXPIRED or TOKEN_REVOKED — sign in again or create a new token413FILE_TOO_LARGE — over 8 MB415UNSUPPORTED_TYPE — an image/* type we don’t compress (only PNG, JPEG, WebP, AVIF)429RATE_LIMITED (see the Retry-After header) or MONTHLY_QUOTA_EXCEEDED — this month’s allowance is used up502UPSTREAM_ERROR — our compression backend rejected this image; retrying the same bytes fails the same way. The attempt is refunded to your quota503UPSTREAM_ERROR or ALL_KEYS_EXHAUSTED — temporarily out of capacity; safe to retry with backoff. The attempt is refunded to your quotaGood to know
- A request counts against your quota the moment we accept it for compression — including ones that go on to fail with 400, 413 or 415. Requests rejected before that point never count: any 401, a 429 RATE_LIMITED, and a file rejected purely on its declared Content-Length header. If we fail on our side (502/503), the attempt is refunded automatically.
Every header and error code, in one machine-readable file: /openapi.json
FAQ
Will an agent touch files I didn’t tell it to?
No — pipic requires an explicit --replace or -o <dir>. Without one it refuses to run, and it never sends a single request before reporting that error.
I signed in once — can an agent keep using pipic forever?
Yes. The credential lives on this machine at ~/.config/pipic/config.json, and any tool running as you can read it — no extra approval needed per agent.
An agent says it’s not signed in — what do I do?
It’s most likely running in a container or sandbox that can’t see your ~/.config. Create a token on your account page and set it as PIPIC_TOKEN in that environment.
What happens when the free quota runs out?
pipic exits with code 4 and nothing is ever charged automatically. Free includes 100 images a month; Pro raises that to 5,000.
Are my images kept anywhere?
No — they’re deleted from the server the moment compression finishes, and nothing is stored long-term.