Every flag, and why it exists.
ScrubPony has four modes and a dozen options. This page is the whole surface; there is nothing configurable that is not on it.
Synopsis
Paths may be files or directories, in any number and any mix. A bare directory covers the files directly inside it; -r descends. A failure on one file never stops the run — the exit code reflects the worst outcome, but only after every other file has been attempted.
At a glance
scrubpony photo.jpg writes photo.scrubbed.jpg scrubpony a.jpg b.jpg c.jpg many files at once scrubpony -l photo.jpg list segment structure, write nothing scrubpony -r ~/Pictures/upload recurse a whole directory tree scrubpony -r -d ~/clean ~/Pictures/upload mirror the tree into ~/clean, same names scrubpony -i -r ~/Pictures/upload in place, atomically, per file scrubpony -n -r ~/Pictures dry run: what would be removed scrubpony --check -r ~/Pictures exit 2 if anything carries metadata scrubpony --strict photo.jpg also drop ICC, APP14, APP0 scrubpony --no-keep-orientation photo.jpg drop the orientation tag as well
Options
| Flag | Meaning |
|---|---|
| -n | Dry run: report what would be removed, write nothing. Prints the keep/drop decision and reason for every non-structural segment, the EXIF summary, and the total that would go. |
| --check | Report only; exit 2 if any file carries identifying metadata. Quiet about clean files — the useful output is the short list of the ones that are not. |
| -l, --list | List the file's segment structure and say nothing about policy: offsets, markers, payload lengths, identifiers. Writes nothing. |
| -v | Verbose: show every decision, including the structural segments the dry run hides, and mention skipped outputs and leftover temporaries. |
| -i | Edit in place: replace the original. Still a write-then-rename. Cannot be combined with -d. |
| -f | Overwrite an existing output. Without it, an occupied output path is an error rather than a silent clobber. |
| -r | Recurse into directories. |
| -d DIR | Mirror the source tree under DIR, keeping filenames, instead of writing .scrubbed files beside the inputs. |
| --strict | Also drop JFIF, ICC and Adobe segments. Costs colour fidelity; see the policy table below. |
| --no-keep-orientation | Do not re-emit the EXIF orientation. Rotated photos will display sideways. |
| -h, --help | Usage text. |
| -V, --version | Print scrubpony 1.0.0. |
| -- | End of options. Everything after it is a path, even if it starts with a dash. |
The four modes
Exactly one of these is in effect on any run. --check takes precedence over -l, which takes precedence over -n; with none of them set, the tool scrubs.
| Mode | Reads | Writes | For |
|---|---|---|---|
| scrub | yes | yes | The job. Sibling output, mirrored tree, or in place. |
| -n | yes | no | Understanding what is in your own files before you touch them. |
| --check | yes | no | Scripts. The verdict is in the exit code. |
| -l | yes | no | Container structure with no opinion attached — what the golden tests compare against exiftool. |
Where the output goes
By default, beside the input: photo.jpg becomes photo.scrubbed.jpg. Fine for one photo and unpleasant across four thousand, where it leaves a tree of double the size with two versions of everything interleaved.
With -d DIR, the source tree is mirrored under the target, filenames unchanged, and the originals are never touched. This is the mode to reach for on a whole library. The two trees may not nest in either direction; that is refused rather than allowed to loop — an output directory inside the source means the second run reads what the first one wrote, and a source inside the output means scrubbing the scrubbed copies. Both are loops that look like progress.
With -i, the destination is the source. The temporary still lands beside it and is renamed over the top, so the original is replaced in one indivisible step or not at all. -i and -d ask for opposite things and are refused together.
Segment policy
This is the whole decision table. The marker code is never sufficient: APP1 is EXIF or XMP, APP2 is an ICC profile or an MPF block, and the identifier string at the front of the payload is what tells them apart.
| Marker | Identifier | Default | --strict | Why |
|---|---|---|---|---|
| APP0 | JFIF | keep | drop | Density fields are weakly fingerprintable but harmless, and some decoders expect an APP0. |
| APP0 | JFXX | drop | drop | Not the same as JFIF despite the shared marker: the extension block exists to carry a thumbnail. |
| APP1 | EXIF | drop | drop | GPS, timestamps, device serial, thumbnail. |
| APP1 | XMP | drop | drop | Editing history, creator, sometimes GPS again. |
| APP1 | XMP (extended) | drop | drop | The continuation blocks of a large XMP packet. |
| APP2 | ICC profile | keep | drop | Dropping it visibly shifts colour, so it survives by default. |
| APP2 | MPF | drop | drop | Can embed a second, unscrubbed image. |
| APP2 | FlashPix | drop | drop | Camera-specific, can embed a thumbnail. |
| APP12 | Ducky | drop | drop | Camera and editor junk. |
| APP13 | Photoshop/IPTC | drop | drop | Captions, credits, location names. |
| APP14 | Adobe | keep | drop | Dropping it can break CMYK and YCCK colour interpretation. |
| COM | comment | drop | drop | Free text. |
| APPn | unrecognised | drop | drop | Default deny. An unrecognised vendor segment is exactly where a manufacturer hides a serial number. |
| — | structural | keep | keep | SOI, SOF, DHT, DQT, DRI, SOS and the scan itself. The picture. |
// 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. --strict drops all three, at the cost of colour fidelity and, for CMYK or YCCK images, correct colour interpretation. Choose it deliberately rather than by reflex.
The 36 bytes it writes back
Phones do not rotate pixels. They store the sensor's native orientation and record an EXIF tag telling viewers to turn the image before displaying it. Dropping EXIF drops that instruction, which is why most metadata strippers leave you with a folder of sideways photographs.
ScrubPony drops the original EXIF block entirely and writes back a minimal replacement carrying exactly one field:
That is the only place this tool writes metadata rather than removing it. The justification is that the block is 36 bytes, holds one field, and that field is not identifying — it says which way up the picture goes, which anyone can see by looking at it. --no-keep-orientation drops it too, for whom the angle the phone was held at is itself a signal.
Nothing is emitted when the orientation is 1 or absent. Spending 36 bytes to say "already upright" would be metadata for its own sake.
Editing in place
-i replaces the original. It is still a write-then-rename: the scrubbed copy is built beside the file, synced, and moved over the top in one indivisible step. Kill the process at any moment and the file on disk is either exactly what it was or exactly what it should become — never a partial of either.
Permissions and ownership are carried onto the replacement. A file that was 0600 does not come back world-readable.
A file that would not change is not touched at all — same inode, same mtime. That is decided by comparing the bytes, not by counting segments: a photo scrubbed once still carries our own orientation block, so a second pass drops one segment and writes an identical one back. Only the bytes settle it.
Three things -i costs you, all of them consequences of replacing the file rather than editing it:
- Extended attributes are lost. Finder tags, Spotlight comments and quarantine flags live on the inode, and the rename installs a new one.
- Hard links are broken. Other links to the old inode keep the unscrubbed content.
- The mtime is reset, deliberately — it is metadata too.
If any of those matter, use the default sibling output or -d and move the results into place yourself.
Checking without changing
--check reports and changes nothing, putting the verdict in the exit code so it can go in a pre-commit hook or an upload script:
$ scrubpony --check -r ~/Pictures/to-upload ~/Pictures/to-upload/IMG_4471.jpg: 3 segments of identifying metadata, including GPS ~/Pictures/to-upload/scan.jpg: 1 segment of identifying metadata scrubpony: 2 of 118 carry identifying metadata (0 not JPEG, 0 skipped, 0 failed) $ echo $? 2
A photo scrubbed once still carries the 36-byte orientation block, and policy still drops it, so counting dropped segments would report every rotated photo as dirty forever and the hook would get switched off within a week. --check counts identifying segments: a block that matches the bytes this tool emits, exactly, is not one of them. Anything with a second tag, a GPS pointer or a thumbnail is.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success, or nothing to do. |
| 1 | Operational error: unreadable, malformed, write failed. |
| 2 | --check only: identifying metadata was found. |
| 64 | Usage error. |
An error outranks a dirty verdict even though 2 is the larger number. A script that sees 2 concludes "found metadata, scrub it"; if a file was also unreadable, that is the thing it needs to know first, and a plain maximum would have hidden it.
Directories, symlinks and ordering
- Directories are first-class. Point it at a folder, not a file. A bare directory covers the files directly inside it;
-rdescends, to a maximum of 64 levels. - Symlinks 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.
- Directory entries are sorted before use, so a dry run is diffable between runs.
- A second pass skips its own output. Running twice over the same folder does not produce
photo.scrubbed.scrubbed.jpg; leftover temporaries are skipped too, and both are only mentioned under-v. - Non-JPEG files are reported and skipped, decided by the SOI marker rather than the extension. A
.jpgthat is really a PNG is not a JPEG here. - Per-file error isolation. One unreadable or malformed file does not stop the run.
What happens when something goes wrong
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. On any failure the temporary is discarded and the original — or the pre-existing destination — is left exactly as it was.
A run whose output would be larger than its input — beyond the exact size of the orientation block it may add back — is treated as a bug and refused rather than published: scrubbing only removes bytes.
Where to go next
- Verify the pixel claim yourself — two commands, no trust required.
- The guarantees — atomicity, the fail-closed policy, and what is deliberately out of scope.
- Guides — task-shaped walkthroughs for the four modes.
- Glossary — EXIF, XMP, MPF, ICC, SOS and the rest, in plain terms.