CHANGELOG
MetaStrip is built in the open, one feature at a time, each one tested and shipped on its own. This page tracks all of it. Newest first.
Surviving a busy day on a free plan
The site went dark for exceeding a free tier allowance. Nothing here runs on a server, so the only way that happens is traffic, and traffic is metered two ways: a million edge requests a month and 100 GB of transfer. Every file a visitor asks for spends the first one, which turns page structure into an uptime problem.
- One script instead of sixteen. A cold visit was costing about twenty requests, most of them scripts, which capped the site at roughly 50,000 visits a month no matter how small those files were. The scripts are now concatenated into a single bundle at build time, taking a visit to about five requests and the ceiling to roughly 200,000.
- Still no dependency, no transpiler and no framework. The individual files stay the source of truth, the bundle is committed so the site runs on any static server with no build step, and the test suite fails if the two have drifted.
- Returning visitors now cost almost nothing. The service worker serves assets from cache rather than asking the network each time whether a version stamped file has changed.
- A 302 KB photo was being shipped for a 210 pixel circle. It is 47 KB now, which is the single largest saving here, and the share image went from 140 KB to 83 KB.
- Assets are cached for a year and marked immutable, since a release changes the URL. Pages are cached at the edge with stale-while-revalidate, so a spike is answered by the CDN instead of the origin.
- Security headers, one of which is on topic: the site can no longer be embedded on someone else's page, which is a cheap way to spend another site's bandwidth. The content security policy allows exactly what this tool does and nothing more, and was tested in a real browser against every preview type before shipping.
- OPERATIONS.md documents what a repository cannot do for itself, including the most useful fact for this particular failure: Vercel's Attack Challenge Mode is free on every plan, and requests it blocks do not count toward your usage.
A false alarm over the name of a muxer
Reported with a downloaded video whose entire metadata was Lavf62.3.100, the version string ffmpeg writes when it remuxes a file. Duration, codec and frame size are not removable and never counted, so that one string was the only thing scoring, and it turned the badge yellow.
- That is a false alarm, and false alarms are expensive here. A muxer name says a version of ffmpeg touched the file. It does not say who you are, what you shot it on, when, or where. Badging it the same as a photo carrying a home address teaches people to ignore the badge.
- Tool strings are now their own tier. The encoder atom in MP4, the muxing and writing app in Matroska, the encoder frame in ID3, the Vorbis vendor string and a PDF's Producer are all marked as trivia: still listed, still ticked, still removed, just not a reason to raise an alarm.
- Everything that points at a person or a device scores exactly as it did. A recording timestamp still counts too, because that says when you were somewhere.
- A file that now comes out CLEAN with a tool string still in it says so in a line under the table, rather than showing a row while claiming there is nothing there.
- Also reported in the same screenshot: "Codec" appearing twice with different values, which reads like a bug. A file with a picture and a sound track now says Video codec and Audio codec.
- The scoring logic moved out of the page wiring into its own file, so the one rule that decides what the tool claims about a file is now covered by tests instead of needing a browser to check.
Six posts, six questions
Ten blog cards and nine FAQ entries is a wall, not a menu. Both are cut to six by merging rather than deleting, so nothing useful was lost.
The blog
- Three evergreen posts and three about what this tool finds that others miss. Each survivor absorbed the one it replaced: Your photos know where you live gained what EXIF actually is, how to read it yourself and the settings that stop the next photo leaking; Cleaning a file without touching a pixel gained the selective-stripping section on keeping the date while losing the location.
- One new post, Raw files, PDFs and voice memos, covers all three in one place instead of two thin posts — serials and the hidden JPEG in every raw file, the two places a PDF stores your name, and the recording that is an MP4 underneath.
- Five URLs retired, and every one of them redirects permanently to the post that absorbed it, with and without the
.html. Nothing that was ever linked ends at a 404, internal links were repointed, and a crawl confirms no broken local link remains.
The questions
- Six kept, three merged away. "Can I clean a video too" folded into the formats answer, the bug-report contact folded into the open-source answer, and the Instagram question dropped because a whole post covers it properly.
- The quality answer now talks about frames and samples rather than only pixels, and names pixel redaction as the single operation here that re-encodes.
- The Blog and FAQPage structured data match the page item for item, checked rather than assumed, and the sitemap is down to eight URLs.
A STOP button for a folder run nobody had started
Reported from a screenshot: the folder progress panel and its STOP button were sitting on the page, clickable, before anything had been picked. Nothing was running, so there was nothing to stop.
- Both carried the
hiddenattribute and were visible anyway. That attribute is weaker than it looks: a browser's own stylesheet gives itdisplay: none, but as an ordinary rule with almost no specificity, so any class that setsdisplaybeats it. The panel is a flex container and every button here is an inline-flex pill, so both quietly ignored it. - The stylesheet already had a one-off
.strip-all[hidden]patch from the last time this bit, which fixed one element and left the pattern in place. Replaced with a single global rule that forces[hidden]todisplay: none, and the patch removed. - That also fixed a bug nobody had reported. Folder mode hides its own button on browsers without a directory picker, and being a pill it was staying visible on Firefox and Safari — a button that could only fail when pressed.
- Now tested rather than eyeballed: the suite asserts the global rule exists, that the panels still start hidden, and that no per-element patch has crept back in. Confirmed in a real browser, where every hidden element computes to
display: noneand measures zero by zero.
The writing catches up with the tool
Ten features shipped in a day and the site was still describing a photo cleaner. This release changes no behaviour: it is the words, the plumbing and the keyboard.
Four new posts
- What Content Credentials say about you — what is actually inside a C2PA manifest, the private corner each format hides it in, and the honest trade in removing it, since it also removes the file's proof of itself.
- Your photo has a video inside it — why motion photos defeat most cleaners, and how the real end of a JPEG is found exactly rather than guessed.
- Your PDF knows who made it, twice — the information dictionary, the XMP packet repeating it, and the identifier that links two documents you did not want linked.
- Your raw file is hiding a JPEG — serials, the name you set in the camera menu once, and the rendered preview that survives every crop.
The six older posts stop being wrong
- Each gets an "Update, August 2026" section rather than a rewrite, so it keeps its own voice while correcting what it now gets wrong: the lossless explainer covers how five other formats define "empty", the EXIF explainer covers the dialects other formats use, and the platform map notes that provenance metadata is now added on purpose rather than left behind by accident.
- All ten posts gained a canonical link, which none of them had, and a dateModified so an edited post is not read as stale.
Plumbing
- The sitemap knew about six posts and had never heard of the changelog. Now twelve pages with real dates, change frequencies and priorities.
- robots.txt says why it allows everything, and stops pointing crawlers at
/js/,/vendor/and/tests/, which are not pages. - The app's structured data was four lines describing a photo tool. It now carries the real feature list, the formats actually handled, the version, and the two facts that matter for this kind of tool: no permissions needed, nothing stored. A Blog node ties the ten posts together as one set.
- Every page gained og:url, og:site_name, twitter:title, twitter:description, an author tag and a robots directive. The blog had none of it.
- Two new FAQ entries for what people actually search: removing Content Credentials, and whether a screenshot with no metadata is safe.
Keyboard and contributors
- The redactor was a modal letting Tab wander behind it. It now traps focus while open, moves focus in on open and back out on close, and labels its canvas. Drawing needs a pointer, which is a real limit, so it says so and points keyboard users at the ordinary strip button rather than pretending.
- The linkage, receipt, storage and folder panels are labelled regions, the receipt's scrollable block is keyboard reachable, and the meter stops animating under reduced-motion.
- The test suite now runs in CI on every push, and CONTRIBUTING.md writes down the three traps in this codebase: one shared global scope, nothing may move, and never load a whole file.
Ten things, and four holes closed
MetaStrip could tell you a photo leaked your location. It could not tell you the photo had a video hidden inside it, that the raw file carried a full-size JPEG of the same picture, or that the "clean" file still held a signed record of who made it. This release is about the things a metadata tool quietly misses.
Four things that survived a clean, and no longer do
- Content Credentials (C2PA) ride in a JUMBF container that every format carries its own way: JPEG APP11 segments, the PNG caBX chunk, a top-level uuid box in MP4 and HEIF. None of those were being touched, so the badge said CLEAN while a signed provenance record with the signer's identity sat in the file. The card now shows what the record says — the tool that made it, whether the origin claim is a camera or a trained model, the certificate's common name, the named author, the edit history, how many earlier files are recorded as ingredients — and removes it on request. Removing it also removes the file's authenticity claim, which the page says out loud.
- The video inside a motion photo. Pixel and Galaxy bolt an entire MP4 onto a JPEG after the end-of-image marker, with its own GPS and camera model. Nothing in the JPEG spec says to look past the image, so the stripper was copying it straight through. The real end of the image is now found exactly rather than guessed, the tail is handed to the video parser, and if the photo is clean while the buried clip is not, the location alert still fires.
- Top-level XMP in MP4, which lives outside moov and so was never walked.
- Everything after the end of a JPEG, whatever it is, now goes.
Four new kinds of file
- Raw — DNG, CR2, CR3, NEF, ARW, ORF, RAF, PEF. A raw file is a TIFF, so the EXIF parser already read them; what needed building was a safe clean. Entries must stay sorted and the structural tags point at the image strips, so values are blanked in place and every entry header stays standing. Out comes the camera and lens serial, the photographer's name, the original file name off the card, the maker note block, and the fully rendered JPEG a raw file hides so viewers have something to show.
- PDF — Title, Author, Creator, Producer, both timestamps, the XMP packet, and the file identifier that follows a document through every save. The cross-reference table holds absolute offsets, so strings are overwritten with spaces inside their own delimiters and hex strings with zeros. Verified against a PDF printed by Chrome: same length, not one byte inside a content stream changed, every xref offset still landing on an object, and macOS's own PDF engine rendering it to identical pixels.
- Audio — MP3, M4A, WAV, FLAC, Ogg, Opus, AIFF. Three strategies, each picked by what the format allows: ID3 blocks are cut out, a WAV chunk is renamed JUNK, a FLAC block becomes PADDING. Ogg needed more, since every page carries a checksum of itself, so the page is rebuilt and its CRC recomputed. Includes the Broadcast Wave chunk, which names whoever recorded the file. Decoded samples come out bit-identical.
- AI generation data is named for what it is. A Stable Diffusion PNG carries the prompt, negative prompt, seed, sampler and model in a text chunk, and ComfyUI embeds the whole node graph; both were listed as "PNG parameters".
Beyond one file at a time
- Folder mode. Pick a folder and every supported file in it is cleaned, one at a time, so memory stays flat at ten files or a thousand. Cleaned copies go into a new metastrip-clean folder mirroring the original structure; your originals are never overwritten, and the walk skips its own output.
- Cross-file linkage. One photo leaking a camera serial is a fact about that photo. Five photos leaking the same serial ties them to one device. With two or more files open, a panel says what they share: the same serial, owner, pairing id, signing certificate, or coordinates within 100 m of each other.
- Receipts. A plain-text record of each file cleaned, with a SHA-256 before and after, for handing a file to somebody who has to trust it.
- Pixel redaction for the leak metadata cannot touch. Drag over a face, a plate, an address; blackout or pixelate, and the pixels underneath are destroyed rather than covered. No detection model is bundled, because a missed face reads as "no faces here" — where a browser has its own detector it places the first boxes. Screenshots get a warning, since they usually carry no metadata at all while the sensitive part sits in the pixels.
Underneath
- A test suite:
node tests/run.mjs, 251 checks, no dependencies and no binary fixtures — every test file is assembled byte by byte in JS, so a test can carry exactly the structure it is about. - It immediately earned its keep. These are plain scripts sharing one global scope, and two files had declared the same helper; the later one won, so PNG text values were being silently truncated at 160 characters by a function written for something else. The suite now fails on any duplicate top-level name.
- The storage meter stopped calling voice memos and documents photos.
Video support, a storage budget, and everything shown in its own shape
The first tagged release. MetaStrip started as a photo tool; this is the version where it handles what people actually share.
Video
- MP4, MOV, WebM and Matroska clips are read the same way photos are: GPS coordinates, camera make and model, recording time, software, the Live Photo pairing id, Google Photos upload ids, embedded XMP and any free-form tag a muxer left behind.
- Stripping a clip is lossless and moves nothing. Chunk offset tables inside a video point at absolute file positions, so instead of cutting bytes out, each doomed box keeps its size and becomes a "free" box with a blank payload. WebM gets the same treatment with Void elements. Frames are never re-encoded, and playback offsets stay exactly where they were. Verified on a tagged clip: decoded frames hash identical to the original.
- Nothing is ever loaded whole. Only header boxes are read, and a cleaned clip is assembled from slices of the file still sitting on disk, so a multi-gigabyte video costs almost no memory.
- The tick boxes work per field on video too, so you can drop the location and keep the recording date, or the other way round.
- Duration, frame size and codec show on the card but never count as a leak, since every player needs them. Display size and stored size are told apart, so anamorphic footage reads honestly as "1920 x 1080 (stored 1440 x 1080, anamorphic)".
- A sample leaking video sits next to the sample photos, and the installed app now accepts videos shared from the system share sheet.
Storage
- A storage budget with a live meter: 2 GB per file, 6 GB across everything open, 20 cards. Going over is refused with the real numbers in the message rather than crashing the tab, and only the clip you are watching ever plays.
- Strip all no longer tries to zip video. Photos are still bundled; clips are cleaned and saved one at a time, straight from disk.
Shape
- Previews are no longer forced into a square and cropped to fill it, which had been quietly mangling every photo that was not 1:1. Each card carries the real ratio of what it holds, taken first from the metadata and then from the decoded media, which accounts for anamorphic pixels and EXIF rotation.
- Each shape gets a column width that suits it, and anything properly widescreen takes the full card width with the metadata table underneath. A 1920x960 clip used to render at 220x110, far too small to watch. It now renders at 994x497.
- A clip takes the full row from 1.5:1 up, a photo only from 1.95:1, because one is watched and the other is glanced at while you read the table beside it. On a phone everything stacks, where a vertical clip fills the screen and a vertical photo stays modest.
Drop anywhere, zip it all, and a proper celebration
- Full-page drop zone: a photo can be dropped anywhere on the page now, not just inside the box, with a dimmed DROP IT ANYWHERE overlay while dragging.
- Strip all now bundles every cleaned photo into a single zip download instead of one browser save dialog per photo.
- A small dismiss button on each result card removes it from the list without needing to strip it first.
- A quick confetti burst celebrates a badge flipping to CLEAN. The first version fired at the thumbnail badge, which turned out to be invisible in real use since nobody is looking there right after clicking the button. Moved it to burst exactly at the button instead.
Metadata icons and the mini map, fixed properly
- Camera settings (exposure time, f-number, ISO, focal length) and pixel dimensions were incorrectly showing the clock icon. Rebuilt the tag classification so every field declares its own risk category up front instead of inheriting a shared default that only got corrected for a couple of cases.
- The mini map's pin could land anywhere within a single static map tile, not necessarily centered on the actual coordinates. Replaced it with a real interactive Leaflet map: properly centered, draggable, zoomable, with a button to snap back to the leaked location after panning around.
- The map's zoom in/out buttons looked misaligned, caused by mismatched font metrics between the "+" and "−" characters. Replaced both with small matched SVG icons drawn on the same baseline.
One logo, everywhere
- The on-page logo (white circle, black M) didn't match the favicon and app icons (orange circle, white M). Unified them into one consistent mark.
- Caught a related bug while in there: the logo circle was getting squashed into an oval on tablet and small laptop widths (641 to 1023px), because the desktop nav didn't actually fit until 1024px but the mobile menu only took over below 640px, leaving a gap where the nav overflowed and squeezed the logo.
Fixed the mobile menu, made the site findable
- Fixed the nav menu vanishing on mobile across all six blog posts, with no way back to the tool. Added a proper hamburger menu everywhere.
- Added a "try a sample photo" button, so anyone without a leaky photo handy can still see the full leak-to-clean flow.
- Added
sitemap.xml,robots.txt, and JSON-LD structured data on every page for search engines. - Ran a full mobile audit with real device viewports (320px through 390px): zero horizontal overflow anywhere, even with a photo loaded, the mini map open, and metadata checkboxes showing.
Live domain, analytics, more reading
- Moved to the live metastrip.vercel.app domain, with share previews and the README updated to match.
- Added Vercel Web Analytics, cookieless, on every page.
- Wrote three more blog posts: what EXIF actually is in plain words, how to stop your phone from geotagging every photo, and how selective stripping works under the hood.
Selective stripping, HEIC, and going installable
- Selective stripping: a checkbox next to every field found, so you can keep the date and lose the location, or any combination. JPEG entries are zeroed byte by byte, PNG chunks get their checksums recomputed, nothing else in the file shifts.
- HEIC support: iPhone photos now get read, redacted in place, or converted to a clean JPEG.
- A before-and-after comparison showing exactly what a strip removed.
- A privacy score badge on every photo: leaking, risky, or clean.
- An on-demand mini map in the GPS alert, one OpenStreetMap tile, fetched only if you ask for it.
- Paste a photo straight from the clipboard, no file picker needed.
- Strip every loaded photo at once with one button.
- Copy the cleaned photo straight to the clipboard, ready to paste into a chat.
- Installable as a PWA, with offline support and an Android share-target so a photo can be shared into MetaStrip straight from the gallery.
The look: stickers, motion, a creator you can meet
- Replaced every emoji with hand-drawn SVG stickers in the site's own palette, then gave them real dimensional depth and a die-cut white border.
- Added a soft 3D foam-textured hero background built from pure SVG filters, no image assets.
- Entrance animations on load, scroll reveals down the page, a smooth height-animated FAQ accordion, and buttons that bounce and shift color on hover, spread across the full palette instead of one repeated color.
- Added a "meet the maker" section with a round profile photo (metadata stripped with MetaStrip itself before publishing) and links out to LinkedIn, GitHub, X, and the portfolio.
- Rewrote every line of copy to sound like a person wrote it, no em dashes anywhere on the site.
The first version
- Built the core engine: a dependency-free byte-level parser reading EXIF, GPS, and PNG text chunks, plus a lossless stripper that removes metadata without ever touching a pixel.
- Drag-and-drop and click-to-upload, with live preview cards and a GPS leak alert linking straight to a map.
- Three original blog posts on photo privacy, and the what-leaks explainer cards on the homepage.
- Everything runs in the browser. No upload, no server, no account, from the very first commit.