Shrink clipboard screenshots by 70% before pasting into AI tools
I paste a lot of screenshots into Claude Code. UI bugs, browser state, error messages, design references. Every screenshot gets base64-encoded and sent to the API, consuming tokens. A single Retina screenshot can easily be 200-400KB as a PNG. Across a coding session with 10-20 screenshots, that adds up fast.
The fix: intercept the clipboard and compress the PNG before you paste it. I built a small macOS utility that does this automatically, reducing most screenshots by 50-80% with no visible quality loss.
Table of Contents
The problem
macOS screenshots are saved as lossless PNGs. This is great for archival quality but wasteful for AI context windows. When you paste a screenshot into Claude Code (or any tool that sends images to an API), the full uncompressed PNG gets transmitted.
A typical macOS screenshot of a code editor or browser window is 150-400KB. After lossy PNG compression, that same image is 40-100KB, and you can't tell the difference.
Why pngquant, not JPEG
The obvious thought is "just convert to JPEG." But JPEG is a bad format for screenshots. It uses DCT-based compression designed for photographs with smooth gradients. Sharp edges (text, UI borders, icons, code) get smudgy compression artifacts. To keep text crisp at JPEG quality 95+, you barely save any space.
pngquant takes a different approach. It reduces the color palette of a PNG from millions of colors to an optimally chosen subset (up to 256 colors per channel). This works perfectly for screenshots because they're mostly flat UI colors, text, and simple gradients. The output stays as PNG, so edges remain pixel-perfect.
| Method | Typical reduction | Text quality |
|---|---|---|
| JPEG 85 | 70-80% smaller | Visible artifacts around text |
| JPEG 95 | 40-50% smaller | Mostly fine, some ringing |
| pngquant 85-100 | 50-80% smaller | Pixel-perfect |
pngquant gives you JPEG-level file size reduction while keeping PNG-level sharpness. For screenshots of code, terminals, and UIs, it's the right tool.
The solution
I wrote a bash script that runs as a background daemon on macOS. It watches the clipboard for new images, compresses them with pngquant, and replaces the clipboard content with the optimized version. The whole thing is invisible once running.
Prerequisites
Install two Homebrew packages:
brew install pngpaste pngquant
- pngpaste reads image data from the macOS clipboard and saves it as a PNG file
- pngquant performs lossy PNG compression with configurable quality
The script
Create ~/bin/clipboard-optimize (or wherever you keep personal scripts):
#!/bin/bash
set -euo pipefail
CONFIG_DIR="$HOME/.config/clipboard-optimize"
ENABLED_FILE="$CONFIG_DIR/enabled"
PID_FILE="$CONFIG_DIR/daemon.pid"
LOG_FILE="$CONFIG_DIR/clipboard-optimize.log"
TEMP_DIR="${TMPDIR:-/tmp}/clipboard-optimize"
mkdir -p "$CONFIG_DIR" "$TEMP_DIR"
log() {
echo "[$(date '+%H:%M:%S')] $*" >> "$LOG_FILE"
}
The script uses ~/.config/clipboard-optimize/ for state: a toggle file, PID file, and log.
Clipboard change detection
The first challenge is knowing when the clipboard changes. My initial approach was hashing the clipboard contents with md5, but macOS doesn't round-trip PNGs faithfully. Writing a 61KB optimized PNG to the clipboard and reading it back with pngpaste produces an 82KB file with a different hash, creating an infinite re-optimization loop.
The fix is NSPasteboard.changeCount, which macOS increments every time the clipboard changes. You can access it from bash via osascript:
get_change_count() {
osascript -e 'use framework "AppKit"' \
-e "return (current application's NSPasteboard's generalPasteboard()'s changeCount()) as integer" 2>/dev/null
}
This returns an integer that only changes when something actually modifies the clipboard. After we write back the optimized image, we capture the new count, so the daemon knows to skip it.
The optimization function
optimize_clipboard() {
local raw="$TEMP_DIR/raw.png"
local opt="$TEMP_DIR/opt.png"
# Extract image from clipboard (fails if no image present)
if ! pngpaste "$raw" 2>/dev/null; then
return 1
fi
local original_size
original_size=$(stat -f%z "$raw")
# Skip tiny images (< 10KB) — not worth optimizing
if [[ $original_size -lt 10240 ]]; then
rm -f "$raw"
return 1
fi
# Lossy compression: quality 85-100, max effort, strip metadata
if ! pngquant --quality=85-100 --speed 1 --strip --force \
--output "$opt" "$raw" 2>/dev/null; then
rm -f "$raw" "$opt"
return 1
fi
local opt_size
opt_size=$(stat -f%z "$opt")
# Only replace if we saved more than 5%
local threshold=$(( original_size * 95 / 100 ))
if [[ $opt_size -ge $threshold ]]; then
rm -f "$raw" "$opt"
return 1
fi
# Write optimized PNG back to clipboard
osascript -e "set the clipboard to (read (POSIX file \"$opt\") as «class PNGf»)" 2>/dev/null
local saved_kb=$(( (original_size - opt_size) / 1024 ))
local orig_kb=$(( original_size / 1024 ))
local opt_kb=$(( opt_size / 1024 ))
local pct=$(( (original_size - opt_size) * 100 / original_size ))
log "Optimized: ${orig_kb}KB -> ${opt_kb}KB (saved ${saved_kb}KB, ${pct}%)"
echo "Optimized: ${orig_kb}KB -> ${opt_kb}KB (-${pct}%)"
rm -f "$raw" "$opt"
return 0
}
Key decisions in the flags:
--quality=85-100keeps quality high. pngquant will skip the optimization entirely if it can't meet the minimum quality of 85, so you never get a bad-looking result.--speed 1uses maximum compression effort (slower but smaller output). Since we're only compressing one image at a time, the extra milliseconds don't matter.--stripremoves PNG metadata (color profiles, timestamps, etc.) for additional savings.- The 5% threshold prevents replacing the clipboard when pngquant can barely improve on the original.
The daemon loop
daemon_loop() {
log "Daemon started (PID $$)"
echo $$ > "$PID_FILE"
local last_count
last_count=$(get_change_count)
trap 'log "Daemon stopped"; rm -f "$PID_FILE"; exit 0' SIGTERM SIGINT
while true; do
if [[ -f "$ENABLED_FILE" ]]; then
local current_count
current_count=$(get_change_count)
if [[ "$current_count" != "$last_count" ]]; then
if optimize_clipboard; then
# Our write incremented changeCount - capture the new value
last_count=$(get_change_count)
else
last_count="$current_count"
fi
fi
fi
sleep 1
done
}
The daemon polls every second. It only attempts optimization when the clipboard changeCount has changed and the enabled toggle file exists. After a successful optimization, it captures the new changeCount (which was incremented by our osascript clipboard write) so it doesn't try to re-optimize the same image.
Command interface
The script supports four modes via the first argument:
case "${1:-}" in
daemon) cmd_daemon ;; # Start background daemon
stop) cmd_stop ;; # Stop running daemon
status) cmd_status ;; # Show status and recent optimizations
*) cmd_oneshot ;; # Optimize current clipboard image (default)
esac
The cmd_daemon function starts the loop in the background with disown, and cmd_stop sends SIGTERM to the PID stored in the PID file.
Shell aliases for toggling
Add these to your ~/.zshrc (or ~/.bashrc):
# Clipboard screenshot optimizer
clipopt() { clipboard-optimize; }
clipopt-on() {
touch ~/.config/clipboard-optimize/enabled
clipboard-optimize daemon
}
clipopt-off() {
rm -f ~/.config/clipboard-optimize/enabled
clipboard-optimize stop
}
clipopt-status() { clipboard-optimize status; }
Make sure ~/bin is in your PATH:
export PATH="$HOME/bin:$PATH"
Usage
One-shot mode (optimize whatever's on the clipboard right now):
$ clipopt
Optimized: 201KB -> 61KB (-69%)
Daemon mode (auto-optimize every screenshot you copy):
$ clipopt-on
Starting clipboard-optimize daemon...
Daemon running (PID 79214)
Check status:
$ clipopt-status
Optimization: ENABLED
Daemon: RUNNING (PID 79214)
Total optimizations: 47
Recent:
[14:23:01] Optimized: 312KB -> 89KB (saved 223KB, 71%)
[14:25:44] Optimized: 187KB -> 52KB (saved 135KB, 72%)
[14:31:12] Optimized: 95KB -> 41KB (saved 54KB, 56%)
Turn it off:
$ clipopt-off
Daemon stopped
Results
In my first test, the script compressed a 201KB screenshot down to 61KB, a 69% reduction. Across a typical coding session, here's what I've seen:
| Screenshot type | Before | After | Reduction |
|---|---|---|---|
| Code editor (VS Code) | 201KB | 61KB | 69% |
| Browser page | 312KB | 89KB | 71% |
| Terminal output | 95KB | 41KB | 56% |
| Figma design | 187KB | 52KB | 72% |
The visual quality is indistinguishable from the originals. Text remains crisp, colors are accurate, and UI elements look identical. The compression works by reducing the color palette intelligently, not by blurring or introducing artifacts.
Gotchas I ran into
macOS clipboard doesn't round-trip PNGs. When you write a PNG to the clipboard via osascript and read it back with pngpaste, you get a different binary representation. The image looks the same, but the file size and hash are different. This broke my initial hash-based change detection and caused an infinite loop where the daemon kept re-optimizing the same image every second. The NSPasteboard.changeCount approach solved this cleanly.
pngquant exits non-zero when quality can't be met. If the input image can't be compressed to meet the --quality minimum, pngquant returns exit code 99. The script handles this gracefully by checking the return code and skipping the replacement.
The daemon needs disown. Starting the loop with & alone isn't enough because the background process is still attached to the shell session. disown detaches it so it survives terminal closure.
Next steps
A few improvements I'm considering:
- launchd integration to start the daemon at login instead of manually running
clipopt-on - Notification Center alerts for each optimization (currently logs only)
- Configurable quality via environment variable or config file
- Stats tracking for total bytes saved across sessions
If you work with AI coding assistants and paste a lot of screenshots, this is an easy win. The whole setup takes about five minutes and runs invisibly in the background.
Consider leaving a small donation to support the blog.
Comments