DEV Community

Max
Max

Posted on

Auto-Optimize Images in a Git Pre-Commit Hook (Local, No Upload)

Every repo I inherit has the same problem: someone dragged a 4.2 MB hero.png straight out of Figma into /public, committed it, and now every clone and every deploy drags that weight around forever. Git never forgets, so even after you shrink it later, the fat blob lives in history permanently.

The fix isn't "remember to compress images." Humans don't remember. The fix is a pre-commit hook that optimizes any staged image before it ever enters a commit — locally, with no upload to a third-party service.

Here's a setup you can paste in today.

The one-file version (no dependencies beyond what's already on your machine)

If you have pngquant and jpegoptim installed (both are in Homebrew and apt), this is the whole thing. Save it as .git/hooks/pre-commit and chmod +x it:

#!/usr/bin/env bash
# Optimize staged images before they enter a commit. All local, no uploads.
set -euo pipefail

# Only look at files that are staged (added/modified/copied)
staged=$(git diff --cached --name-only --diff-filter=ACM)

changed=0
while IFS= read -r file; do
  [ -f "$file" ] || continue
  case "${file,,}" in
    *.png)
      before=$(wc -c < "$file")
      pngquant --force --skip-if-larger --quality=65-90 --output "$file" -- "$file" 2>/dev/null || true
      after=$(wc -c < "$file")
      if [ "$after" -lt "$before" ]; then
        git add "$file"; changed=1
        echo "  png  $file  $((before/1024))KB -> $((after/1024))KB"
      fi
      ;;
    *.jpg|*.jpeg)
      before=$(wc -c < "$file")
      jpegoptim --strip-all --max=85 --quiet "$file" || true
      after=$(wc -c < "$file")
      if [ "$after" -lt "$before" ]; then
        git add "$file"; changed=1
        echo "  jpg  $file  $((before/1024))KB -> $((after/1024))KB"
      fi
      ;;
  esac
done <<< "$staged"

[ "$changed" -eq 1 ] && echo "Images optimized and re-staged."
exit 0
Enter fullscreen mode Exit fullscreen mode

What it does:

  • Runs only on staged images, so it never rewrites files you didn't touch.
  • --skip-if-larger / the size check means it never makes a file bigger (compression can occasionally do that on already-tiny PNGs).
  • Re-stages the optimized file with git add so the smaller version is what actually gets committed.
  • strip-all removes EXIF — which also quietly deletes GPS coordinates from phone photos before they hit a public repo. (That alone has saved me from committing my home address in a screenshot's metadata.)

Make it team-wide with pre-commit

.git/hooks isn't version-controlled, so a raw hook only protects your machine. To enforce it for everyone, use the pre-commit framework and commit the config:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: optimize-images
        name: optimize staged images
        entry: bash scripts/optimize-images.sh
        language: system
        types_or: [png, jpeg]
        pass_filenames: true
Enter fullscreen mode Exit fullscreen mode

Now pre-commit install wires it up for every contributor, and CI can run pre-commit run --all-files to catch anyone who skipped the hook with --no-verify.

Why local-only matters here

The lazy alternative is a SaaS compressor: upload the image, download the smaller one. That's fine for a one-off marketing PNG. It's a bad default inside an automated dev workflow for three reasons:

  1. Latency + rate limits. A network round-trip per image in a pre-commit hook makes committing feel broken. The free tiers (TinyPNG = 500/mo) run out fast in CI.
  2. Secrets/NDA exposure. Product screenshots, client mockups, internal dashboards — a lot of images in private repos are things you contractually cannot ship to a random third-party server.
  3. Determinism. A hook that depends on someone's API being up is a hook that will fail your commit at the worst possible moment.

pngquant/jpegoptim run on your machine, offline, deterministically. That's the right tool for the automated path.

When you just need a quick manual squish

Hooks are for the repo. For the one-off "designer Slacked me a 6 MB PNG and I need it web-ready right now," a CLI install is overkill. I built a browser-only compressor for exactly that case — it runs the compression in the tab via the Canvas API, nothing is uploaded, and it has web/social/email presets so it's one click:

QuickShrink — drop an image, get a smaller one, close the tab. Same privacy property as the CLI (no upload), just without installing anything.

Takeaway

Don't rely on discipline to keep image weight out of your repo — automate it at the commit boundary. A ~30-line pre-commit hook using local tools will quietly shave megabytes off every PR, strip EXIF/GPS as a bonus, and never leak a private image to a third party. Ship the hook once; benefit on every commit forever.

If you're already doing this in CI, I'd like to hear how — especially anyone running it as a required GitHub Action gate rather than a local hook. What breaks at scale?

Top comments (0)