JPEG metadata removal · Docker · GPL-3.0

scrubexif

High-trust EXIF removal with a transparent supply chain. Two-stage pipeline: byte-level APP wipe via jpegtran, then allowlisted EXIF rebuild via ExifTool. Closes the gap parser-based tools leave open.

⬡  Promise: scrubexif will not write an unscrubbed JPEG into an output directory. If a scrub fails for any reason, no output file is created for that image.
tag CI license docker pulls base image cosign
▶ Quick Start GitHub Docker Hub Full Docs ↗

mechanism

Two-stage pipeline

STAGE 1
jpegtran
Byte-level strip of all JPEG APP segments — including unknown and proprietary vendor segments invisible to parsers
STAGE 2
ExifTool
Writes back a small allowlist: exposure, ISO, focal length, orientation. Everything else stays gone.
OUTPUT
Clean JPEG
No GPS. No serial numbers. No maker notes. No private metadata. ICC color profile re-embedded.

Parser-based tools can only see segments they know about. jpegtran operates at the byte level — it removes everything regardless of format, including segments from obscure camera manufacturers. The allowlist rebuild ensures the resulting image is still technically complete and usable, without carrying anything identifying.


capabilities

What it does

🛡
JPEG-only by design
Avoids format-specific edge cases. Predictable, auditable behavior every time.
Allowlist scrubbing
Only explicitly permitted tags survive. Everything else — GPS, maker notes, serial numbers — is gone.
🎨
ICC color preserved
Color profiles are re-embedded after the jpegtran strip in normal mode. Colors stay accurate.
📁
Auto intake mode
--from-input watches a hot directory. Works with PhotoSync, rclone, FTP uploads.
🔀
Filename sanitisation
--rename replaces capture-timestamp filenames with random hex, UUID, or custom format strings.
Paranoia mode
Byte-level wipe only. Zero metadata survives — not even ICC. For when you need absolute certainty.
🔍
Dry-run & preview
--dry-run and --show-tags let you inspect before committing to any changes.
🏷
Copyright stamping
Optionally write copyright and comment into EXIF/XMP on output with --copyright.
🔒
Hardened container
Example invocations use --read-only, --no-new-privileges, and --tmpfs.

production use

In the wild

🐕
hunde.pics — dog photography by Per Jensen

Every JPEG uploaded from a camera runs through scrubexif before landing in PhotoPrism. GPS coordinates, camera serial numbers, and maker notes are stripped at intake. Only technical shooting data — exposure, ISO, focal length — is kept. The gallery is public; the metadata is not.

📷 Camera upload scrubexif --from-input PhotoPrism intake 🌐 Public gallery
Browse the gallery to see the result: hunde.pics/gallery ↗  ·  Served via a hardened HAProxy + Docker stack on a Hetzner VPS.

usage

Quick start

# Scrub all JPEGs in $PWD → writes to $PWD/output/
docker run --rm \
  -v "$PWD:/photos" \
  per2jensen/scrubexif:0.7.22

Originals are untouched in $PWD/. Scrubbed copies go to $PWD/output/.
Run is refused if output/ already exists — no accidental overwrites.

# Hardened in-place (destructive) scrub
docker run -it --rm \
  --read-only --security-opt no-new-privileges \
  --tmpfs /tmp \
  -v "$PWD:/photos" \
  per2jensen/scrubexif:0.7.22 --clean-inline

Overwrites originals in-place. Use when you want the file count to stay the same and you don't need a non-destructive copy. Container runs fully read-only.

# Fully anonymous — 8-char random hex filenames
docker run -it --rm \
  --read-only --security-opt no-new-privileges \
  --tmpfs /tmp \
  -v "$PWD:/photos" \
  per2jensen/scrubexif:0.7.22 --clean-inline --rename "%r8" --recursive

# Keep camera prefix, drop timestamp
  per2jensen/scrubexif:0.7.22 --clean-inline --rename "D80_%r6"

Filenames leak too. 2026-04-07_11-13-45.jpeg reveals exact capture time. D80_ identifies the camera body.
Tokens: %r (random hex) · %u (UUID) · %n (counter) · %Y (year) · %m (month)

# Create directories, then run auto-intake mode
mkdir input scrubbed processed errors

docker run -it --rm \
  --read-only --security-opt no-new-privileges \
  --tmpfs /tmp \
  -v "$PWD/input:/photos/input" \
  -v "$PWD/scrubbed:/photos/output" \
  -v "$PWD/processed:/photos/processed" \
  -v "$PWD/errors:/photos/errors" \
  per2jensen/scrubexif:0.7.22 --from-input

input/ → new uploads · output/ → scrubbed · processed/ → originals · errors/ → duplicates (with --on-duplicate move).
Pair with --stable-seconds for hot upload directories.


reference

Common options

FlagDescription
--from-inputAuto intake mode — watches input/ directory
--clean-inlineIn-place scrub (destructive — modifies originals)
--rename FORMATRename output files using a format string (spec ↗)
--paranoiaByte-level wipe only — zero metadata survives (no EXIF, no ICC)
--previewNo writes — view what would be changed
--dry-runSimulate the run without writing any files
--show-tagsInspect metadata before/after
-o, --output DIRWrite scrubbed files to DIR (default safe mode)
--stable-seconds NIntake stability window (hot upload directories)
--on-duplicatedelete | move — what to do with duplicate filenames
--delete-originalRemove originals after successful scrub in auto mode
--copyrightStamp copyright string into EXIF/XMP
--commentStamp comment into EXIF/XMP
--recursiveProcess subdirectories
-q, --quietNo output on success

Full CLI reference → DETAILS.md ↗


trust & verification

Supply chain transparency

Every release image is cryptographically signed using cosign keyless signing via the Sigstore public infrastructure. The signature is tied to the exact GitHub Actions run — no long-lived signing keys exist anywhere.

cosign signature
Keyless signing via Sigstore. Tied to the exact CI run that built the image.
SPDX SBOM
Software Bill of Materials attached to each GitHub Release and as a signed attestation.
Grype scan
Vulnerability scan on every release. Releases blocked on any high or critical CVE.
build-history.json
Every release tracked: Git commit, image digest, Grype counts, Rekor log entry, CI run URL.

VERIFY ANY RELEASE — requires cosign

cosign verify per2jensen/scrubexif:0.7.22 \
  --certificate-identity-regexp="https://github.com/per2jensen/scrubexif" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

built on

Acknowledgements

ExifTool by Phil Harvey — metadata extraction and selective tag write-back (GPL-1.0-or-later / Artistic)  ·  jpegtran from libjpeg-turbo — lossless byte-level JPEG transformation  ·  cosign — image signing  ·  Syft — SBOM generation  ·  Grype — vulnerability scanning  ·  Ubuntu 24.04 — base image