An open-source, AI-native Markdown app for macOS, Windows, and Linux. Double-click any .md and
it's instantly beautiful: read it, edit it, export it, and understand it with
free, private, on-device AI built in.
Think Preview.app, but Markdown-aware, editable, and AI-native, and cross-platform.
Website · Download · Contributing · Roadmap · Vision
- Download v0.4.0 for your OS from Releases:
.dmg(Apple Silicon),.msi/-setup.exe(Windows),.deb/.rpm/.AppImage(Linux). - Double-click any
.mdfile, or runmdopen notes.mdafter Preferences → Command-line tool → Install mdopen (the app's installers leavemdopenoff yourPATH; see docs/AGENTS.md). - Ask the AI sidebar about it. On a Mac with Apple Silicon and macOS 26 or later, the built-in on-device model needs no account and no API key: turn on Apple Intelligence in System Settings, then restart Ashlr MD. On Windows and Linux, install Ollama for free local AI, or add an Anthropic API key in Preferences.
Easiest: Preferences → AI agents (MCP) → Connect to Claude Code / Cursor. By hand, with the bundled mdopener-mcp:
# Claude Code (macOS path shown; Linux: "/usr/lib/Ashlr MD/mdopener-mcp")
claude mcp add --scope user ashlr-md "/Applications/Ashlr MD.app/Contents/Resources/mdopener-mcp"
# Codex
codex mcp add ashlr-md -- "/Applications/Ashlr MD.app/Contents/Resources/mdopener-mcp"Cursor (~/.cursor/mcp.json):
{ "mcpServers": { "ashlr-md": { "command": "/Applications/Ashlr MD.app/Contents/Resources/mdopener-mcp" } } }Windows path: %LOCALAPPDATA%\Ashlr MD\mdopener-mcp.exe. More in docs/AGENTS.md.
The Settings copy command targets a Unix shell on macOS/Linux and PowerShell on
Windows; its quoting is not intended for Windows Command Prompt (cmd.exe).
Your AI agents write Markdown all day: README.md, PLAN.md, research dumps.
On most systems those open as a wall of raw syntax (TextEdit, Notepad) or a blank
page (Preview). No app owns the simple job of making them look right instantly,
especially for non-technical people. Ashlr MD does exactly that, on every platform.
- Instant, beautiful rendering: GFM, Shiki-highlighted code, Mermaid diagrams, KaTeX math, GitHub-style callouts, tables, footnotes.
- Edit without the syntax: Typora-style WYSIWYG (Milkdown) + a lossless source mode (CodeMirror) + a clean Read view. Atomic save; external-change detection with a conflict banner.
- Export anywhere: one-click PDF / DOCX / HTML, fully offline, no Pandoc.
- ⌘K command palette: fuzzy, keyboard-first access to every action and your recent files.
- Free, private, on-device AI: summarize / explain / rewrite / translate. On macOS 26+ (Apple Silicon): zero-install via Apple Foundation Models (macOS only). On Windows & Linux (and as a fallback on Mac): local Ollama → your own cloud key → optional hosted. Nothing leaves your device unless you opt in.
- AI superpowers: select text to Explain / Summarize / Rewrite / Translate; inline AI rewrite in the editor (⌘I) with Rewrite / Fix grammar / Make concise / Expand; "Explain changes" when a file changes on disk; and chat-with-document.
- Agent activity drawer (⌘B): watch the folder your AI agent writes to and
new Markdown files appear live; click to open instantly. The answer to "buried
in agent
.mdfiles." - Outline navigation (⌘⇧O): an auto table-of-contents with scrollspy for long docs.
- Tabs: open several documents at once; the tab bar appears only when more
than one is open, so the single-file view stays clean.
⌘⇧]/⌘⇧[to switch,⌘Wto close. - Find & search: find-in-document (
⌘F) in both rendered and source views, find-and-replace in source (⌘⌥F), and full-text search across your recent & watched files (⌘⇧F). - Split view & Zen mode: edit with a live preview side-by-side (
⌘\, synced scrolling), or go distraction-free with Zen mode (⌘⇧Z,Escto exit). - Obsidian-compatible: reads your vault natively, with
[[wikilinks]](with#heading/#^blockanchors and|aliases) that resolve vault-wide and open,![[embeds]]and partial transclusion, sized![[image.png|300]]embeds,==highlights==, hidden%%comments%%, and KaTeX math. Read-only JSON Canvas (.canvas) viewing with pan/zoom, plus an Open in Obsidian command to hand a note back. Never writes to your.obsidian/config. Details: docs/OBSIDIAN.md. - Reading polish: word count & reading time, footnote hover previews, a path breadcrumb, and copy-link-to-heading on hover.
- Secure by default: malicious Markdown is sanitized (no script/HTML injection), API keys live in the OS keychain (never plaintext), and a strict CSP is enforced. See SECURITY.md.
- Agent-native review loop:
mdopen file.md, themdopener://URL scheme, and an MCP server so Claude Code / Codex / Cursor can open, read, edit, and export the live doc; the activity drawer (⌘B) surfaces new agent.mdfiles the instant they land and "Explain changes" narrates each on-disk edit. One-click MCP setup in Settings. Full guide: docs/AGENTS.md. - Phantom's Markdown viewer: when Ashlr MD is installed, Phantom
hands its inbox and digest Markdown (
phm inbox show --open,phm digest --open) tomdopen. The broader "open any.md" opener is not on Phantom'smasteryet. See docs/PHANTOM.md. - Obsidian integration: the Open in Ashlr MD
plugin sends any vault note to Ashlr MD with one click (ribbon, command, and
right-click) via the
mdopener://scheme. - VS Code integration: the Open in Ashlr MD
extension opens the active
.mdfile in Ashlr MD from an editor title-bar button (or the command palette) via themdopener://scheme, with amdopenCLI fallback. - Smart agent output: callouts, interactive checkboxes that save back to the file, and automatic plan / diff / multi-file detection with badges.
- Three themes: Paper, Sepia, Midnight (light + dark), switch live.
- Native & instant: built on Tauri 2; a tiny native binary on every OS, no Electron bloat. MIT, local-first, no telemetry.
The AI sidebar, semantic search and the MCP sidecar read these environment variables at call time. Unset or blank means the defaults below, which are what the app always used.
| Variable | Default | Accepts |
|---|---|---|
ANTHROPIC_BASE_URL |
https://api.anthropic.com |
an https:// base URL, or http://127.0.0.1 / http://[::1] with optional port; the app appends /v1/messages |
OLLAMA_HOST |
http://localhost:11434 (MCP sidecar: http://127.0.0.1:11434) |
host, host:port, :port or a full URL |
Changed from #131: ANTHROPIC_BASE_URL must be https:// (any host), or plain http:// only for the loopback addresses 127.0.0.1 and [::1]. http://localhost, plain http to any other host, other schemes, malformed URLs, a user name or password in the URL, a query and a fragment are refused when you send a message: the sidebar shows an error, no request is made and your API key is not sent. Anthropic requests do not follow redirects, so the key goes only to that host. The error never repeats the value.
Trailing slashes are trimmed. This lets a gateway such as a LiteLLM proxy or a Phantom proxy sit in front of Anthropic, or points the app at Ollama on another machine. Only the URL handling is covered by tests; the app has not been tested against a specific gateway. The MCP sidecar accepts an https:// OLLAMA_HOST and applies the same rules as ANTHROPIC_BASE_URL: https:// to any host, or plain http:// only to 127.0.0.1 or [::1]. :port means 127.0.0.1:port, and 0.0.0.0 or [::] mean the matching loopback address. Any other value (for example gpu-box:11434 or http://localhost:11434) is refused: no request is made and the sidecar falls back to keyword search (a search that asked for semantic: true says why in its note). The sidecar does not follow redirects from Ollama. A desktop app launched from the Dock or Start menu may not see variables set in your shell profile.
The installer also reports whether the optional Phantom CLI is installed and which version. It runs only phantom --version and never reads or reveals secret values. To run tools with Phantom placeholder keys and base URLs, start them with phantom exec -- <cmd>.
Behavior change for OLLAMA_HOST: the app now accepts OLLAMA_HOST only for a loopback host (127.0.0.1, [::1] or localhost; 0.0.0.0 and [::] mean the matching loopback address). A remote Ollama such as gpu-box:11434 is refused (AI and embeddings report an error, no request is made) unless ~/.ashlr/config.json sets "localInference": { ..., "allowRemote": true } AND the URL is https://. Only the config file can allow remote, never an environment variable.
To use a local OpenAI-compatible server (Ollama, oMLX, a LiteLLM proxy), add a localInference block to ~/.ashlr/config.json. It uses the same shape as Phantom's (ashlrai/phantom#917):
"localInference": {
"provider": "ollama",
"baseUrl": "http://127.0.0.1:11434/v1",
"models": { "coder": "qwen3.8:27b" },
"allowRemote": false
}providerisollama(default base URLhttp://127.0.0.1:11434/v1),omlx(http://127.0.0.1:8000/v1) oropenai-compat(baseUrlrequired).- The model defaults to
qwen3.8:27b. Other options from Phantom's docs/LOCAL-INFERENCE.md:qwen3.6:27b,qwen3.6:35b,qwen3-coder:30b(ollama pull qwen3.8:27b). Abliterated or uncensored models are refused. - The URL must be loopback (
127.0.0.1,[::1]orlocalhost, written exactly). A remote URL needsallowRemote: trueandhttps://. Refusals use closed codes (remote-not-allowed,remote-requires-https,host-not-canonical, ...) and never repeat the URL. - No key is ever sent to this endpoint: no Authorization or x-api-key header, not your Anthropic key, not a
phm_placeholder. If a gateway needs a key, run it throughphantom exec --so the gateway holds the key itself. - When the block is valid, the sidebar uses it before any other provider. An invalid block is ignored and never routes anywhere.
mdopener-mcp doctorchecks that the server answers: one loopback-only GET of the model list, no key, 3 s timeout, redirects refused, at most 256 KiB read. It prints only a status (reachable,unreachable,not-configured,config-invalid,remote-not-probedorbad-response);--jsonfor machines.mdopener-mcp setup --localgets you from nothing to a working local server. It looks for Ollama on127.0.0.1:11434and oMLX on127.0.0.1:8000(the same probe asdoctor: loopback only, no key, 3 s each), probes thelocalInferenceblock when there is one, and prints the exact next steps: the model pull command (ollama pull qwen3.8:27b, or the configured model), thelocalInferenceblock to add with"offline": true, and thedoctorcheck. It downloads nothing and writes nothing: a model is several GB, so you run the pull yourself. Add--jsonfor machines (kind: ashlr.setup-local, withdownloads: falseandwrites: false). oMLX has no pull command; it loads models from its dashboard or the folder given toomlx serve --model-dir.
Set "offline": true on the same localInference block when this machine must not call a cloud model or a model server on another host. The default is off: leaving the key out (or setting false) changes nothing.
"localInference": {
"provider": "ollama",
"offline": true
}While it is on:
- Chat, embeddings, in-app search and
mdopener-mcptalk only to loopback:127.0.0.1,[::1]andlocalhost(that name is pinned to loopback, with no DNS).127.0.0.2,2130706433,localhost.evil.comand a trailing dot are refused. A remotebaseUrlorOLLAMA_HOSTis refused even whenallowRemoteis true, and no environment variable can add a host. - Anthropic and the hosted Ashlr endpoint are not called at all, including through a loopback gateway. The error is
Offline mode (localInference.offline in ~/.ashlr/config.json): cloud AI is disabled. No request was sent.There is no fallback to another cloud provider. - A loopback Ollama or
localInferenceserver still works. On-device Apple Foundation Models are unchanged. Requests still send no API key or OAuth token to a local URL, follow no redirects, and do not use a proxy for loopback. - A value that is present but not a boolean counts as offline (a typo cannot turn the guarantee off). The
localInferenceblock itself must still be a boolean or it is invalid and is not used. mdopener-mcp doctorreports it: a second line,doctor: offline [ok] verified, whenofflineis on, the configured server is loopback and it answered the model-list probe, so no remote endpoint is in use and cloud AI is refused.offline [warn] unverifiedmeansofflineis on but no loopback server answered (AI is unavailable until one does;mdopener-mcp setup --localsays what to run).offline [info] not enabledmeans the flag is off.doctor --jsonadds the same as anofflinecheck (native_statusverified,unverifiedornot-enabled) to theashlr.doctorenvelope, after thelocal-inferencecheck;infonever raises the top-level status.
| Capability | macOS | Windows | Linux |
|---|---|---|---|
| Core (render · edit · export · themes) | ✅ | ✅ | ✅ |
Default .md handler |
✅ one-click (Launch Services) | ✅ registers association (opens Settings to confirm) | ✅ via xdg-mime |
| Free local AI (zero-install) | ✅ Apple Foundation Models (macOS 26+, Apple Silicon) | Ollama (install separately) | Ollama (install separately) |
mdopen CLI & mdopener:// deep link |
✅ | ✅ | ✅ |
| MCP server | ✅ | ✅ | ✅ |
| Command palette · activity drawer · inline AI · outline | ✅ | ✅ | ✅ |
Shown with ⌘ on macOS; use Ctrl on Windows & Linux.
| Action | Shortcut |
|---|---|
| Command palette | ⌘K |
| AI assistant | ⌘L |
| Inline AI rewrite (in editor) | ⌘I |
| Cycle theme | ⌘⇧L |
| Open file | ⌘O |
| Save | ⌘S |
| Export | ⌘E |
| Copy as rich text | ⌘⇧C |
| Insert table | ⌘⇧T |
| Read / Edit / Source view | ⌘1 / ⌘2 / ⌘3 |
| Find in document | ⌘F |
| Find & replace (source) | ⌘⌥F |
| Search across files | ⌘⇧F |
| Split preview | ⌘\ |
| Zen mode | ⌘⇧Z |
| Next / Previous tab | ⌘⇧] / ⌘⇧[ |
| Close tab | ⌘W |
| Agent activity drawer | ⌘B |
| Outline | ⌘⇧O |
| Settings | ⌘, |
| Keyboard shortcuts (cheat sheet) | ⌘/ |
Open Preferences → AI agents (MCP) and click Connect to Claude Code or
Connect to Cursor; no terminal required. For Codex, copy the command from the
same panel. The Agent Activity drawer (⌘B) is the recommended way to watch an
agent's output folder and open new .md files the moment they appear. Full guide
- config snippets: docs/AGENTS.md.
Downloads are on the GitHub Releases page. Full, per-OS instructions live in docs/INSTALL.md.
| Platform | Download (latest release) |
|---|---|
| macOS (Apple Silicon only) | .dmg (drag to /Applications) |
| Windows (x64) | .msi or NSIS -setup.exe installer |
| Linux (x86_64) | .deb · .rpm · .AppImage |
Auto-update is built in on every platform. Intel Macs aren't covered by the current release; build from source (below).
Note: Release builds aren't code-signed yet. On macOS the app isn't notarized, so right-click the app and choose Open the first time. On Windows you may see a SmartScreen warning ("More info → Run anyway"). There are no Homebrew, winget or AUR packages yet; use the release downloads. See docs/RELEASING.md.
There's also a one-shot installer that builds from source, installs the mdopen
CLI, and wires up Claude Code:
bash scripts/install.shThe full macOS source installer builds the Swift sidecars declared in the app's
bundle configuration. It needs Xcode 26 / Swift 6.2+ with the macOS 26 SDK for
mdopener-afm; a missing sidecar stops packaging with its build instructions.
For the Rust CLI and MCP server only, use SKIP_APP_BUILD=1 bash scripts/install.sh;
on Linux that is all the script installs (get the app from the release packages).
Prerequisites: Rust and Bun.
On macOS, also install Xcode Command Line Tools (xcode-select --install).
On Windows, install the MSVC build tools.
On Linux, install build-essential (Debian/Ubuntu) or equivalent, plus the
Tauri Linux dependencies.
bun install
bun run tauri dev # hot-reloading desktop app
bun run tauri dev -- -- file.md # open a file on launchQuality gates (all green in CI):
bun run typecheck # tsc --noEmit
bunx biome check src # lint + format
bun run test # Vitest unit tests
cargo check --workspace --manifest-path src-tauri/Cargo.tomlSource-only MCP setup checks (Python 3, Rust, and Bash; no installer or agent
CLI execution): python3 scripts/test-mcp-setup.py.
The on-device AI sidecar (optional, macOS 26+) builds separately:
cd src-tauri/bins/mdopener-afm && ./build.sh| Layer | What |
|---|---|
src/ |
React 19 + TS frontend: renderer (remark/rehype + Shiki/Mermaid/KaTeX), Milkdown & CodeMirror editors, AI sidebar, export, Zustand stores |
src-tauri/src/ |
Rust core: file I/O, watcher, deep links, AI proxy (reqwest), loopback IPC, on-device AI bridge |
src-tauri/bins/ |
mdopen CLI · mdopener-mcp MCP server · mdopener-afm Swift on-device AI sidecar |
landing/ |
The marketing site (static, deploy-anywhere) |
See docs/VISION.md for the north star and
docs/RELEASING.md for the release process.
mdopener-mcp doctor --fix previews repairs to Ashlr MD's state folder, ~/.mdopener, and writes nothing (exit 0). mdopener-mcp doctor --fix --apply makes them. mdopener-mcp doctor --apply without --fix is refused with exit 2. Add --json for one { mode, plan, outcome } object. This is in source (main, 0.4.1 unreleased) and not yet in a release.
$ mdopener-mcp doctor --fix
doctor --fix (dry run, nothing written):
would fix make owner-only (0644 to 0600): ~/.mdopener/ipc-port
would fix move aside a saved review that does not parse: ~/.mdopener/reviews/<file>.json
Run `mdopener-mcp doctor --fix --apply` to make these changes (a file's contents are backed up before it is moved).
manual ipc-token is mode 0644: other users may read the app's token. It is a secret, so doctor --fix never touches it; run: chmod 600 ~/.mdopener/ipc-tokenWhat it repairs: owner-only permissions (~/.mdopener, sessions/ and reviews/ to 0700; session files, saved reviews and ipc-port to 0600), reporting the previous mode, and a saved review that is oversized, does not parse, has another schema or sits under another file's name, which is backed up to <file>.bak-<UTC stamp> and removed. What stays manual: ipc-token permissions (it is the app's secret, so the tool never opens or changes it; run chmod 600 ~/.mdopener/ipc-token yourself), symlinks and special files, and MCP client registration (Preferences > AI agents). The manual line still prints after --apply. Unix only. Details: docs/doctor-fix.md. To repair every Ashlr tool at once, see Repair everything in the Ashlr Stack guide.
Ashlr MD is one of the open-source tools from Ashlr.AI. Each one installs and runs on its own:
- Phantom (
phm): the agent workbench. Source: ashlrai/phantom. - Phantom Secrets (
phantom): keeps real API keys out of agent context withphm_placeholders. Source: ashlrai/phantom-secrets. - Locus (
locus): pins each agent session to one account. - Lexicon (
lexicon): fixes the words dictation gets wrong. - Ashlr Stack (
stack): provisions services and stores their keys in Phantom Secrets. - ashlr-plugin (
ashlr-plugin): token-efficiency tools for Claude Code and Codex.
Post-quantum readiness: docs/PQ-READINESS.md records what Ashlr MD does with cryptography today (no primitives implemented in the application source; dependencies such as keyring, reqwest and tauri-plugin-updater bring their own, not audited there) and what a migration would touch. It makes no claim that Ashlr MD is quantum-safe.
Contributions welcome! Please read CONTRIBUTING.md. The short version: local-first, no GPL in the bundle, and verify in the real app.
MIT © Ashlr MD contributors