Version 1.0.0 — source available, MIT

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.

JPEG only, and it says so zero dependencies beyond libc macOS and Linux
a real run
$ 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.

// how it works

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.

01

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.

02

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.

03

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.

// see it first

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
$ 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.

// what you get

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.

// how far it has been pushed

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.

741

checks across 7 test binaries, plus 8 integration suites

7,575,421

libFuzzer executions with zero crashes and zero leaks

30

SIGKILLs mid-write on a 24 MB file, zero partial files

13 / 13

fixtures agreeing with exiftool on structure and orientation

0

warnings under -Wall -Wextra -Wpedantic -Wconversion, gcc and clang

3.2 s

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.

// what it does not do

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.
  • -i loses 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. --strict drops 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.
// questions

Common questions.

What does ScrubPony actually remove?
EXIF (which is where GPS coordinates, capture timestamps, device model and serial number, and the embedded thumbnail live), XMP and extended XMP, Photoshop/IPTC records, JFXX thumbnails, MPF blocks that can hold a second whole photograph, FlashPix, Ducky, JPEG comments, and any APPn segment it does not recognise. ICC colour profiles and the JFIF and Adobe segments are kept by default because dropping them changes how the picture looks; --strict drops those too.
Does it re-encode the photo?
No. Everything from the SOS marker to the end of the file is the entropy-coded image data, and ScrubPony copies it verbatim in a single operation. There is no decode step and no encoder in the program, so there is nothing that could alter a pixel. The test suite checks this four ways and finds the SOS offset with its own scanner rather than asking ScrubPony where it thinks the scan starts.
Will my portrait photos come out sideways?
No. Phones do not rotate pixels — they record an EXIF orientation tag and let the viewer turn the image, which is why most metadata strippers leave you a folder of sideways photographs. ScrubPony drops the original EXIF block and writes back a minimal 36-byte replacement carrying exactly one field: the orientation. That is the only place in the program that writes metadata rather than removing it. Pass --no-keep-orientation if you would rather not have even that.
What formats does it handle?
JPEG only. PNG, HEIC, RAW, TIFF and video are out of scope today. Point it at a folder of mixed files and the non-JPEGs are reported and skipped, not mangled.
Does it anonymise a photo?
No, and the distinction matters. ScrubPony removes container metadata. It cannot touch anything encoded in the pixels themselves — an invisible watermark, a reflection in a window, a street sign, a face. A scrubbed photo is a photo with no EXIF, not an anonymous photo.
Is editing in place safe?
The write itself is: the scrubbed copy is built in a temporary beside the file, fsynced, and renamed over the top in one indivisible step, so a crash or a kill leaves the file either exactly what it was or exactly what it should become. What -i costs you is separate and worth knowing up front — extended attributes such as Finder tags and Spotlight comments are lost, hard links are broken, and the modification time is reset. All three follow from replacing the file rather than editing it.
Why is the modification time reset?
Deliberately, because the mtime is metadata too. A scrubbed file still carrying its original capture time gives away part of what you were removing. People are surprised by this, so it is stated here rather than left to be discovered.
Can I use it in CI or a pre-commit hook?
That is what --check is for. It reports and changes nothing, and puts the verdict in the exit code: 0 if everything is clean, 2 if any file carries identifying metadata, 1 if something went wrong. An error outranks a dirty verdict even though 2 is the larger number.
Does it run on Windows?
No, and that is a design decision rather than a to-do. The safety guarantees rest on O_NOFOLLOW, POSIX permission bits and rename-over-an-existing-file, all of which behave differently there. The BSDs should work untouched — same POSIX surface — but they are untested rather than supported.
What licence is it under?
MIT. The whole tool is public at github.com/norsehorse-dev/ScrubPony: 2,251 lines of C, against a test suite with more lines in it than the program, which is small enough to read in an afternoon if you want to know exactly what it does to your photographs.

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