A cross-platform command-line interface for creating, extracting, validating, and inspecting .mdz files.
The .mdz format is a portable, self-contained document format that packages one or more Markdown content files together with their associated assets into a single ZIP archive. See the MDZ specification for full details.
Although the CLI is built with C#, binaries are distributed as prebuilt release assets. .NET is not required for normal CLI use.
Windows (WinGet):
winget install MDZip.CliWindows (PowerShell, will put mdz.cmd in %LOCALAPPDATA%\Microsoft\WindowsApps):
irm "https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.ps1?nocache=$(Get-Date -UFormat %s)" | iexIf you need to pin a specific release version during install:
$env:MDZ_VERSION='vX.Y.Z'
irm "https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.ps1?nocache=$(Get-Date -UFormat %s)" | iexLinux/macOS system-wide install (recommended if you can use sudo):
curl -fsSL https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.sh | sudo shLinux/macOS (no sudo required, but you might need to fix PATH):
curl -fsSL https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.sh | shIf mdz is not found after non-sudo install, add ~/.local/bin to your PATH:
# current shell
export PATH="$HOME/.local/bin:$PATH"
# persist for bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
# persist for zsh
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrcAfter install, run:
mdz --helpmdz <command> [options]
mdz create <source> <output> [options]
mdz add <archive> <entry-path> <file>
mdz remove <archive> <entry-path>
mdz extract <archive> [options]
mdz validate <archive>
mdz ls <archive> [options]
mdz inspect <archive>
Note: <archive> is the .mdz file path; .mdz extension is optional.
| Option | Description |
|---|---|
-v, --version |
Show version information |
-?, -h, --help |
Show help and usage information |
| Command | Description |
|---|---|
create |
Create an .mdz archive from a source directory |
add |
Add or replace a file entry in an existing .mdz archive |
remove |
Remove a file entry from an existing .mdz archive |
extract |
Extract the contents of an .mdz archive |
validate |
Validate an .mdz archive against the specification |
ls |
List the contents of an .mdz archive |
inspect |
Inspect metadata and manifest information of an .mdz archive |
textconv |
Print an archive as plain text, so git diff shows what changed inside it |
Creates an .mdz archive from all files in a source directory.
If no unambiguous entry point is found and you are in an interactive terminal, mdz prompts to generate a default index.md.
mdz create ./my-doc-folder my-doc.mdz --title "My Document" --author "Jane Smith" --entry-point index.md
mdz create --source ./my-doc-folder --output my-doc.mdz --force
mdz create ./my-doc-folder my-doc.mdz --create-index
mdz create ./my-doc-folder my-doc.mdz --map-files --entry-point index.md
mdz create ./my-doc-folder my-doc.mdz --mode project --entry-point index.md
mdz create ./my-doc-folder my-doc.mdz --filter "**/*.md" --filter "**/*.png" --filter "**/*.jpg" --filter "**/*.jpeg" --filter "**/*.webp" --filter "**/*.svg"| Option | Description |
|---|---|
--source, -s |
Required source directory (can also be provided positionally as <source>) |
--output, -o |
Required output archive path (can also be provided positionally as <output>). If no extension is supplied, .mdz is added automatically |
--filter, -fi |
Glob filter (archive-relative, repeatable). If omitted, defaults to markdown and common image files |
--force, -f |
Overwrite output file if it already exists |
--create-index, -ci |
Auto-generate index.md when no unambiguous entry point can be resolved |
--map-files, -mf |
* Write/update manifest.json files[] mapping (path, originalPath, title). Invalid source paths are sanitized when needed |
--title, -t |
* Sets manifest.title |
--entry-point, -e |
* Sets manifest.entryPoint. Must name a .md or .markdown file in the archive, with exact case |
--language, -l |
* Sets manifest.language (BCP 47, for example en, fr-CA). Defaults to en when another * option is passed |
--author, -a |
* Sets manifest.author.name |
--description, -d |
* Sets manifest.description |
--mode |
* Sets manifest.mode to document or project |
--doc-version |
* Sets manifest.version (for example 1.0.0) |
Notes:
- Every archive
mdz createwrites has amanifest.jsonwithspec.version,title,entryPointandmode(spec 1.2.0 §14.1). Without*options, the title is the source folder name and the entry point is discovered. *= Manifest metadata option. If none is passed and--filterpackages amanifest.jsonfrom the source, that manifest is used as-is (unknown fields kept) and only missing required fields are added. Otherwise the sourcemanifest.jsonis replaced by the generated one.- Entry point discovery follows spec §5.5:
index.md, else the one root Markdown file. RootREADME,AGENTS,LICENSE,CHANGELOGandCONTRIBUTINGfiles are set aside when another root Markdown file exists. manifest.modedefaults todocumentwhen omitted; use--mode projectfor multi-document project archives.manifest.produceris optional in the spec; this CLI writes it by default.- If manifest-writing options are used without
--title, title defaults to the source folder name. - If invalid archive path characters are found and
--map-filesis not passed, interactive mode prompts to enable file mapping.
Example manifest generated by mdz create:
{
"spec": {
"name": "mdzip-spec",
"version": "1.2.0"
},
"title": "My Document",
"entryPoint": "index.md",
"mode": "document",
"language": "en",
"producer": {
"application": {
"name": "mdz-cli",
"version": "<current mdz-cli version>",
"url": "https://github.com/mdzip-project/mdzip-cli"
},
"core": {
"name": "MDZip.Core",
"version": "<current MDZip.Core version>",
"url": "https://github.com/mdzip-project/mdzip-core"
}
},
"author": {
"name": "Jane Smith"
}
}Adds a local file into an existing archive at the given archive-relative path. If the entry path already exists, it is replaced.
add, remove and manifest set keep unknown manifest fields and every untouched entry byte-for-byte, and add a manifest.json to an archive that has none. A change that would make the archive open at a different entry point (for example removing index.md when other root Markdown files remain) is refused with ERR_ENTRYPOINT_CHANGED; set --entry-point with mdz manifest set first.
mdz add my-doc.mdz assets/images/cover.png ./cover.png
mdz add my-doc.mdz chapter-02.md ./chapter-02.mdRemoves a file entry from an existing archive.
mdz remove my-doc.mdz assets/images/old-cover.pngExtracts an .mdz archive to a destination directory.
mdz extract my-doc.mdz --output ./extracted| Option | Description |
|---|---|
--allow-invalid |
Extract even if archive validation fails |
--output, -o |
Destination directory. Defaults to a directory named after the archive in the current folder |
Validates an .mdz archive against the specification. Exits with code 0 if valid, 1 if invalid.
Errors carry the spec's error codes, for example ERR_ENTRYPOINT_INVALID (a malformed entryPoint), ERR_ENTRYPOINT_MISSING (no such entry; a case-only match is suggested), ERR_DUPLICATE_PATH (entries equal after NFC normalization) and ERR_ZIP_INVALID (compression other than stored or DEFLATE). Symbolic link entries are reported as warnings and are never followed or extracted as links. A missing manifest.json is a note, not a warning: the archive still conforms.
mdz validate my-doc.mdzLists the contents of an .mdz archive.
mdz ls my-doc.mdz
mdz ls my-doc.mdz --long| Option | Description |
|---|---|
--long, -l |
Show detailed information (size, compressed size, last modified) |
Displays metadata and manifest information from a .mdz archive.
mdz inspect my-doc.mdzinspect surfaces manifest.spec.version (falling back to legacy mdz if present), manifest.mode, and other key manifest metadata.
Prints an archive as plain text for Git. Git treats an .mdz as binary, so git diff and git log -p normally show only "Binary files differ". With textconv set up, they show the lines that changed in each document inside the archive.
Set it up once per repository, and commit the .gitattributes line:
echo "*.mdz diff=mdz" >> .gitattributesThen once in each clone (Git doesn't let a repository choose commands to run, so every clone opts in):
git config diff.mdz.textconv "mdz textconv"
git config diff.mdz.cachetextconv trueClones without the git config lines still work: they show "Binary files differ" as before.
Each file in the archive becomes a section headed ===== path =====: manifest.json first, then the rest in path order. Text files (Markdown, JSON, SVG, CSS and similar) are printed in full; binary files such as images are one line with their size and SHA-256, so a replaced image shows up as a changed line:
===== index.md =====
# Story
-- Users can reset their password
+- Users can reset their password by email
+- The link expires after 1 hour
===== images/login.png =====
-[binary, 48211 bytes, sha256 9f2c…]
+[binary, 51007 bytes, sha256 1ab4…]It helps wherever Git computes the diff on your machine: git diff, git log -p, git show, and editors' Git views. The diff views on GitHub and Azure DevOps don't run it, and it doesn't merge: two branches changing the same .mdz still conflict (#14 proposes a merge driver). A file that isn't a readable archive, such as a Git LFS pointer, is printed as it is, so the diff never fails. The output matches mdzip.org's scripts/mdz-textconv.mjs.
A .mdz file follows the MDZ specification:
document.mdz
|- index.md # Recommended entry point
|- manifest.json # Metadata (mdz always writes one)
|- chapter-01.md
`- assets/
`- images/
`- cover.png
Paths inside an archive are case-sensitive and compared after Unicode NFC normalization (spec §5.4.1): mdz cat doc.mdz Index.md won't open index.md, but it will suggest it.
Implementation note: this project is written in C# and targets .NET 10.
A standalone browser page is available at mdzip.org/packager.html. It accepts a dropped or selected folder, builds an .mdz, previews the resolved entry-point markdown, and provides a download link for the generated archive.
Building from source requires the .NET 10 SDK (Windows, macOS and Linux). The release downloads, install scripts and WinGet package don't need .NET installed.
# Build
dotnet build
# Test
dotnet test
# Run directly
dotnet run --project src/mdz.Cli/mdz.Cli.csproj -- --help
# Publish a single self-contained executable (example: Windows x64)
dotnet restore .\mdz-cli.slnx --configfile .\NuGet.Config
dotnet publish src/mdz.Cli/mdz.Cli.csproj -c Release -r win-x64 --self-contained true --no-restore -p:DebugType=None -p:DebugSymbols=false -o out/win-x64
# Or use the helper script (restores + verifies the mdz.exe/mdz output)
powershell -ExecutionPolicy Bypass -File .\scripts\publish-single.ps1 -Rid win-x64 -ConfigFile .\NuGet.ConfigWinGet release automation is documented in docs/winget.md.