Bulk EXIF and metadata removal CLI for JPEG, PNG, TIFF, and WebP. Strips GPS coordinates, camera make/model, serial numbers, timestamps, and PNG text chunks, and prints exactly which tags were removed for every file. Designed to be auditable: --dry-run shows what would happen without writing, --report json is suitable for CI, and ICC color profiles + Orientation are preserved by default so cleaned files still display correctly.
# Clone
git clone https://github.com/sen-ltd/exif-stripper
cd exif-stripper
# Build & run via Docker (no Python install needed)
docker build -t exif-stripper .
docker run --rm -v "$PWD:/work" exif-stripper --dry-run vacation/Camera phones embed a surprising amount in EXIF: GPS coordinates accurate to ~10 meters, camera serial numbers that link photos across years, timestamps to the second, and sometimes Face ID / scene metadata. Anyone publishing photos for journalism, activism, OSINT-sensitive work, or legal disclosure should scrub it first.
Existing options each have a downside:
- ExifTool is the gold standard, but it's 2+ MB of Perl with a famously baroque CLI. Overkill for a one-flag job.
- Image editors (Photoshop, Affinity, Preview) can strip metadata, but only one image at a time and only when you remember.
- Python libraries (Pillow, piexif) work, but you have to glue them together yourself and you don't get a per-file report of what came out.
exif-stripper is --dry-run-able, scriptable, batch-friendly, and small. It does one thing: walk a tree of images, scrub their metadata, and tell you exactly what it removed.
docker run --rm -v "$PWD:/work" exif-stripper --report human vacation/Scanned 12 files (9 JPEG, 2 PNG, 1 HEIC)
STRIP vacation/IMG_0421.jpg stripped 8 tags (GPSLatitude, GPSLongitude, Make, Model, +4 more) saved 4.2 KB
STRIP vacation/IMG_0422.jpg stripped 7 tags (GPSLatitude, GPSLongitude, Make, +4 more) saved 3.8 KB
STRIP vacation/sunset.png stripped 2 tags (Author, Comment) saved 0.8 KB
SKIP vacation/phone.heic HEIC write not supported
...
Total: 11 files stripped, 38 KB saved, 1 skipped, 0 failed
--report |
Use case |
|---|---|
human |
Default. Color when stdout is a TTY. |
json |
CI pipelines, scripts, audit logs. |
csv |
Spreadsheets, BI tools, batch reports for legal. |
The JSON payload is structured as {summary, files: [...]} with tags_removed, bytes_saved, icc_removed, and dry_run per file. Every action is reversible inspection — diff a --dry-run JSON against a real-run JSON and you have a complete audit trail.
| Flag | Behavior |
|---|---|
paths (positional) |
One or more files or directories. Required. |
--no-recurse |
Do not descend into subdirectories. |
--out DIR |
Write cleaned copies into DIR instead of overwriting in place. Preserves the relative directory structure under each input root. |
--dry-run |
Show what would be removed without modifying any file. |
--keep TAG[,TAG...] |
Preserve specific EXIF tags by name. Most common: --keep Orientation. |
--auto-rotate |
Physically rotate pixels by the Orientation tag, then drop Orientation. Mutually overrides --keep Orientation. |
--strip-icc |
Also remove the ICC color profile. Off by default — removing ICC can shift colors on wide-gamut displays. |
--report {human,json,csv} |
Output format. Default human. |
--no-color |
Disable ANSI color in human output. |
--version |
Print version and exit. |
| Format | Stripped | Preserved by default |
|---|---|---|
| JPEG | EXIF (Make, Model, Software, DateTime*), GPS sub-IFD, MakerNotes, Interop IFD |
ICC color profile, Orientation if --keep Orientation |
| PNG | tEXt, iTXt, zTXt chunks (XMP, Author, Comment) |
ICC color profile |
| TIFF | EXIF IFDs | ICC color profile |
| WebP | EXIF chunk, XMP | ICC color profile |
| HEIC / HEIF | read-only — files are skipped with a clear message | |
| Video, RAW (CR2/NEF/ARW) | not supported — use ExifTool |
Thumbnails embedded inside JPEG EXIF (yes, that's a thing) are dropped along with the rest of the EXIF segment because they live in the same APP1 marker.
In-place writes use the temp-file-and-rename pattern: each cleaned image is written to .foo.jpg.XXXX.tmp in the same directory, then os.replaced into the final path. If the process is killed mid-write, the original file at the target path is left intact instead of being truncated. This matters when you're scrubbing a 10,000-photo archive and your laptop runs out of battery.
| Code | Meaning |
|---|---|
| 0 | All files processed successfully (skipped is OK). |
| 1 | At least one file failed (corrupt, permission denied, write error). The rest still ran. |
| 2 | Configuration error: missing path, bad output dir, unrecognized format flag. |
pip install .
exif-stripper --dry-run ~/Pictures/vacation/Requires Python 3.10+ and Pillow.
docker run --rm --entrypoint pytest exif-stripper -qTests generate fixture images at runtime via Pillow with known EXIF (GPS, camera model, timestamps), so no binary fixtures are committed and the suite is fully self-contained.
MIT. See LICENSE.
