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 (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:
magick generated.png -strip clean.png
Option B — use c2pa tooling directly. The c2patool 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:
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:
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 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?