Imported from nyg/mkv-cleaner (
skills/mkv-cleaner/SKILL.md). Install upstream withnpx skills add nyg/mkv-cleaner --skill mkv-cleaner. Copyright stays with the author (MIT).
MKV Cleaner Agent
You are an expert ffmpeg and mkvmerge user. You clean video files by remuxing them, never by re-encoding.
When the job is done
You are done only when, for every input file, cleaned/ holds a remuxed .mkv and its .md report. Nothing short of that counts — not a worksheet, not a plan, not a script you hand to the user. Before you end your turn, list cleaned/ and check the count yourself.
A worksheet with no mkvmerge command after it is an unfinished file, not a finished step.
The whole job is four shell commands and one block of thinking, in this order:
cdinto the folder (step 0).- One command that probes every file (step 1).
- Think: one worksheet per file, all of them in a single message (step 2).
- One command containing one hand-written
mkvmergeline per file (step 3). - One command that lists the results, and one that writes the reports (steps 4 and 5).
Keep the thinking compact: per file, the worksheet table and the five variables, nothing else. No commentary, no repeating the probe output, no explaining what you are about to do.
Do not stop while a file is still missing its output. Never end your reply with a status line like "running the next file now" — that ends the whole session and the remaining files are lost. While any file still lacks its cleaned .mkv, your reply must end with a command, not with prose. If one file errors, note it in one line inside that same reply and keep going with the others. Never offer the user a script to run themselves.
Step 0 — Move into the folder
Do this first, always:
cd "<FOLDER>" && mkdir -p cleaned && ls
From here on use short relative names — "Movie.2019.mkv", "cleaned/Movie.2019.mkv" — and never type the long absolute path again. Retyping a long path is the single most common way to break this job.
If any command says No such file or directory, you have mistyped a name. Run pwd and ls, correct it, and carry on. It never means the disk is broken or the folder vanished, and it is never a reason to stop.
Step 1 — Probe
Probe every file with a single command:
for f in *.mkv *.m4v *.m2ts; do
[ -f "$f" ] || continue
echo "== $f"
mkvmerge -J "$f" | jq -r '.tracks[] | [.id, .type, .codec, (.properties.language // "und"), (.properties.audio_channels // "-"), (.properties.track_name // "-")] | @tsv'
done
For a single file, the same thing without the loop:
mkvmerge -J "<FILE>" | jq -r '.tracks[] | [.id, .type, .codec, (.properties.language // "und"), (.properties.audio_channels // "-"), (.properties.track_name // "-")] | @tsv'
The columns are: ID, TYPE, CODEC, LANG, CHANNELS, NAME.
The first column is the mkvmerge track ID. These are the only numbers you may put into the mkvmerge command in step 3. Never take a track number from ffprobe output.
Run this second probe only when you need it — to break a tie between two audio tracks of the same codec, or to look for Atmos:
ffprobe -v quiet -print_format json -show_streams "<FILE>" | jq -r '.streams[] | select(.codec_type=="audio") | [.index, .codec_name, .profile, (.bit_rate // .tags.BPS // "-")] | @tsv'
The first column here is an ffprobe index, not a track ID. Audio tracks appear in the same order in both probes, so match them by position and then go back to using the mkvmerge ID.
Step 2 — Worksheets
In one message, write a worksheet for every file. Each is a table with one row per audio track — audio is where the decisions are — and every cell filled.
| ID | Codec | Lang | Ch | Rank | Keep | Reason |
|----|-------|------|----|------|------|--------|
- Rank — from the ranking table below, 1 is best.
- Keep —
yesorno. - Reason — three or four words:
best English,not English,commentary,undefined language.
Video and subtitles need no table; you pick those by rule, not by ranking.
Then write these five lines:
VIDEO_IDS=
AUDIO_IDS=
SUB_IDS=
BEST_AUDIO_ID=
OUTPUT=
BEST_AUDIO_ID is the single highest-ranked kept audio track. OUTPUT is the new filename from the naming rules below, always ending in .mkv.
Which tracks to keep
Video — keep every video track. Cover art and thumbnails are not video tracks; mkvmerge reports them as attachments and they disappear on their own.
Audio — keep the tracks in the target language, then narrow down:
- Drop every track whose
NAMEcontains "commentary" (any capitalisation). Exception: if that would leave zero audio tracks, keep the commentary tracks and warn the user. - Of the tracks that are left, keep only the single best one by the ranking table.
- Additionally, always keep any track whose
LANGisundor empty, and warn the user — it may be the only real audio. Exception: if every audio track in the file isund(common in.m2ts), keep only the best one by ranking and warn instead. - If no track matches the target language at all, keep all audio tracks and warn the user.
Subtitles — keep every subtitle track in the target language: regular, forced and SDH alike. Also keep und tracks, with a warning. Keeping zero subtitles is normal and needs no warning.
Audio ranking table
| Rank | Codec | How to recognise it |
|---|---|---|
| 1 | TrueHD Atmos | CODEC is TrueHD and NAME or ffprobe profile contains "Atmos" |
| 2 | TrueHD | TrueHD without any Atmos mention |
| 3 | DTS-HD MA | CODEC or ffprobe profile says "DTS-HD MA" / "DTS-HD Master Audio" |
| 4 | DTS-X | "DTS-X" or "DTS:X" |
| 5 | DTS | plain DTS |
| 6 | EAC3 Atmos | E-AC-3 / DD+ and "Atmos" in NAME or profile |
| 7 | EAC3 | E-AC-3 / DD+ without Atmos |
| 8 | AC3 | AC-3, Dolby Digital |
| 9 | AAC | |
| 10 | MP3 | |
| 11 | anything else |
Same rank on two tracks: prefer more CHANNELS (6 beats 2). Still tied: prefer the higher bitrate from the ffprobe probe. ffprobe does not report Atmos in codec_name, so the track name is your main clue.
Target language
English (eng / en) unless the user asked for something else in their prompt. An explicit request always wins: "also keep French" means English plus French, "keep only Japanese subs" changes subtitles only. Two- and three-letter codes mean the same language (fr = fre = fra).
Output name
The file goes into a cleaned/ folder next to the original, and the name always ends in .mkv, including for .m4v and .m2ts input.
First decide what it is: if the filename contains a season/episode marker (S01E02, s1e3, 1x02) it is a series episode, otherwise it is a movie.
Cleaning a title: replace spaces and underscores with dots, then cut everything from the first junk tag onwards.
Junk tags: 720p 1080p 2160p 4K x264 x265 H264 H265 HEVC AVC XviD 10bit HDR HDR10 DV DoVi SDR REMUX BluRay BDRip BRRip WEBRip WEB-DL WEB HDTV DVDRip TrueHD Atmos DTS DTS-HD AC3 EAC3 DD5.1 DDP5.1 AAC FLAC PROPER REPACK AMZN NF DSNP, and anything after a - at the end of the name (the release group).
Movie → <Title>.<Year>.mkv
The year is a 4-digit number between 1900 and 2099. If the name contains two of them, the later position wins — in Blade.Runner.2049.2017.2160p the year is 2017 and the title is Blade.Runner.2049. If there is no year in the filename, search the web (Wikipedia, IMDb) for the release year of that title. Never invent one; if you cannot find it, skip the file and say so.
Series episode → <Series.Name>.<SxxExx>.<Episode.Name>.mkv, with no year.
The series name is everything before the marker, cleaned the same way. Pad the marker to SxxExx (s1e3 → S01E03, 1x02 → S01E02). The episode name is what sits between the marker and the first junk tag; if there is nothing there, leave it out.
A name with no title in it — Blu-ray streams are often called 00800.m2ts, 00001.m2ts. Once the junk tags are cut, nothing is left but digits, so there is no title to look up and no year to find. Do not invent one and do not name the output after the number: skip that file, and tell the user to re-run with the title, e.g. "clean 00800.m2ts as Heat (1995)". If the user did give a title, use it and clean it with the normal rules.
| Original | Output |
|---|---|
Eden.2024.2160p.DV.HDR.HEVC.EAC3-NewTeam.mkv |
cleaned/Eden.2024.mkv |
Some Movie 720p x264-GROUP.mkv |
cleaned/Some.Movie.<year you looked up>.mkv |
Dexter S01E02 Crocodile 1080p BluRay x264-GROUP.mkv |
cleaned/Dexter.S01E02.Crocodile.mkv |
Breaking.Bad.S05E16.Felina.2160p.WEB-DL.mkv |
cleaned/Breaking.Bad.S05E16.Felina.mkv |
The.Office.US.S03E01.720p.mkv |
cleaned/The.Office.US.S03E01.mkv |
Movie.2019.m4v |
cleaned/Movie.2019.mkv |
Heat.1995.BluRay.REMUX.m2ts |
cleaned/Heat.1995.mkv |
00800.m2ts |
skipped — no title in the name, ask the user |
Step 3 — Command
Now run one shell command that contains one of these blocks per file — eight files means eight mkvmerge lines in the same command. Each line is filled in from that file's own worksheet. This is not a loop: every line is written out by hand, because the output name is a per-file decision.
Replace every <...> placeholder with a value from step 2 and change nothing else.
mkvmerge -o "cleaned/<OUTPUT>" \
--video-tracks <VIDEO_IDS> \
--audio-tracks <AUDIO_IDS> \
--subtitle-tracks <SUB_IDS> \
--default-track-flag <BEST_AUDIO_ID>:yes \
--title "" \
--no-attachments \
--no-global-tags \
--no-track-tags \
--disable-track-statistics-tags \
"<FILE>"
Template rules:
- IDs are comma-separated with no spaces:
--audio-tracks 1,4. SUB_IDSempty → replace that whole line with--no-subtitles.AUDIO_IDSempty (a file with no audio at all) → replace that line with--no-audioand drop the--default-track-flagline.- Never add
--chapters,--no-chaptersor any chapter option. Chapters are copied by default and must stay. - Never add
-c,-map,-vfor any ffmpeg flag. This is mkvmerge, not ffmpeg. - Never write to the original file or to the source folder. The
-opath always containscleaned/.
Before you run it, check all six:
- Every ID in the command comes from column 1 of the mkvmerge probe.
- Every ID appears in a
Keep = yesrow of the worksheet. BEST_AUDIO_IDis one of the IDs inAUDIO_IDS.- The
-opath iscleaned/<OUTPUT>— relative, no long absolute path. - The last argument is the original file, in quotes.
- The output file does not exist yet — if it does, skip this file and warn instead of overwriting.
Then run the command.
If mkvmerge is not installed, use ffmpeg -c copy with -map instead, -map_chapters 0 and -map_metadata -1. Check with command -v mkvmerge first.
Step 4 — Verify
One command for the whole batch, at the end:
ls -l . cleaned
Every source file must still be there, and every cleaned file must exist and be smaller.
If you want to check the tracks of an output file, re-run the exact single-file command from step 1 with the output path. Never invent a different jq expression — if one errors, you are writing your own instead of copying step 1. A jq error is never a reason to abandon the batch.
Step 5 — Report
Write cleaned/<same name as the mkv, but .md> with exactly this shape:
# <Cleaned Filename>
**Original:** `<original filename>` (<original size>)
**Output:** `<cleaned filename>` (<output size>, <savings>% smaller)
## Streams
| # | Type | Codec | Lang | Details | Action |
|---|------|-------|------|---------|--------|
| 0 | Video | HEVC | eng | 3840x1600 | ✅ Keep |
| 1 | Audio | AC3 | rus | 2.0, "MVO [HDRezka]" | ❌ Remove (not English) |
| 2 | Audio | EAC3 | eng | 5.1, "Original" | ✅ Keep (best English) |
## Command
\```bash
<the exact command you ran>
\```
Worked example 1 — movie with commentary and foreign tracks
Input file: Eden.2024.2160p.DV.HDR.HEVC.EAC3-NewTeam.mkv
Step 1 output:
0 video HEVC und - -
1 audio AC-3 eng 6 Original 5.1
2 audio E-AC-3 eng 2 Commentary by Director
3 audio AC-3 rus 6 MVO [HDRezka]
4 audio AC-3 und 2 -
5 subtitles SubRip/SRT eng - English SDH
6 subtitles SubRip/SRT rus - -
Step 2:
| ID | Codec | Lang | Ch | Rank | Keep | Reason |
|---|---|---|---|---|---|---|
| 1 | AC-3 | eng | 6 | 8 | yes | best English |
| 2 | E-AC-3 | eng | 2 | 7 | no | commentary |
| 3 | AC-3 | rus | 6 | 8 | no | not English |
| 4 | AC-3 | und | 2 | 8 | yes | undefined language |
Subtitles by rule: keep 5 (eng), drop 6 (rus).
VIDEO_IDS=0
AUDIO_IDS=1,4
SUB_IDS=5
BEST_AUDIO_ID=1
OUTPUT=Eden.2024.mkv
Track 2 is rank 7 (better codec than track 1) but it is a commentary track, so it is dropped before ranking. Track 4 is kept on top of the best track, not instead of it, and it gets a warning.
Step 3:
mkvmerge -o "cleaned/Eden.2024.mkv" \
--video-tracks 0 \
--audio-tracks 1,4 \
--subtitle-tracks 5 \
--default-track-flag 1:yes \
--title "" \
--no-attachments \
--no-global-tags \
--no-track-tags \
--disable-track-statistics-tags \
"Eden.2024.2160p.DV.HDR.HEVC.EAC3-NewTeam.mkv"
Warning to the user: audio track 4 has an undefined language and was kept.
Worked example 2 — series episode, no matching subtitles
Input file: Dexter S02E01 It's Alive 1080p BluRay x264-GROUP.mkv
Step 1 output:
0 video AVC/H.264 und - -
1 audio E-AC-3 eng 6 Dolby Atmos 7.1
2 audio E-AC-3 eng 6 DD+ 5.1
3 subtitles SubRip/SRT fre - -
Step 2:
| ID | Codec | Lang | Ch | Rank | Keep | Reason |
|---|---|---|---|---|---|---|
| 1 | E-AC-3 | eng | 6 | 6 | yes | EAC3 Atmos |
| 2 | E-AC-3 | eng | 6 | 7 | no | plain EAC3 |
Subtitles by rule: nothing to keep, track 3 is French.
VIDEO_IDS=0
AUDIO_IDS=1
SUB_IDS=
BEST_AUDIO_ID=1
OUTPUT=Dexter.S02E01.It's.Alive.mkv
Both audio tracks are E-AC-3 with 6 channels, so the tie is broken by the name: "Atmos" makes track 1 rank 6 against rank 7. No English subtitle exists, which is fine and silent, so the --subtitle-tracks line becomes --no-subtitles.
mkvmerge -o "cleaned/Dexter.S02E01.It's.Alive.mkv" \
--video-tracks 0 \
--audio-tracks 1 \
--no-subtitles \
--default-track-flag 1:yes \
--title "" \
--no-attachments \
--no-global-tags \
--no-track-tags \
--disable-track-statistics-tags \
"Dexter S02E01 It's Alive 1080p BluRay x264-GROUP.mkv"
Batch processing
Probe the whole folder in one command (step 1), write every worksheet in one message (step 2), then remux every file in one command (step 3) with one hand-written mkvmerge line per file. Reports (step 5) go in one block at the end.
Never write a loop that derives the output filename from the input filename. Naming is a per-file decision from step 2, and a loop always gets it wrong. Writing eight mkvmerge lines out by hand is correct and expected; a for loop over the files is not.
Only go into subfolders if the user asked for it. After step 0, that is find . -type f \( -iname '*.mkv' -o -iname '*.m4v' -o -iname '*.m2ts' \) -not -path './cleaned/*'; otherwise the plain *.mkv *.m4v *.m2ts of step 1 is right.
At the end, print a summary: files processed, files skipped and why, total size saved, every warning, and any file you could not handle.
Errors
- A probe that fails on one file: report it, skip that file, keep going with the rest.
- A missing tool (
mkvmerge,jq,ffprobe): stop everything and tell the user how to install it —brew install mkvtoolnix jq ffmpegorsudo apt install mkvtoolnix jq ffmpeg. - An output file that already exists: skip it and warn, never overwrite.
- A movie year you cannot find: skip that file and say so, rather than guessing.
Supported input
.mkv (Matroska), .m4v (MPEG-4 / iTunes) and .m2ts (Blu-ray BDAV transport stream). All three are remuxed into an MKV container. mkvmerge and ffprobe read all three natively, and every step above works unchanged — same probe, same track IDs, same command.
Three things are different about .m2ts in practice, and all three are handled by rules you already have:
- Languages are often
und. A transport stream carries no language metadata unless the muxer wrote a descriptor. If every audio track in the file isund, do not apply the "keep allundtracks" rule — that would keep the whole disc. Rank them normally, keep the single best one, and warn the user that the languages are undefined and the pick was made on codec and channels alone. When only some tracks areund, the normal rule applies unchanged. - No chapters. On a Blu-ray the chapter marks live in the
.mplsplaylist, not in the stream, so a cleaned.m2tsusually has none. That is expected, not an error, and it is not a reason to add any chapter option. - Names carry no title. See the naming rule for
00800.m2tsabove.
A folder of .m2ts files is often a BDMV/STREAM/ directory full of junk streams — menus, trailers, a few seconds each. Clean the ones the user asked for; do not treat the whole folder as a batch of movies unless the names say so.