Strip the metadata. Keep the photo.
A photo off your phone carries GPS coordinates accurate to a few metres, the capture time, the device serial, and often an uncropped thumbnail of the thing you cropped out. ScrubPony removes all of it and provably does not touch the image — every byte from the SOS marker onward is copied through unchanged.
$ scrubpony -r -d ~/clean ~/Pictures scrubpony: 834 scrubbed, 366 already clean, 3 not JPEG, 4 skipped, 2 failed scrubpony: 1148605 bytes of metadata removed scrubpony: 257 carried GPS coordinates scrubpony: 266 rely on EXIF orientation to display correctly (266 preserved)
That GPS line is the one people react to. It prints even when nothing went wrong.
It reads the labels, not the numbers.
A JPEG is a run of marked segments followed by the image itself. ScrubPony walks the segments, decides each one on the identifier at the front of its payload, and copies the image through untouched.
Walk the segments
Every APPn and COM segment before the scan is parsed: its marker, its length, and the identifier string at the front of its payload. Nothing is decoded, nothing is decompressed. A malformed file stops the run for that file before anything is written.
Decide on the identifier
APP1 is either EXIF or XMP. APP2 is either an ICC colour profile or an MPF block that can hold an entire second photograph. One of each pair is kept and the other dropped, and the only thing that tells them apart is the identifier. The marker code never decides.
Copy the picture through
Kept segments are written back byte for byte. Everything from the SOS marker to the end of file — the entropy-coded image data — is copied in one operation. The result goes to a temporary that is fsynced and renamed into place.
Nothing is removed until you have read the list.
-n is a dry run: it prints the decision and the reason for every interesting segment, writes nothing, and touches nothing. It is the fastest way to understand what is inside your own photographs.
$ scrubpony -n kitchen_sink.jpg kitchen_sink.jpg: 10059 bytes action marker payload contents drop APP12 9 Ducky (camera and editor junk) keep APP0 14 JFIF drop APP1 1958 EXIF (GPS, timestamps, device serial, thumbnail) drop APP13 58 Photoshop/IPTC (captions, credits, location names) drop APP1 2879 XMP (editing history, creator, sometimes GPS again) keep APP2 602 ICC profile drop COM 20 comment (free text) EXIF: big-endian byte order, 8 entries in IFD0, GPS present, thumbnail present, orientation 8 (rotated 270 CW) would remove 5 segments, 4944 bytes (49.2% of file) orientation would be preserved in a 36-byte EXIF block
Two APP1 segments, same marker code, both dropped — one is EXIF and one is XMP. Two APP2 segments elsewhere in the corpus, same marker code, opposite fates.
Small tool, specific promises.
Each of these is an engineering decision with a reason behind it, not a feature bullet.
The marker never decides
Identification is by the string at the front of the payload, not the segment's number. Getting that wrong is how a scrubber either wrecks your colours or quietly leaves behind the coordinates it promised to remove.
It fails closed
An unrecognised APPn segment is dropped by default, because an undocumented vendor block is exactly where a manufacturer parks a serial number. A privacy tool that fails open is not a privacy tool.
Portraits stay upright
The original EXIF block goes and a 36-byte replacement holding one field — the orientation — is written back. It is the only place the program writes metadata, and the field identifies nobody.
Losslessness you can check
The scan is copied verbatim, and tests/verify_scrub.py proves it four ways — including locating the SOS offset with an independent scanner, because using the tool under test to find the bytes that vindicate it would prove nothing.
In-place writes are atomic
Temporary beside the destination, fsynced, renamed over the top. Kill it at any moment and the file is either exactly what it was or exactly what it should become — asserted by SIGKILLing a 24 MB scrub thirty times.
Batch-first
Nobody scrubs one photo; they scrub the folder they are about to upload. Whole trees, mirrored output directories, per-file error isolation, symlink refusal, deterministic ordering so a dry run is diffable between runs.
A verdict for scripts
--check changes nothing and puts the answer in the exit code, so it drops into a pre-commit hook or an upload pipeline. It counts identifying segments, so a photo scrubbed once does not report dirty forever.
It tells you what it found
Not "done" — a count. How many files carried GPS, how many depend on orientation, how many bytes went. The summary prints even when everything succeeded, because that is the part worth reading.
Nothing to install but a compiler
C99 and POSIX.1-2008. No configure step, no package tree, no runtime. make and you have a binary; exiftool and Python are optional and only used by parts of the test suite, which skip cleanly without them.
2,251 lines of C. More lines of tests.
Every input this program reads was written by somebody else, so the read path is fuzzed rather than merely tested. These numbers are from the released 1.0 and are reproducible with make check.
checks across 7 test binaries, plus 8 integration suites
libFuzzer executions with zero crashes and zero leaks
SIGKILLs mid-write on a 24 MB file, zero partial files
fixtures agreeing with exiftool on structure and orientation
warnings under -Wall -Wextra -Wpedantic -Wconversion, gcc and clang
for a 1,200-file nested library; 2.9 s in place
// don't take our word for it
Every test binary runs under AddressSanitizer and UndefinedBehaviorSanitizer, and CI runs the full suite on macOS and Linux with both compilers on every push. The one claim you should check yourself is pixel identity — here is how to do that in two commands.
The honest list.
Overclaiming on a privacy tool is worse than underselling one. None of the following is a to-do item being politely deferred; they are the edges of what this program is for.
- JPEG only. PNG, HEIC, RAW, TIFF and video are out of scope today. Mixed folders are handled — non-JPEGs are reported and skipped, never mangled.
- It does not anonymise a photo. Container metadata goes; anything encoded in the pixels stays — invisible watermarks, a reflection in a window, a visible street sign, a face.
- No Windows. By design, not by neglect: the safety guarantees rest on POSIX semantics that differ there. The BSDs are untested rather than unsupported.
-
-iloses extended attributes and breaks hard links. Finder tags, Spotlight comments and quarantine flags live on the inode, and replacing a file installs a new one. - The mtime is reset, deliberately. It is metadata too. Lead with it rather than let people discover it.
-
ICC colour profiles are kept by default. Dropping them shifts colour.
--strictdrops them; the default is not maximum paranoia and does not pretend to be. - No editing, no re-encoding, no resizing. Removing is a much smaller problem than writing, and keeping the scope there is what makes the pixel claim worth making.
- No GUI. It is a command line tool that reads directories. If you want to click something, this is not it.
Common questions.
What does ScrubPony actually remove?
Does it re-encode the photo?
Will my portrait photos come out sideways?
What formats does it handle?
Does it anonymise a photo?
Is editing in place safe?
Why is the modification time reset?
Can I use it in CI or a pre-commit hook?
Does it run on Windows?
What licence is it under?
The folder you are about to upload.
Point it there first. It takes one command and 3.2 seconds for twelve hundred photographs.
Install ScrubPony