Do not take our word for it.
Pixel identity is the strongest claim this tool makes. It is also the easiest one to check without trusting anybody, so here is how.
The two commands
$ scrubpony photo.jpg $ python3 tests/verify_scrub.py photo.jpg photo.scrubbed.jpg && echo identical identical
What those four checks are
| # | Check | What it rules out |
|---|---|---|
| 1 | exiftool reports nothing from the drop list, and an EXIF block if present contains Orientation and nothing else. |
Metadata that survived, and a rewritten EXIF block that quietly carries more than it should. |
| 2 | The entropy-coded scan is byte-for-byte identical. | Any re-encoding at all. This is the strong one. |
| 3 | Both files decode to identical pixels. | A structural change before the scan that alters how it is decoded. |
| 4 | Both files look identical once EXIF orientation is applied. | A photo that is byte-identical and still comes out sideways. |
// the part that makes it worth anything
tests/verify_scrub.py locates the SOS offset with its own segment scanner rather than asking ScrubPony where it thinks the scan starts. Using the tool under test to find the bytes that prove the tool under test is correct would prove nothing at all.
Doing check 2 by hand
If you want the byte-identity claim without running anybody's Python, it is a hex viewer and two hashes. Find FF DA — the SOS marker — in each file, take everything from that offset to the end, and compare:
The two digests match. There is no decoder in ScrubPony and no encoder either, so there is nothing in the program that could have altered them — but a hash you computed yourself is a better argument than that sentence.
The rest of the evidence
Pixel identity is the claim you can check in a minute. These are the ones that took longer to establish, all reproducible from the repository 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 in-place scrub; zero partial files
fixtures agreeing with exiftool on segment structure and orientation
warnings under -Wall -Wextra -Wpedantic -Wconversion, gcc and clang, both platforms
lines of C, against a test suite with more lines in it than the program
Fuzzing
Every input this program reads was written by somebody else, so the read path gets fuzzed rather than merely tested. tests/fuzz_jpeg.c drives the segment parser, the policy, the prefix labelling and the EXIF reader over arbitrary bytes, in memory, with no filesystem in the loop.
The second exists because a fuzzer nobody can run is a fuzzer nobody runs: it is a self-contained random mutator that needs nothing but a C compiler, so it works on a Mac without installing clang's fuzzer runtime. It finds less than libFuzzer and it finds it without ceremony. A 20,000-iteration run is part of make check so the harness cannot quietly rot.
The EXIF reader is handed the raw input directly as well as whatever the parser passes it. Most random bytes never survive as far as a valid APP1, so without that the most hostile parser in the project would barely be exercised.
Atomicity
tests/run_interrupt.sh SIGKILLs a 24 MB in-place scrub thirty times at varying moments and checks that nothing but the two legal states ever appears on disk: the file exactly as it was, or the file exactly as it should become. No partials, and no stray temporaries left behind either.
Fixtures
tests/fixtures/synthetic/ holds machine-drawn images carrying real metadata written by real tools. They are committed, because there is nothing personal in any of them, and they are what the 13/13 exiftool cross-check runs against.
tests/fixtures/real/ is gitignored and empty in the repository. Put actual phone photos there and the golden tests pick them up automatically. They are not committed for the obvious reason: they are precisely the GPS-tagged files this tool exists to keep out of public repositories.
Read the source
2,251 lines is small enough to read in an afternoon, and the parts that matter are smaller still. If you only read three files, read src/policy.c for the keep/drop decision, src/rewrite.c for the copy, and src/io.c for the write-then-rename.