Detecting and Stripping AI Metadata (C2PA, EXIF, XMP) from Generated Images — A Developer's Guide A developer's guide explains how to detect and strip AI provenance metadata (C2PA, EXIF, XMP) from generated images using tools like ExifTool, ImageMagick, sharp, and Pillow. The guide highlights that re-encoding pixels is the most reliable way to remove bound C2PA manifests, and emphasizes verification after stripping. If you ship anything that touches AI-generated images — a thumbnail pipeline, a user-upload feature, a design tool — you've probably noticed something: the images your model spits out are heavier than they should be, and they carry baggage you never asked for. That baggage is provenance metadata. Modern generators GPT Image / DALL·E, Google's Nano Banana / Gemini, Midjourney, many hosted Stable Diffusion endpoints stamp each output with tags that mark it as machine-made. Some of it is harmless. Some of it survives a Photoshop round-trip. And most developers have no idea it's even there until a downstream platform flags an image or a QA person asks "why does this PNG have a certificate chain in it?" This is a hands-on guide to seeing that metadata and removing it — from the CLI, from Node, from Python, and when you just want it gone from the browser. There are four layers worth knowing about, because they don't all come off the same way: Software , ImageDescription , or a custom Make / Model to identify themselves. Trivial to read, trivial to strip.The mistake I see repeatedly: someone runs a one-liner that clears EXIF, sees "no EXIF" in their viewer, and assumes the image is clean. The C2PA manifest and XMP packet are often still sitting there. Install ExifTool https://exiftool.org/ brew install exiftool , apt install libimage-exiftool-perl , etc. and dump everything: exiftool -G1 -a -s generated.png -G1 shows the group each tag belongs to, -a allows duplicates, -s uses short tag names. On a fresh AI export you'll typically see groups like ExifIFD , XMP-xmp , and — the tell — a JUMBF or C2PA-related group. To specifically probe for a provenance manifest: exiftool -jumbf:all -a generated.png If that returns anything, you have an embedded C2PA manifest, not just plain EXIF. The blunt instrument: exiftool -all= -overwrite original generated.png -all= sets every writable tag group to empty. This reliably clears EXIF and XMP. Re-run your exiftool -G1 -a -s check and confirm those groups are gone. Caveat: -all= operates on tags ExifTool knows how to write. Depending on your build and the file, the C2PA/JUMBF payload may not be fully removed by this alone — which is why you verify instead of trusting. A signed C2PA manifest is deliberately sticky. Two reliable ways to get rid of it: Option A — re-encode the pixels. A manifest is bound to specific bytes; decode the image to a raw bitmap and re-encode, and the manifest no longer validates and is dropped by most encoders: via ImageMagick — strip + re-encode in one shot magick generated.png -strip clean.png Option B — use c2pa tooling directly. The c2patool https://github.com/contentauth/c2pa-rs CLI can read and detach manifests explicitly, which is the honest way to confirm one existed and is now gone. Whichever you pick, finish with the same verification from Step 1. "It looks clean in Preview" is not verification. Most of us don't want a manual CLI step in a pipeline. Two common runtimes: Node sharp . sharp drops metadata by default when you re-encode — you have to opt in with .withMetadata to keep it. So the clean path is simply not opting in: python import sharp from "sharp"; // Re-encoding without .withMetadata produces an output with // EXIF/XMP stripped. The pixel re-encode also breaks a bound C2PA manifest. await sharp "generated.png" .png .toFile "clean.png" ; Python Pillow . Open, copy the pixel data into a fresh image, save. The new object carries no info dict from the original: python from PIL import Image src = Image.open "generated.png" clean = Image.new src.mode, src.size clean.putdata list src.getdata clean.save "clean.png" no EXIF/XMP carried over Both approaches lean on the same trick as ImageMagick's re-encode: rebuild the file from pixels so nothing rides along. Verify the output with ExifTool regardless of language — libraries change defaults across versions. The code above is great for a server-side pipeline. But there are plenty of moments where spinning up ExifTool + ImageMagick + a C2PA CLI is overkill: For those, the pragmatic move is a browser-based tool that will remove AI metadata for you. It does the full stack — EXIF, XMP, It's what I reach for when the "correct" answer wire it into the pipeline isn't worth the setup for a handful of images. You can remove AI metadata https://removeaimetadata.com from a whole batch and move on. Be honest with yourself about scope: removing metadata is not the same as removing a pixel-level watermark. SynthID-style signals are embedded in the image content, not in a header you can delete. Stripping EXIF/XMP/C2PA removes the declarative provenance — the tags that say "I was made by X" — but a robust in-pixel watermark is a different problem with different and much harder tradeoffs. Any tool, CLI or web, that promises to scrub metadata is solving the first problem, not the second. Don't conflate them. exiftool -G1 -a -s file.png to inspect; exiftool -all= for EXIF/XMP. magick -strip , sharp , Pillow or use c2patool , then Happy to hear how others handle this in their upload pipelines — do you strip on ingest, on export, or not at all?