Communitygithub.com

Video Scrub

Rebuilds a video into a clean file with **only video and audio streams**—stripping all metadata, GPS, device info, timestamps, chapters, telemetry, and hidden identity strings.

What is Video Scrub?

Video Scrub is a zero-dependency Node.js tool that rebuilds any video file into a pristine copy containing only its video and audio streams—no metadata, device identifiers, GPS coordinates, creation timestamps, or hidden encoder fingerprints survive. It uses a whitelist approach: instead of listing what to delete (which inevitably misses something), it maps exactly two streams (one video, one audio) and discards everything else. The tool goes beyond visible tags (which ffprobe can see) to eliminate metadata hidden in H.264 SEI NAL units, AAC DSE packets, and the avc1 compressorname field—five specific hiding places, each verified with byte-level tests. It supports two modes: `--mode copy` (default, bit-perfect video pass-through with only audio re-encoded, typical speed ~1.3s for a 53-second clip) and `--mode encode` (full re-encode to also scrub fragile bitstream watermarks). Four output profiles (`obs`, `quicktime`, `screencapture`, `bare`) control what container-level tags remain. Verification is mandatory: the tool performs a byte-level scan of the output file using all metadata strings from the source as needles, flagging any match >=5 bytes. All 12 security gates (from byte residue to profile conformance) are tested by a built-in self-test suite with 117 assertions. Works on macOS/Linux, requires only Node.js and ffmpeg, no API keys.

Works with✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/eternityspring/reelbench-skills/tree/HEAD/skills/video-scrub

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

video-scrub

Rebuild a video into a clean file: video and audio carried over, not a single byte of metadata.

{baseDir} = the directory containing this file. Script at {baseDir}/scripts/video-scrub.mjs, zero dependencies, run directly with node.

Principle: whitelist, not blacklist

There are two approaches to scrubbing metadata—live or die:

ApproachProblem
BlacklistList fields to delete and remove them one by one (like exiftool)You can't delete what you don't know about—vendor-private udta, GoPro telemetry tracks, SEI in the bitstream; miss one and you've leaked it
WhitelistDon't say what to remove, say what to carry over: one video stream, one audio stream, nothing else—

With the whitelist approach, the source's metadata has no channel to enter. It's not "deleted"—it was never carried over.

But ffmpeg will betray this structure twice by default, and you must stop it:

  1. It copies the source container tags to the output by default → -map_metadata -1
  2. It auto-selects all streams by default, which will carry GoPro's gpmd telemetry track along → -map 0:v:0 -map 0:a:0?

Item 2 is the only defense for GoPro / DJI sources: their GPS isn't in a tag, it's in a separate timed metadata track. Clearing tags won't remove it; only "select exactly two streams" stops it.

The real challenge: invisible layers

With those two stopped, the tag layer is clean and ffprobe is silent. But three identity strings still live inside the file:

Hidden inContentVisible to ffprobe
H.264 SEIx264 full parameter string cabac=1 ref=3 … crf=23.0✗
AAC bitstream DSELavc62.28.102✗
avc1 compressornameLavc libx264✗

Five hiding places and their countermeasures are detailed at {baseDir}/references/residue-map.md—each one has empirical test records, including several traps that look like they should work but don't (-flags:v +bitexact and -x264-params info=0 both fail to block SEI).

Two modes

ModeVideoAudioSpeedExtra removals
copy (default)Byte-for-byte copyRe-encode53-second clip in 1.3s—
encodeFull re-encodeRe-encodeMuch slowerBitstream watermarks, fragile steganography

Audio is re-encoded in both modes—AAC version number is written in DSE inside the bitstream, -c:a copy can't remove it. Re-encoding once is cheap and buys a clean audio track. Video in copy mode truly doesn't move a single bit (verified with cmp, except for the stripped SEI).

About watermarks: encode removes more than copy—bitstream watermarks and fragile steganography—but these are rare in real footage. The truly dangerous robust pixel watermarks (SynthID, forensic watermarks) are designed to survive re-encoding, so neither mode can remove them. Choose mode by quality and speed, don't rely on re-encoding to wash out watermarks.

Four profiles

The layer written back after scrubbing:

ProfileWrites
obs (default)Looks like OBS Studio's output. OBS wraps with ffmpeg anyway, so this isn't forgery—it keeps the normal appearance of similar tools
quicktimemacOS QuickTime screen recording: Core Media Video / Core Media Audio
screencaptureScreenCaptureKit screen recording
bareWrites nothing. Highest privacy strength—leaves no claims that could be disproven

One thing that can't be done, mentioned here so you don't try: the container encoder field is owned by the muxer, -metadata encoder=Lavf60.16.100 cannot override it, and setting it to empty doesn't clear it (tested). Only -fflags +bitexact can make it disappear entirely. So the obs profile doesn't fake a version number—it lets ffmpeg write its real version.


Step 0 — Inspect the source first

node {baseDir}/scripts/video-scrub.mjs inspect <video>

Shows everything in one screen: container tags, per-stream tags and side data, chapters, and the identity strings inside the bitstream that ffprobe can't see. Tells you how many needles to scan for during verification, and whether SEI surgery will happen.

Always run inspect before scrubbing. Pay special attention to:

  • Whether there are data / timecode streams—if so, the source may carry telemetry (GoPro, DJI)
  • Whether it's HDR—if so, SEI is never removed (explained below)

Step 1 — Scrub

node {baseDir}/scripts/video-scrub.mjs scrub <video> -o out.mp4

Defaults to --mode copy --profile obs. Common options:

--mode encode          # Full re-encode
--profile bare         # Writes nothing, highest privacy
--crf 18               # Only meaningful in encode mode
--date 2026-01-01T00:00:00Z   # Specify creation time; defaults to now if omitted

Step 2 — Verify ⛔ do not skip

node {baseDir}/scripts/video-scrub.mjs verify <source> <output>

Or all-in-one (use this daily):

node {baseDir}/scripts/video-scrub.mjs run <video> -o out.mp4

The core of verification is this: all previous steps are claims—"I set the right parameters"; this step is proof—"I scanned, it's gone."

The approach: extract every metadata string from the source as a "needle"—tag keys and values, handler names, chapter titles—then perform a byte-level scan of the output file. If any needle hits, it's flagged red.

Two implementation details:

  • Needles must be ≥5 bytes. Shorter strings will randomly hit false positives inside compressed data; skipped needles are listed in the output.
  • Generic strings are not needles. VideoHandler is everywhere—zero information; Lavf58.45.100 pins down a specific version—it's a fingerprint. The dividing line: "Can this string narrow down to a specific person, device, or export session?"

The 12 gates

GateBlocks
Byte-level residueSource metadata string hits the output
Tag layer residueOutput tag value came from source
Location infoAny GPS / location field
Device & software identifiersmake / model / software / artist / comment, etc.
Source timestampOutput creation_time equals source's
Rotation matrixEncode mode: rotation not baked into video (copy mode skips—removing it would distort the video)
Stream compositiondata / timecode / attachment streams mixed in
ChaptersChapters carried over
Bitstream identity stringsSEI / DSE / compressorname still contain encoder names
Profile conformanceRemaining tags don't match declared profile
DurationOutput secretly truncated
DecodabilityProduced a broken file that can't be opened

HDR and embedded subtitles: when SEI surgery is forbidden

ffmpeg only offers NAL-level granularity—filter_units removes by NAL type, and h264_metadata has no "remove only this SEI payload" option. So remove_types=6 means all SEI removed at once. But SEI contains more than x264's junk:

  • HDR static metadata (mastering display / content light level)—removing it breaks color, tone mapping collapses
  • CEA-608/708 embedded subtitles—removing it deletes subtitles

So the script's policy: only performs surgery when identity strings are actually detected; never touches HDR sources (it clearly states why it didn't). This decision is made by seiPlan(), you don't need to worry about it, but you should know it's there.

Boundaries (what we don't do)

No watermark removal (robust pixel watermarks survive re-encoding, don't count on it), no video content manipulation (no crop, scale, or rotation), no fake device models or GPS (profiles only write tool-class info, not fabricated capture devices or locations), no batch directory scanning (one video at a time), no containers other than mp4/mov.

Self-test

node {baseDir}/scripts/selftest.mjs

117 assertions, no ffmpeg required, no real files required. Each of the 12 gates has a break-case test—proof that the gate actually blocks. Run this after modifying any script.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

eternityspring/reelbench-skills

拉片:把一条成片拆成逐镜头的分析表——每个镜头的时长、景别、类别、运镜、画面。 分工刻在骨子里:**能量的都由代码量**(切点来自 ffmpeg 场景检测,时长是切点相减, 运动量是逐帧差分的中位数),模型只判断它真正该判断的那几件事(景别 / 类别 / 运镜 / 画面 / 节奏), 然后每一条判断都被代码当场对账——**声称推拉摇移却实测几乎不动,门直接拦**。 看片走联系表(每镜起手帧 + 收尾帧各拼一张大图,一屏二十几个镜头,a/b 对照就是运镜), 不是一张张翻。检测漏刀多刀用 recut 补刀并刀,自动重编号重算时长,手改边界过不了门。 产出 shots.json + Markdown 镜头表 + **单页交互式拉片报告**:内嵌播放器(播放时同步高亮镜头、 点镜头跳转)、镜头节奏带、可搜索可筛选可排序的镜头表(列表 / 卡片两种视图、首尾关键帧并排、 点图开大图)、景别类别运镜分布、出场人物、质量门、导出 JSON。单文件零依赖,离线双击能开。 15 道质量门全部由脚本确定性检查。 零依赖、零 API key,只要 node 和 ffmpeg。 Use when asked to 拉片、拆镜头、分析视频镜头、镜头时长、景别、运镜、镜头表、 video shot breakdown、shot list from video。

eternityspring/reelbench-skills

把拉片数据和原片合成一条**能直接看的视频**:一边是画面,一边是这一镜的分镜信息 (镜号、起止、时长、景别、类别、运镜、画面描述、台词),**镜头切了信息跟着切**, 镜头表自动滚动并高亮当前这一镜。 版式只看原片的宽高比:**横版 / 方版 → 画面在上、信息在下;竖版 → 画面在左、信息在右**, 画面永远原样缩放,不裁不拉。 镜头表**随播放滚动**:切点处滚一小段把当前镜头带到锚点、随后停住,高亮条同步滑过去。 实现上只截三张图(底板 + 铺开的长图 ×2),滚动与高亮由 ffmpeg 按时间裁窗, **布局全在 scripts/panel.css 里,改它就能改版式**,不用碰脚本。 吃 video-shots 产出的 shots.json(有 frames/ 就把关键帧当缩略图用)。 零 npm 依赖,用 ffmpeg 合成、无头浏览器渲面板。 Use when asked to 导出视频、合成视频、分镜视频、带分镜信息的视频、解说版视频、 video with shot info、annotated shot video。

Related Skills