A dead simple Markdown preview command-line tool. Renders a .md file to GitHub-flavored HTML and serves a live preview in your browser β the page re-renders instantly whenever the file changes on disk (or in the built-in editor). The server keeps running until you stop it (Ctrl-C).
Features:
- GitHub-flavored Markdown β tables, strikethrough, task lists, footnotes
- TeX math β inline
$...$and display$$...$$, rendered with KaTeX - Syntax highlighting β class-based, with bundled GitHub Light/Dark themes
- Copy-code buttons on every code block
- Auto light/dark β page, code, and math follow your system
prefers-color-scheme - Live reload β saving the file (from any editor) re-renders the preview over a WebSocket, no manual refresh
- Built-in editor β switch between preview, split (editor + preview), and editor views from an in-page toolbar; edits save straight back to the file
- Cross-file links & local images β relative
.mdlinks open in the same live preview, and local images/assets render without a CDN - Dual-mode daemon β run in read-only mode (default) or enable the full editor with
--edit; one mode per daemon lifetime, chosen at startup
md-preview yourfile.md
# or via the short alias:
md yourfile.mdThis contacts the always-on daemon (spawning it if needed), registers the file's project root, and opens the browser at the preview URL.
md-preview <file.md> [--no-open]
md-preview --warm-cache
md-preview --daemon--no-openβ register the file and print the URL without opening a browser (alsoMD_PREVIEW_NO_OPEN=1).--warm-cacheβ pre-fetch and verify all pinned bundle assets into the local cache, then exit. Run once after install if you want offline-first operation from the first preview.--daemonβ start the daemon in server mode (no document); used by the systemd user unit. If a daemon is already running this exits cleanly.--editβ enable the collaborative editor routes (/editand/collab). Without--editthe daemon starts in read-only mode (the view-switcher preview pane works, but the editor is disabled). If a read-only daemon is already running and--editis requested, stop it first and re-run with--edit. Part of the mycelium ecosystem (ADR-0012 dual-mode single binary).BROWSERβ if set, used as the opener command instead of the system default. SettingBROWSER=true(orBROWSER=/bin/true) intentionally suppresses opening any browser β the daemon is still spawned and the preview is served, nothing is launched. Useful in CI/headless runs.
The normal flow is a single command β point md-preview at a file and it does the rest:
md-preview path/to/file.mdEach md-preview <file> is a thin client. If a daemon is already running it connects to it and opens the preview. If none is running, it auto-spawns a detached md-preview --daemon background process (new session via setsid, stdio redirected to ~/.cache/md-preview/daemon.log), waits for it to start listening, then opens the preview β so the command returns immediately and the daemon keeps running in the background (it never ties up your terminal). Later md-preview <file> invocations reuse that same daemon (no second process, no "Address already in use"). If two run at once, only one daemon wins the election; the other's spawned --daemon exits as a no-op and both clients connect. The daemon keeps running until you stop it (kill the background md-preview --daemon, or the systemd user unit β installed separately by the operator).
Warm the bundle cache (optional, recommended once after install):
md-preview --warm-cachePre-fetches and SHA-384-verifies the pinned Mermaid, KaTeX, KaTeX fonts, and github-markdown-css assets into ~/.cache/md-preview/ (respects $XDG_CACHE_HOME). After this, previews render fully offline. Without it the daemon fetches+verifies each asset lazily on first use.
Multiple project roots, one daemon:
md-preview ~/projectA/docs/intro.md # registers projectA's root, opens it
md-preview ~/projectB/README.md # same daemon now also serves projectBThe daemon holds a multi-root registry. Each md-preview <file> registers the file's project root (detected by walking ancestors for .git/Cargo.toml/etc., stopping before $HOME); any file under any registered root is then previewable. Roots persist across restarts with a 30-day sliding TTL.
Port model β loopback only, two ports:
- The daemon binds
127.0.0.1only (never a routable address). It is never reachable off-host. - Shell origin β the trusted SPA (
/view,/content,/ws,/claim, β¦): port7878by default, falling back to an OS-assigned ephemeral port if7878is busy. - Secondary static origin β bundle assets + short-TTL
/cap/<token>capability URLs for document images, served cross-origin to the sandboxed render iframe: the shell port + 1 (7879by default), with the same ephemeral fallback.
A strict Host allowlist (127.0.0.1:<port> / localhost:<port> only) guards both origins against DNS-rebinding; the CLIβdaemon control channel is a 0600 Unix socket under $XDG_RUNTIME_DIR/md-preview/ (peer-uid checked), so no token ever touches argv or a URL.
Headless / CI: add --no-open (or set MD_PREVIEW_NO_OPEN=1, or BROWSER=/bin/true) to register the file and print the preview URL without launching a browser.
- Rust toolchain β install via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- C linker (required by Rust to link binaries):
# Debian / Ubuntu / WSL sudo apt-get install -y gcc build-essential
git clone https://github.com/martian17/md-preview.git
cd md-preview
cargo install --path .This places md-preview in ~/.cargo/bin/. Add it to your PATH if it isn't already:
echo '. "$HOME/.cargo/env"' >> ~/.bashrc
source ~/.bashrcCreate an md symlink so you can type md file.md instead:
ln -sf ~/.cargo/bin/md-preview ~/.cargo/bin/mdWorks out of the box on WSL2 β no extra configuration needed.
The webbrowser crate detects WSL by reading /proc/version and launches your default Windows browser via explorer.exe. WSL2's built-in localhost proxy forwards http://127.0.0.1:<port> from Windows into the WSL network, so the browser connects directly to the Rust server running inside WSL.
Tested on: Ubuntu 24.04 (aarch64) on WSL2 running on Snapdragon X Windows.
If the browser fails to open for any reason, the URL is printed to stdout so you can open it manually.
md <file>is a thin client: it talks to the always-on daemon over a Unix socket ($XDG_RUNTIME_DIR/md-preview/), registers the file's project root, and opens the browser at the preview URL. If no daemon is running it spawns one detached first. To authenticate the browser without ever putting the secret in argv or a URL, the client writes a0600bootstrap HTML (an auto-submittingPOSTform) into the0700runtime dir and opens thatfile://. The form carries a single-use nonce in its body toPOST /claim, which sets themdp_sessioncookie and 302-redirects to the real/view(Post/Redirect/Get β a clean, reload-safe authenticated landing). A form POST is a navigation, not afetch, so it works from afile://page where a cross-originfetchwould be CORS-blocked.- The daemon (
warp+tokio) runs as a systemd user service (Restart=always) β one long-lived process per login, not one per file. It holds a multi-root registry: onmd <file>, the project root (detected by walking ancestors for.git/Cargo.toml/etc., stopping before$HOME) is registered; all files under it can then be previewed. Roots persist across reboots with a 30-day sliding TTL. - All filesystem access goes through a single confinement funnel (
confine.rs): canonicalize β check against the union of registered roots β deny sensitive paths (~/.ssh,$HOME,/etc, β¦) β open withO_NOFOLLOWand hold the fd for the read. No TOCTOU re-resolution. - The preview page is a trusted SPA shell (intact localhost origin, session-cookie-gated) that loads the rendered content into a sandboxed null-origin
<iframe>(sandbox="allow-scripts",connect-src 'none'). Document images/assets are served from a second loopback port as short-TTL capability URLs, cross-origin to the iframe (canvas-tainted, unexfiltratable). - Converts Markdown to HTML using
pulldown-cmarkwith GFM extensions (tables, strikethrough, task lists, footnotes) plus math ($...$,$$...$$). - Highlights code blocks server-side with
syntect, emitting class-based markup styled by bundled GitHub Light/Dark themes. - Watches the file with
notify; on every change β an external editor or the built-in editor's save β it re-renders and pushes the new HTML to the page over a WebSocket, so the preview updates live without a reload.
Built with the default
daemonfeature, the daemon fetchesgithub-markdown-css, KaTeX, and Mermaid from pinned CDN URLs on first use and caches them locally (verifying SHA-384); subsequent starts and offline use serve from that cache (~/.cache/md-preview/). Built without the daemon feature (--no-default-features), the standalone HTML output references the CDN URLs directly and requires an internet connection to render styles and math. Syntax-highlight themes are always bundled into the binary and work offline.
Built without the default daemon feature (cargo build --no-default-features), the binary drops every web dependency and instead renders the file to stdout once β a zero-dependency fallback that keeps the renderer reusable as a library.
Always built in:
| Crate | Purpose |
|---|---|
pulldown-cmark |
Fast GFM-compatible Markdown parser (with math extension) |
syntect |
Syntax highlighting (class-based HTML + theme CSS) |
yrs |
CRDT document model (Yjs-compatible) backing the live document |
similar |
Text diffing β turns an external save into minimal edits |
notify |
Filesystem watching for live reload |
Daemon only (the default daemon feature; absent under --no-default-features):
| Crate | Purpose |
|---|---|
tokio |
Async runtime |
warp |
HTTP + WebSocket server |
futures-util |
Stream helpers for WebSocket forwarding |
serde / serde_json |
Query-string deserialization + control-plane protocol |
webbrowser |
Cross-platform browser launcher |
getrandom |
Entropy for nonce/session/capability tokens |
libc |
SO_PEERCRED, O_NOFOLLOW, fstat (confinement funnel) |
ureq |
Bundle cache HTTP fetcher |
sha2 |
SHA-384 SRI verification for the bundle cache |
subtle |
Constant-time comparison for secrets |
base64 |
URL-safe + standard encoding for tokens |
Licensed under the Apache License, Version 2.0. Copyright 2026 Yutaro Yoshii.