What it promises, and what it cannot.
A privacy tool that overclaims is worse than one that undersells. This page states the guarantees precisely enough to be falsified, and then states the limits with the same care.
The four guarantees
1. The pixels are not touched
Everything from the SOS marker to end of file is the entropy-coded image data, and it is copied verbatim in a single operation. There is no decoder in the program and no encoder either, so there is no code path that could alter a pixel even by accident. The claim is checkable in two commands — here is how — and the test suite verifies it four ways, locating the SOS offset with an independent scanner rather than asking ScrubPony where it thinks the scan starts.
2. The write is atomic
Every write goes to a temporary file beside the destination, is fsynced, and is renamed into place only once it is complete. A crash, a full disk, or a kill signal leaves the destination either untouched or fully written, never half of each — and never a stray temporary file. tests/run_interrupt.sh asserts this by SIGKILLing a 24 MB in-place scrub thirty times at varying moments and checking that nothing but those two states ever appears.
Nothing is written at all if the input turns out to be malformed partway through. A partially scrubbed file is worse than no file, because it looks finished.
3. The policy fails closed
An APPn segment whose identifier is not recognised is dropped, not kept. An undocumented vendor segment is exactly where a manufacturer parks a serial number, and a privacy tool that fails open is not a privacy tool. If that ever breaks a decoder somewhere, it is a bug report worth receiving and a new row in the policy table — not a reason to invert the default.
Identification is by the string at the front of the payload, never by the marker number. APP1 is EXIF or XMP; APP2 is an ICC colour profile or an MPF block that can carry an entire second photograph. Getting that distinction wrong is how a scrubber either wrecks your colours or quietly leaves behind the coordinates it promised to remove.
4. It will not wander
Symbolic links are never followed — not files, not directories, not even one named directly on the command line. A symlink loop is an infinite walk, and a symlink out of the tree is a way to modify files you did not mean to touch. Recursion stops at 64 levels. With -d, the source and output trees may not nest in either direction, and that is refused rather than allowed to loop.
What in-place editing costs
-i is safe in the sense that matters — the file is never left half-written — but replacing a file installs a new inode, and three things ride on the old one:
| Cost | Detail | Avoid it by |
|---|---|---|
| Extended attributes lost | Finder tags, Spotlight comments, quarantine flags. | Using the default sibling output or -d, then moving results into place yourself. |
| Hard links broken | Other links to the old inode keep the unscrubbed content. | The same. |
| mtime reset | Deliberate. The modification time is metadata too. | Nothing — this one is the point, not a side effect. |
Permissions and ownership are carried onto the replacement: a file that was 0600 does not come back world-readable. And a file that would not change is not touched at all — same inode, same mtime — decided by comparing bytes rather than counting segments.
What it cannot do
These are not gaps waiting to be closed. They are the edges of the problem this tool takes on, and knowing them is part of using it correctly.
It removes container metadata, not information
ScrubPony strips the labelled blocks that sit around a JPEG's image data. It cannot touch anything encoded in the pixels themselves: an invisible watermark, a reflection in a window, a visible street sign, a face, a screen in the background with your email open. A scrubbed photo is a photo with no EXIF. It is not an anonymous photo, and treating it as one is the most likely way to be hurt by a tool like this.
It is 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 rather than mangled — the SOI marker decides, not the file extension, so a .jpg that is really a PNG is correctly identified as not a JPEG.
It does not defend against your filesystem
The atomic rename guarantees that no observer sees a half-written file. It does not guarantee that the original bytes are unrecoverable from the disk afterwards — on a copy-on-write or log-structured filesystem, on an SSD with wear levelling, or anywhere with snapshots or Time Machine, the pre-scrub content may persist in blocks the filesystem no longer references. If your threat model includes physical access to the drive, scrubbing is not the last step.
It does not defend against what already left
If a GPS-tagged photo has already been uploaded, emailed, or backed up, scrubbing the local copy changes nothing about the copies elsewhere. Scrub before you share, which is why the tool is built around "the folder I am about to upload" rather than "my whole library, eventually".
The default is not maximum paranoia
ICC colour profiles, JFIF and Adobe segments are kept by default, because dropping them changes how the picture looks. An unusual ICC profile does narrow down the software or the device that produced a file, so if that matters to you, --strict drops all three — at the cost of colour fidelity, and for CMYK or YCCK images, correct colour interpretation.
Similarly, the 36-byte orientation block ScrubPony writes back is itself a small signal: it says the photograph was taken with the phone held a particular way. --no-keep-orientation drops it, and your rotated photos will then display sideways. Both defaults chose "usable" over "absolute", on purpose and in the open.
No Windows
O_NOFOLLOW, POSIX permission bits and rename-over-an-existing-file all behave differently there, and those are exactly the things guarantees 2 and 4 rest on. Porting would mean weakening them quietly, so it is out of scope by design. The BSDs share the POSIX surface and should work untouched, but they are untested, which is not the same as unsupported.
How the guarantees are held up
- 741 checks across 7 test binaries, plus 8 integration suites, every binary running under AddressSanitizer and UndefinedBehaviorSanitizer.
- 7,575,421 libFuzzer executions against the segment parser, the policy, the prefix labelling and the EXIF reader, with zero crashes and zero leaks. The EXIF reader is handed raw input directly as well, because most random bytes never survive as far as a valid
APP1. - 30 SIGKILLs mid-write on a 24 MB in-place scrub, with zero partial files and no leftover temporaries.
- 13/13 fixtures agreeing with
exiftoolon segment structure and orientation — an independent implementation, not a second opinion from the same code. - Zero warnings under
-Wall -Wextra -Wpedantic -Wconversionon gcc and clang, on both platforms. - CI on every push runs the full suite on macOS and Linux with both compilers, plus a timed libFuzzer job.
All of it is reproducible from the repository with make check, and the pixel claim specifically is two commands away.
Reporting a problem
Bug reports are welcome, particularly ones that come with the file that caused them — a JPEG this misreads is worth more than a description of how it misreads it. Strip it first if it is yours; scrubpony -i will do that, which is either reassuring or circular depending on the bug.
For anything you would rather not put in a public issue, write to NorseHorse@norsehor.se — encrypted if you like, with the PGP key. One developer reads everything.