Bài viết này hiện chỉ có bằng tiếng Anh.

GitOct 31, 20258 min read

Safe Image Conversion in Pre-Commit Hooks

Pre-commit hooks can catch image workflow mistakes before they reach a pull request. They can also make commits slow, surprising, or frustrating if they rewrite files without clear review. Image conversion is a good candidate for pre-commit checks, but usually not for aggressive automatic mutation.

The safest pattern is to use hooks for small, mechanical checks: inspect staged files, detect oversized additions, run a non-writing preview, verify that expected WebP outputs exist, or remind contributors to run the documented conversion command.

This is where image automation advice often becomes unsafe. "Add a pre-commit hook to optimize images" is incomplete unless the hook policy explains what the hook may change, whether the files are staged, how contributors review generated outputs, and what happens when two source files would create the same .webp name.

Decide Whether the Hook Checks or Changes#

A hook can either check files or modify them. For image workflows, checking is usually safer:

  • fail when a new source image lacks an optimized output
  • warn when an image exceeds a size threshold
  • run a dry run on staged image folders
  • verify that generated files are in the expected folder

Automatic conversion inside a hook is riskier because it may create files the contributor does not inspect before committing. If a hook writes WebP outputs, the hook should make that obvious and require the contributor to review and stage the generated files. For most teams, a better rule is: the hook checks, the contributor runs conversion intentionally, and CI verifies the same policy before merge.

LocationGood responsibilityAvoid
Pre-commit hookCheck staged paths, print the exact remediation command, reject missing outputsRe-encode a whole media library on every commit
Manual commandConvert a known source folder after the contributor is ready to review outputHide generated files behind an automatic hook
CI jobRe-run policy checks and parse structured outputDepend on a developer's local hook being installed

Keep Hooks Fast#

A pre-commit hook runs at a sensitive moment. If it takes too long, contributors will look for ways to bypass it. Avoid converting entire image libraries on every commit.

Scope the hook to staged image files or specific folders. For example, a hook can inspect files under public/images/new/ rather than the whole repository.

If full conversion is slow, move it to CI and let the pre-commit hook only check that the documented command exists or that obvious mistakes are absent.

Prefer Staged-File Checks#

The hook should usually inspect what is about to be committed, not every image in the repository. A staged-file check can answer focused questions: did this commit add a large JPEG, did it include the matching WebP output, and did it place generated files in the right folder?

That keeps the feedback tied to the contributor's change. It also avoids failing a new commit because of unrelated legacy images that already existed before the work began.

A minimal staged-file detector starts from Git's index:

git diff --cached --name-only --diff-filter=ACMR |
  grep -Ei '^assets/originals/.*\.(jpe?g|png|bmp|webp|heic|heif|avif)$' || true

Use --diff-filter=ACMR so the hook looks at added, copied, modified, and renamed files. Keep the path prefix narrow. A broad expression such as .*\.(jpg|png)$ can make the hook responsible for every legacy image in the repository.

Here is the shape of a check-only hook:

#!/usr/bin/env bash
set -euo pipefail

staged_images=$(
  git diff --cached --name-only --diff-filter=ACMR |
    grep -Ei '^assets/originals/.*\.(jpe?g|png|bmp|webp|heic|heif|avif)$' || true
)

[ -z "$staged_images" ] && exit 0

missing=0

while IFS= read -r src; do
  [ -z "$src" ] && continue

  rel=${src#assets/originals/}
  base=${rel%.*}
  expected="assets/optimized/${base}.webp"

  if ! git ls-files --error-unmatch "$expected" >/dev/null 2>&1; then
    echo "Missing optimized output for $src" >&2
    echo "Expected: $expected" >&2
    missing=1
  fi
done <<< "$staged_images"

if [ "$missing" -ne 0 ]; then
  echo >&2
  echo "Run the documented conversion command, review the output, and stage the generated files." >&2
fi

exit "$missing"

This hook does not prove visual quality. It only enforces the repository contract: source files live in one folder and generated WebP files live in another.

Preserve Originals#

Hooks should never overwrite source images without a clear team decision. The same rule used in manual workflows applies here: originals are source material, generated WebP files are outputs.

Use a folder convention:

assets/originals/
assets/optimized/

Then write hooks that understand the separation. A hook can fail if an optimized file appears in the originals folder or if a source image is added without a matching output.

Use Dry Run for Preview Checks#

GetWebP CLI supports --dry-run, which makes it useful for a non-mutating hook:

getwebp ./assets/originals --recursive --dry-run

A dry run can confirm that the folder is processable without creating outputs during commit. In GetWebP CLI, --dry-run previews which files would be converted and writes nothing. The commands reference also documents --recursive, --output, --quality, --skip-existing, and --json for scripted conversion workflows.

This keeps the hook informative instead of surprising.

Do not treat dry run as proof that the final image is acceptable. It does not compare screenshots, inspect transparent edges, or prove that the chosen quality level preserves product detail.

Keep Conversion Intentional#

The command that actually writes files should be something a contributor runs deliberately:

getwebp ./assets/originals \
  -o ./assets/optimized \
  --recursive \
  --quality 82 \
  --skip-existing \
  --json > ./assets/reports/conversion.ndjson

GetWebP preserves original files; converted files are written next to sources by default or into the directory supplied with -o, --output. In a repository workflow, using --output is easier to review because generated files have a predictable destination.

--skip-existing is a convenience, not a quality guarantee. It only means "do not write a new output if the expected output path already exists." It does not prove that the existing .webp was generated from the latest source image or reviewed at the current quality setting.

If your workflow needs a repeatable evidence file, add a manifest step where your installed CLI supports it:

getwebp ./assets/originals \
  -o ./assets/optimized \
  --recursive \
  --quality 82 \
  --manifest ./assets/reports/image-manifest.json

The manifest is useful for audit trails, but it should not replace visual review for hero images, product photos, diagrams, or screenshots.

Parse Structured Output in CI, Not Human Logs#

When --json is enabled, GetWebP emits newline-delimited JSON on stdout. Human messages and warnings belong on stderr. The JSON output reference documents the envelope events, including convert.completed, convert.truncated, and convert.failed.

A CI job can fail on conversion errors without scraping terminal text:

getwebp ./assets/originals \
  -o ./assets/optimized \
  --recursive \
  --quality 82 \
  --json > ./assets/reports/conversion.ndjson

completed=$(
  grep '"convert.completed"' ./assets/reports/conversion.ndjson |
    tail -n 1
)

failed_count=$(printf '%s\n' "$completed" | jq '.data.failedCount')

if [ "$failed_count" -ne 0 ]; then
  jq -r '
    select(.type == "convert.completed")
    | .data.results[]
    | select(.status == "error")
    | "\(.file): \(.error)"
  ' ./assets/reports/conversion.ndjson >&2
  exit 1
fi

Also watch for convert.truncated if your repository includes more files than the current plan allows:

if grep -q '"convert.truncated"' ./assets/reports/conversion.ndjson; then
  echo "Image conversion was truncated before all files were processed." >&2
  exit 1
fi

This kind of parsing belongs in CI or a manual verification script more often than in a pre-commit hook. It is slower than a staged-path check, but it produces a better merge gate.

Check Output Name Collisions#

One subtle failure mode is a pair of inputs that map to the same generated filename:

assets/originals/team/photo.jpg
assets/originals/team/photo.png
assets/optimized/team/photo.webp

If both sources produce photo.webp, only one final output path can exist. GetWebP warns about output filename conflicts, but a repository hook can catch the policy problem earlier by checking staged source basenames.

duplicates=$(
  printf '%s\n' "$staged_images" |
    sed -E 's#^assets/originals/#assets/optimized/#; s#\.[^.]+$#.webp#' |
    sort |
    uniq -d
)

if [ -n "$duplicates" ]; then
  echo "Multiple staged sources would create the same optimized output:" >&2
  printf '%s\n' "$duplicates" >&2
  exit 1
fi

This is more useful than letting the conversion command run and then trying to interpret which source won.

Avoid Hiding Visual Review#

A hook can tell you that a WebP file exists. It cannot tell you whether the image still looks acceptable in the page. Do not use a passing hook as the final quality approval.

For high-risk images, the pull request should still include review of:

  • product texture
  • screenshot readability
  • face and skin tone quality
  • transparent edges
  • responsive crops
  • brand colors

The hook protects workflow hygiene. Human review protects visual quality.

For low-risk decorative images, the review might be a quick file-size and page-load check. For high-risk assets, require before-and-after inspection in the page or component where the image appears. A pre-commit hook has no page context, no brand judgment, and no knowledge of whether a crop is still readable on mobile.

Make Bypass Rules Explicit#

Sometimes a contributor needs to bypass a hook for a legitimate reason: emergency fixes, work-in-progress commits, or files that are intentionally not optimized yet. If bypassing is allowed, document when and how to use it.

The rule should be clear enough that bypassing does not become the normal path. A hook that everyone skips is not a control; it is noise.

Practical bypass rules include:

  • allow git commit --no-verify only with a pull request note
  • require CI to run the same image policy even when the local hook is skipped
  • let maintainers approve intentional exceptions for originals that must remain unconverted
  • keep a small allowlist file instead of adding one-off shell conditions inside the hook

Git's hooks documentation explains how hooks are executed. Google's WebP documentation provides background on the format used by generated outputs.

A good hook failure should tell the contributor exactly what happened:

New image added without optimized WebP output:
assets/originals/team-photo.jpg

Run:
getwebp ./assets/originals -o ./assets/optimized --quality 82
Then review and stage the generated file.

Do not print a vague "image check failed" message. Pre-commit hooks should reduce confusion.

The strongest hook message includes the source path, expected output path, exact command, and review step. If the remediation requires a license, plan tier, or CI secret, say that directly instead of sending the contributor into trial and error.

Used carefully, pre-commit hooks make image optimization more consistent. Used aggressively, they create hidden work and slow commits. Keep them narrow, staged-file based, non-destructive by default, and tied to a visible review process.

Jack avatar

Jack

GetWebP Editor

Jack writes GetWebP guides about local-first image conversion, WebP workflows, browser compatibility, and practical performance checks for teams that publish images on the web.