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-100 keeps 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 1 uses maximum compression effort (slower but smaller output). Since we're only compressing one image at a time, the extra milliseconds don't matter.
  • --strip removes 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.

Enjoy this post?

Consider leaving a small donation to support the blog.

$