Give your coding agent an image compressor
Ask Claude Code or Codex to "compress the images in this folder" and watch what
happens. It will offer to install sharp, write a throwaway Node script, guess
at quality settings, and hand you back a folder where some files got smaller,
one got bigger, and nobody can tell you which.
The agent isn't the problem. Image compression is one of those capabilities that looks trivial and isn't: format-specific encoders, quality heuristics, and a result you can only judge by measuring. That's a tool's job, not a script the agent improvises each time.
The split that makes it work
There's one step an agent genuinely cannot do: sign in. pipic login opens a
browser and waits for you to approve it. An agent running in a sandbox has a
different HOME, no browser, and no way to click the button.
So don't ask it to. Split the work by who can actually do it:
You, once. In your own terminal:
npm i -g @pipic/cli
pipic login
Credentials land in ~/.config/pipic/config.json. Every tool running as you on
that machine can now use them — including your agent.
Your agent, every time after that. Paste this into the conversation:
Use the `pipic` CLI to compress the images in ./assets.
It's already installed and signed in — do not install it or run
`pipic login` yourself.
pipic ./assets --replace --json
It prints one JSON object per file:
{"file":"a.png","status":"ok","before":102400,"after":41000,"saved":61400}
status is "ok", "skipped" or "error". "skipped" is not a failure — it
carries an `error` string explaining why (e.g. already small enough).
Exit 0 = all good, 1 = some files failed, 3 = not signed in,
4 = monthly quota exhausted.
Don't retry on exit 2 (usage error), 3 or 4 — on 3, stop and tell the
user to sign in. On exit 1, re-run only the paths whose rows had status
"error": a full re-run re-uploads every file and spends quota again.
Every line in there prevents a specific failure, and two of them prevent expensive ones.
Without the "do not install it or run pipic login yourself", the agent tries
anyway — and pipic login waits up to ten minutes for a browser approval it
can't give. Inside Codex or Claude Code that shows up as a tool call your
framework eventually kills, which reads as the tool hanging.
Without the exit-1 line, an agent that sees two failures in a hundred-file run does the obvious thing and re-runs the whole directory. There's no local "already compressed" check — every file gets uploaded again, and the monthly allowance counts every request it accepts. On the free tier that's a hundred a month, so one blind retry can spend the lot.
The rest are cheaper mistakes: without a destination flag it gets a usage error,
and without the note that skipped isn't a failure it reports trouble where
there is none — skipped rows carry an error string too, explaining why nothing
changed.
What the agent sees
An illustrative session — the shape is real, the numbers are made up and yours will differ:
> compress the images in ./assets
I'll use the pipic CLI.
$ pipic ./assets --replace --json
{"file":"assets/hero.png","status":"ok","before":2411233,"after":486201,"saved":1925032}
{"file":"assets/team.jpg","status":"ok","before":880640,"after":712704,"saved":167936}
{"file":"assets/icon.svg","status":"skipped","before":0,"after":0,"saved":0,"error":"unsupported file type"}
{"file":"assets/logo.webp","status":"skipped","before":18944,"after":18944,"saved":0,"error":"not smaller — original kept"}
Done. Two files compressed, saving about 2.0 MB — hero.png did most of
the work, dropping from 2.4 MB to 486 KB. I left logo.webp alone because
compressing it came back larger, and skipped icon.svg since SVG isn't a
raster format.
That last paragraph is the part worth noticing. The agent isn't guessing — it's
reading a contract. saved is a byte count, not a ratio. skipped is not
error. not smaller — original kept says exactly why nothing changed.
Improvised scripts don't produce that. They print whatever console.log the
agent felt like writing, and the summary you get back is the agent's impression
of its own output.
Why "only if it's smaller" matters here
Already-optimised images sometimes come back larger. A naive script overwrites them anyway, and your repo quietly grows every time it runs.
pipic writes a file back only when the result is actually smaller. Under
--replace the original stays untouched; under -o it's copied across so the
output directory is still a complete set. Either way the file reports as
skipped, not error — so an agent reading the output doesn't tell you
something failed when nothing did.
The flag that isn't optional
--replace and -o <dir> are mutually exclusive, and one of them is required.
Run pipic ./assets with neither and it exits with a usage error before sending
a single request.
This is deliberate, and it matters more when an agent is driving. A tool that overwrites by default is a tool that eventually overwrites something you meant to keep, on a run you didn't fully read. Making the destination explicit means the agent has to state its intent, and you can see that intent in the command it shows you.
When the agent can't see your credentials
Sandboxed agents, containers, CI — anywhere ~/.config isn't yours. Create a
token on your account page and expose it as
PIPIC_TOKEN in that environment. It takes priority over the credentials file,
so the same command works without pipic login.
Note that pipic token create deliberately sends you to the web page rather
than minting a token locally. A stolen CLI credential shouldn't be able to mint
fresh ones.
Exit codes are the interface
For anything automated, this is the part that matters most:
| Code | Meaning |
|---|---|
| 0 | Everything succeeded |
| 1 | Some files failed |
| 2 | Usage error |
| 3 | Sign-in required |
| 4 | Monthly quota exhausted |
3 means run pipic login — the one thing you have to do yourself. 4 means
you're out of monthly allowance for CLI and API use; nothing is charged
automatically, and compressing on pipic.cc stays free and unlimited regardless.
An agent that branches on these does the right thing without asking you to
interpret a wall of text. One that pattern-matches on message strings breaks the
first time we reword something — which is why the codes, the status values and
the field names are the stable contract, and the human-readable error text
isn't.