metakill

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT

README

metakill

metakill is a single, dependency-free binary that strips identifying metadata from images, documents, audio and video: GPS coordinates, timestamps, camera and device identifiers, software/encoder strings, authors, comments, thumbnails and (by default) media ID tags.

  • Pure Go, no cgo, no external tools. Nothing is shelled out (no ffmpeg, exiftool or ImageMagick). Builds for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and windows/amd64 with CGO_ENABLED=0.
  • No network access, no telemetry. See Network for details.
  • Aggressive by default. Everything that is not needed to display or play the file is removed unless you ask to keep it.
  • Safe. Output is written to a temp file and re-validated before the original is replaced. A .bak backup is made by default.
$ metakill -n photo.jpg          # see what would be removed
$ metakill -y photo.jpg          # remove it (photo.jpg.bak is kept)
$ metakill -d -r -y ~/Pictures   # a whole tree

[!WARNING] Media ID tags are removed by default. Title, artist, album, track number and year in your music and video files are deleted unless you pass -k/--keep-tags. Players and libraries that rely on them will show "Unknown artist" afterwards. Cover art is also removed unless you pass -t/--keep-thumbnails. Keep tags are rewritten from scratch: only those five fields survive, the year is reduced to YYYY, and kept cover art has its own EXIF stripped.

Install

go install github.com/computerguymackay/metakill/cmd/metakill@latest

Building needs Go 1.26 or newer. An older go command downloads the right toolchain automatically.

Or download a binary from the releases page, or build one yourself with make build (output in bin/).

Usage

metakill [flags] FILE...
metakill -d [flags] DIR...

In file mode each argument is a file. In directory mode (-d) each argument must be a directory, and file arguments are rejected. Without -r only the top level of each directory is processed. Files are processed in lexicographic order. Unrecognised files are skipped with a warning (silenced by -q). Backups (*.bak) and metakill's own temp files are never processed.

Flags

Every flag has a short and a long form.

Short Long Type Default Purpose
-d --directory bool false Treat arguments as directories, not files
-r --recursive bool false Recurse into subdirectories (only with -d)
-n --dry-run bool false Report only; modify nothing; never prompts
-j --json bool false One JSON document on stdout instead of text
-q --quiet bool false Errors only (JSON is still printed with -j)
-o --output string "" Write the cleaned file to this path (single file only; no backup, no prompt)
-y --yes bool false Skip the confirmation prompt (required without a TTY)
-b --backup bool true Create FILE.bak before editing in place; --backup=false disables
-k --keep-tags bool false Keep Title/Artist/Album/Track/Year
-t --keep-thumbnails bool false Keep embedded thumbnails and cover art (implies -k)
-c --keep-icc bool false Keep ICC colour profiles in images
-z --no-zero-pad bool false Do not zero pre-existing padding/free-space regions
-v --verbose bool false One line per field, with its namespace
-h --help Help
--version Print the version

--version has no short form because -v is --verbose. A hidden --selftest flag runs the built-in smoke test on generated fixtures.

Exit codes
Code Meaning
0 Success, and every processed file is fully clean
1 At least one file failed, or you answered "no" at the prompt
2 Usage error (unknown flag, bad arguments, -y missing without a TTY); nothing was touched
3 No failures, but some metadata could not be removed (see fully_clean in JSON)

Skipped files never cause a non-zero exit code. This covers unrecognised files and recognised packages that metakill deliberately won't repack (such as a signed APK); the reason is printed as a warning.

Errors always go to stderr. -q hides warnings but never hides a file that is not fully clean: that is reported as an error line on stderr and with exit code 3.

Confirmation

When stdin is a terminal and -y is not given, metakill inspects everything first and asks once:

metakill: remove 25 metadata fields from 1 file(s) in place (backups on)? [y/N]

Without a terminal, -y is required (exit 2 otherwise). Dry runs never prompt, and neither do -o runs, since the original is not modified.

Paths in output

Paths are printed as given on the command line, cleaned with filepath.Clean. Directory-mode paths are the directory argument joined with the file's relative path. This keeps output reproducible (and testable) regardless of where it runs.

Output

Dry run, text
$ metakill -n photo.jpg
photo.jpg  [JPEG]
  GPS:             GPSLatitudeRef = "N", GPSLatitude = "37 46 29.64", GPSLongitudeRef = "W", GPSLongitude = "122 25 9.84", GPSAltitude = "16", exif:GPSLatitude = "37,46.494N", City = "San Francisco"
  Timestamps:      DateTime = "2023:04:12 18:33:02", DateTimeOriginal = "2023:04:12 18:33:02", DateTimeDigitized = "2023:04:12 18:33:02", xmp:CreateDate = "2023-04-12T18:33:02"
  Device:          Make = "Apple", Model = "iPhone 14 Pro", BodySerialNumber = "SN-0042-SECRET", LensModel = "iPhone 14 Pro back camera"
  Author:          Artist = "Jane Photographer", dc:creator = "Jane Photographer", By-line = "Jane Photographer"
  Software:        Software = "iOS 16.4", xmp:CreatorTool = "Adobe Photoshop 24.0"
  Thumbnail:       ThumbnailImage = <binary, 631 bytes>
  ICC profile:     ICC_PROFILE = <binary, 150 bytes>
  Custom:          UserComment = "secret comment", Comment = "secret comment", TrailingData = <binary, 25 bytes>
  Kept:            Orientation = "6"
  --- 25 fields, ~2.0 KiB removable

Fields are grouped into categories: GPS, timestamps, device, author, software/encoder strings, ID tags, thumbnails, ICC profiles and custom (everything else). Printable values are shown quoted; binary values as <binary, N bytes>. With -v each field gets its own line as namespace:key = value (for example exif:GPSLatitude = "37 46 29.64"). Files nested inside other files (images in a DOCX, a JPEG inside a PDF, cover art) are shown indented with >. After a real run the footer reads --- N fields removed, X saved.

Dry run, JSON

-j prints one document per run. The example is abridged to five fields.

{
  "dry_run": true,
  "results": [
    {
      "path": "photo.jpg",
      "format": "jpeg",
      "dry_run": true,
      "status": "cleaned",
      "fully_clean": true,
      "fields": [
        {"category": "device",    "namespace": "exif", "key": "Make",     "value": "Apple"},
        {"category": "device",    "namespace": "exif", "key": "Model",    "value": "iPhone 14 Pro"},
        {"category": "software",  "namespace": "exif", "key": "Software", "value": "iOS 16.4"},
        {"category": "timestamp", "namespace": "exif", "key": "DateTime", "value": "2023:04:12 18:33:02"},
        {"category": "thumbnail", "namespace": "exif", "key": "ThumbnailImage", "value": null, "size": 631}
      ],
      "kept": [
        {"category": "custom", "namespace": "exif", "key": "Orientation", "value": "6"}
      ],
      "bytes_removable": 2059,
      "warnings": []
    }
  ],
  "summary": {
    "files": 1, "cleaned": 1, "already_clean": 0, "skipped": 0, "errors": 0,
    "not_fully_clean": 0, "fields_removed": 25, "bytes_removable": 2059
  }
}

The schema is identical for real runs except for dry_run and the byte count label: bytes_removable becomes bytes_saved. Real runs also add backup or output where relevant. Other fields:

  • status: one of cleaned, already_clean, skipped or error. In a dry run, cleaned means "would be cleaned".
  • error: present only when status is error.
  • embedded: present only when a nested file was found.
  • Arrays are always arrays, never null.

bytes_removable is the size of the metadata found. bytes_saved is the actual change in file size. Formats cleaned in place (HEIF, MKV/WebM, AVI, WAV) zero their metadata without shrinking, so they report bytes_saved: 0.

Format support

Full means everything identifying that metakill knows about is removed, and the round-trip tests prove it. Partial means some metadata is kept by design and the file is reported with fully_clean: false (exit 3) when that happens.

Format Status What is removed Notes
JPEG Full EXIF (incl. GPS, MakerNote), XMP + extended XMP, IPTC/Photoshop, ICC (unless -c), JFXX/EXIF thumbnails (unless -t), COM, APP3–APP15 (incl. C2PA/JUMBF, MPF), trailing data (motion photos) EXIF is replaced by a minimal block holding only Orientation, so photos stay upright. JFIF and Adobe APP14 (needed for CMYK) are kept.
PNG / APNG Full tEXt, zTXt, iTXt (incl. XMP), eXIf (Orientation kept), tIME, iCCP (unless -c), private ancillary chunks, trailing data Rendering chunks (gAMA, sRGB, pHYs, cICP, APNG frames…) are kept.
GIF Full Comments, XMP, unknown application extensions, ICC (unless -c), trailing data Loop extension kept.
WebP Full EXIF, XMP, ICCP (unless -c), unknown chunks VP8X flags updated. EXIF Orientation is not kept (most decoders ignore it).
TIFF Full Every tag not needed to decode (whitelist), EXIF/GPS IFDs, XMP, IPTC, Photoshop, GeoTIFF tags, ICC (unless -c), reduced-resolution pages (unless -t) Rebuilt from scratch with relocated strips/tiles. BigTIFF, DNG and files with SubIFDs (raw/pyramidal) are refused.
HEIC / HEIF / AVIF Partial Exif and XMP items (zeroed in place) Thumbnail image items are kept, because removing them means rewriting item locations: reported not fully clean unless -t. The ICC profile (colr) is always kept (warning). Image sequences (moov) are not scrubbed.
SVG Full <metadata>, <title> (kept with -k), <desc>, comments, processing instructions, all Inkscape/Sodipodi/Illustrator/Sketch/Serif/RDF/DC/CC elements, attributes and namespace declarations (incl. sodipodi:docname, export paths) Embedded data: images are cleaned recursively. SVGZ is not supported, and Illustrator files that use custom DOCTYPE entities may fail to parse.
PDF Full* /Info (Title kept with -k), every /Metadata XMP stream, /PieceInfo, /LastModified, /SpiderInfo, annotation authors and dates, attachment dates, incremental-update history (old revisions), EXIF in embedded JPEG images The trailer /ID is regenerated. *Embedded file attachments are not scanned (reported not fully clean). Encrypted PDFs are refused. Digital signatures are invalidated (warning).
DOCX / XLSX / PPTX (+ macro and template variants) Full core.xml (all but language; title with -k), app.xml application/version/company/manager/template/total time, custom.xml, thumbnails (unless -t), comment and tracked-change authors (→ Author), initials (→ A) and dates (→ 1980-01-01), people.xml/persons user IDs, rsid session IDs, document variables, attached template paths, absolute save paths, revision IDs, Excel fileVersion build numbers Embedded media and nested documents are cleaned recursively. Relationships and content types are pruned to match. ZIP timestamps, extra fields, comments and OS clutter are removed. NuGet, VSIX and APPX packages share this layout but are skipped (see ZIP).
ODT / ODS / ODP / ODG Full meta.xml (all but language/statistics; title with -k), printer name/setup, data-source and rsid settings, tracked-change and annotation authors/dates/initials, officeooo:rsid attributes, thumbnails (unless -t), digital signatures The manifest is updated, and mimetype stays first and stored. Pictures are cleaned recursively.
EPUB Full Creator (kept with -k), contributors, publisher, rights, dates, subjects, descriptions, extra identifiers, Calibre/other <meta>, refinements of removed elements, Calibre bookmarks, iTunes plists The fields EPUB requires are kept: the unique dc:identifier, dc:title and dc:language, plus dcterms:modified, which is reset to 2000-01-01. Images are cleaned recursively.
Plain text / Markdown Recognised, no-op Nothing .txt, .text, .md, .markdown
ZIP (.zip, .cbz, unsigned .jar) Full* Member timestamps (→ 1980-01-01), extra fields (uid/gid, Unix/NTFS times), archive and member comments, OS clutter (__MACOSX/, ._* AppleDouble files, .DS_Store, Thumbs.db, desktop.ini), and the metadata inside every member a handler recognises, including nested archives Unrecognised members are copied byte-for-byte without being decompressed. Permissions are normalised to 644/755, keeping directories, symlinks and executable bits. Member names are never changed, but home-directory-looking paths produce a warning. *Members over 512 MB that a handler recognises are copied unchanged and reported not fully clean. Encrypted archives are refused. Signed or integrity-checked packages (APK/AAB, IPA, signed JAR/XPI, Python wheels, NuGet, VSIX, APPX/MSIX) are skipped, because repacking would break them.
MP3 Full ID3v2.2/2.3/2.4 (leading and appended), ID3v1 and v1 extended, APEv1/v2, Lyrics3v2, the LAME/Xing encoder version string (CRC recomputed) With -k, a fresh minimal ID3v2.4 tag holds the kept fields. Audio frames are copied unchanged.
FLAC Full Vorbis comments (vendor string blanked), PICTURE (unless -t), APPLICATION blocks, stray ID3v2/ID3v1 PADDING is kept but zeroed.
Ogg Vorbis / Opus / Theora / Speex Full Comment headers (vendor blanked), pictures (unless -t), Opus extra tag data Header pages are re-paginated, and later page sequence numbers and CRCs are rewritten. Ogg FLAC is refused. Streams with unknown codecs are reported not fully clean.
M4A / AAC (MP4 audio) Full Handled by the MP4 handler
WAV / BWF / RF64 Full LIST INFO (essentials kept with -k), bext (originator, dates, UMID, coding history), id3 chunk, iXML, aXML, cart, DISP, XMP, umid Cleaned in place: chunks become zeroed JUNK.
MP4 / M4V / MOV / 3GP Full* udta and meta boxes (iTunes, QuickTime, 3GPP, ©xyz GPS, Apple com.apple.quicktime.* keys incl. location, make, model, software), XMP and other uuid boxes, prft, mvhd/tkhd/mdhd creation and modification times, handler names, compressor names, timed-metadata tracks (GoPro gpmd, camm, Apple mebx, Sony rtmd) and their samples The file is compacted and all stco/co64/tfhd offsets are rewritten. *Encoder settings embedded inside the video bitstream (e.g. x264 SEI) are not touched.
MKV / WebM Full Title (kept with -k), DateUTC, segment UIDs/filenames, MuxingApp/WritingApp (blanked), track names and codec names, Tags (essentials kept with -k), Attachments (kept but cleaned with -t), matching SeekHead entries Cleaned in place with Void elements (like mkvpropedit). CRC-32 elements of edited masters are voided.
AVI Partial LIST INFO, IDIT/ISMP dates, stream names, vendor LISTs (e.g. Nikon ncdt) Cleaned in place with JUNK. strd codec/camera data is kept, because removing it can break decoding (reported not fully clean when present).
What -k, -t and -c keep
Format family -k keeps -t also keeps
Audio and video tags Title, Artist, Album, Track, Year (as YYYY) Cover art (cleaned)
PDF, Office, ODF, SVG Title Thumbnails (cleaned)
EPUB Creator (the book's author); title is always kept Images are content and are always kept
Images Nothing. Orientation is always kept in JPEG/PNG EXIF/JFXX thumbnails, TIFF reduced-resolution pages

-c keeps ICC profiles in JPEG, PNG, GIF, WebP and TIFF.

These are always removed regardless of flags: GPS, timestamps (except the kept year), device identifiers (make, model, serial numbers, lens, owner), and software/encoder strings.

Zero padding

Removed metadata bytes never stay in the output. Formats cleaned in place always zero what they remove. -z/--no-zero-pad only affects padding that already existed: MP4 free/skip, MKV Void, FLAC PADDING, RIFF JUNK/PAD , HEIF free. These often contain leftovers from earlier edits, and by default they are zeroed and reported.

Safety model

  1. Each file is inspected first. Nothing is written for files with nothing to remove.
  2. The cleaned copy is written to a temp file (.metakill-*.tmp) in the same directory.
  3. Post-write validation: the same handler re-opens the temp file. It must parse, and it must contain nothing that handler would remove. Otherwise the temp file is deleted and the original is left untouched.
  4. Only then is FILE.bak created (never overwriting an existing backup: an existing .bak is an error) and the temp file atomically renamed over the original. File permissions are preserved, and symlinks are followed so the link itself survives.

Because the original is never modified before validation succeeds, there is nothing to restore when validation fails.

Network

metakill makes no network calls and has no telemetry. Its own packages import nothing from net, and a test enforces this. pdfcpu, the PDF library, links net/http for features metakill never uses: signature revocation checks, link validation and remote images. metakill runs pdfcpu with Offline set and in its stateless configuration mode, so pdfcpu also never reads or creates ~/.config/pdfcpu (a test checks this).

cgo

None. Every dependency is pure Go and release builds use CGO_ENABLED=0. The only cgo use is optional, during development: make test-race runs the race detector, which requires cgo.

Dependencies

Module Why
github.com/spf13/cobra (+ pflag) CLI, short/long flags, grouped help
golang.org/x/term TTY detection for the confirmation prompt
github.com/pdfcpu/pdfcpu PDF parsing and rewriting
github.com/abema/go-mp4 ISO-BMFF box parsing (MP4/MOV/M4A/HEIF)
github.com/beevik/etree Namespace-preserving XML editing (SVG, OOXML, ODF, EPUB, XMP)

EXIF/TIFF, IPTC, ID3, APE, Vorbis comments, FLAC, Ogg, RIFF and EBML (Matroska) are handled by small in-repo parsers in internal/meta. This is because metakill needs to write those formats, not just read them. golang.org/x/image is used only in tests, to decode TIFF and WebP output.

Development

make build      # bin/metakill
make test       # unit, round-trip, golden and CLI integration tests
make lint       # gofmt, go vet, golangci-lint (if installed), govulncheck
make fixtures   # regenerate testdata/fixtures from internal/fixtures
make golden     # regenerate testdata/golden after an intentional output change
make release    # cross-compile all targets into dist/ with SHA256SUMS
make selftest   # run the built binary's --selftest

Fixtures are generated by Go code in internal/fixtures rather than checked in as opaque samples, so every planted value is visible in the source. See CONTRIBUTING.md for adding a format.

Licence

MIT. See LICENSE.

Directories

Path Synopsis
cmd
metakill command
Command metakill strips metadata from images, documents, audio and video.
Command metakill strips metadata from images, documents, audio and video.
internal
cli
Package cli orchestrates a metakill run: it collects files, picks handlers, asks for confirmation, processes each file and emits the report.
Package cli orchestrates a metakill run: it collects files, picks handlers, asks for confirmation, processes each file and emits the report.
fixtures
Package fixtures generates small files with known metadata.
Package fixtures generates small files with known metadata.
fixtures/genfixtures command
Command genfixtures writes every generated fixture into a directory (default testdata/fixtures).
Command genfixtures writes every generated fixture into a directory (default testdata/fixtures).
handlers
Package handlers holds one metadata handler per format family plus the registry that picks a handler for a file (magic bytes first, extension second).
Package handlers holds one metadata handler per format family plus the registry that picks a handler for a file (magic bytes first, extension second).
meta/ape
Package ape parses APEv1/APEv2 tags (found at the end of MP3 and other audio files).
Package ape parses APEv1/APEv2 tags (found at the end of MP3 and other audio files).
meta/classify
Package classify is the single place that maps metadata keys from every namespace (EXIF, XMP, IPTC, ID3, Vorbis comments, iTunes atoms, PDF Info, Office/ODF properties, ...) to report categories, and that decides which keys count as essential media ID tags for -k/--keep-tags.
Package classify is the single place that maps metadata keys from every namespace (EXIF, XMP, IPTC, ID3, Vorbis comments, iTunes atoms, PDF Info, Office/ODF properties, ...) to report categories, and that decides which keys count as essential media ID tags for -k/--keep-tags.
meta/exif
Package exif reads and writes TIFF-structured metadata (EXIF in JPEG, PNG, WebP and HEIF, and TIFF files themselves).
Package exif reads and writes TIFF-structured metadata (EXIF in JPEG, PNG, WebP and HEIF, and TIFF files themselves).
meta/id3
Package id3 parses ID3v1 and ID3v2.2/2.3/2.4 tags and writes minimal ID3v2.4 tags (used to re-emit the tags kept by -k).
Package id3 parses ID3v1 and ID3v2.2/2.3/2.4 tags and writes minimal ID3v2.4 tags (used to re-emit the tags kept by -k).
meta/iptc
Package iptc summarises Photoshop image resource blocks (APP13 / TIFF tag 34377) and the IPTC-IIM records inside them.
Package iptc summarises Photoshop image resource blocks (APP13 / TIFF tag 34377) and the IPTC-IIM records inside them.
meta/isobmff
Package isobmff provides box-tree helpers for ISO base media files (MP4, MOV, M4A, HEIF).
Package isobmff provides box-tree helpers for ISO base media files (MP4, MOV, M4A, HEIF).
meta/memio
Package memio provides an in-memory io.WriteSeeker.
Package memio provides an in-memory io.WriteSeeker.
meta/vorbis
Package vorbis parses and writes Vorbis comment blocks (used by FLAC, Ogg Vorbis, Opus, Theora and Speex) and FLAC PICTURE blocks.
Package vorbis parses and writes Vorbis comment blocks (used by FLAC, Ogg Vorbis, Opus, Theora and Speex) and FLAC PICTURE blocks.
meta/xmp
Package xmp summarises XMP packets as report fields.
Package xmp summarises XMP packets as report fields.
report
Package report defines the per-file and per-run result types.
Package report defines the per-file and per-run result types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL