// guarantees

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:

CostDetailAvoid it by
Extended attributes lostFinder tags, Spotlight comments, quarantine flags.Using the default sibling output or -d, then moving results into place yourself.
Hard links brokenOther links to the old inode keep the unscrubbed content.The same.
mtime resetDeliberate. 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 exiftool on segment structure and orientation — an independent implementation, not a second opinion from the same code.
  • Zero warnings under -Wall -Wextra -Wpedantic -Wconversion on 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.