marsdawn for agents
A reference for AI agents and scripts that call the marsdawn command-line tool. Every example on this page was run against the tool built from the current source.
To turn a Markdown file into a PDF, run marsdawn export notes.md --json and read one JSON object from stdout. Mermaid diagrams and highlighted code are rendered the same way as in the MarsDawn app. export doesn't need the app; open does.
What it does
exportrenders one Markdown file to a paginated PDF with the same exporter as the MarsDawn app. No window opens.openopens one or more Markdown files in the MarsDawn app, so a person can review them, can name the line each file should land on, and can show a folder in the window's sidebar.
What it does not do
- It doesn't read Markdown from stdin. Pass a file path.
- It doesn't write the PDF to stdout. The PDF always goes to a file; stdout carries only the result.
- It doesn't replace an existing file unless you pass
--force. - It doesn't load images from the web unless you pass
--allow-remote-images, and then only over https. opendoesn't work without the MarsDawn app installed; it exits with code 3.exportdoesn't need the app. The app is on the Mac App Store.- MarsDawn 1.0 opens the file at the line
opennames. - It runs on macOS only.
export
marsdawn export notes.md --json
Writes notes.pdf next to notes.md. Options:
-o, --output <path>: where to write the PDF. Defaults to the input path with a.pdfextension.--theme <dawn|classic|modern|vivid>: the theme's light palette. Defaults to$MARSDAWN_THEME, thendawn.--paper <a4|letter>: paper size. Defaults toa4.--allow-remote-images: load https images from the web while rendering.--force: replace the output file if it exists.--json: print one JSON object on stdout instead of text.
marsdawn export notes.md -o out.pdf --theme classic --paper letter --force --json
Success, exit code 0:
{"diagramErrors":[],"ok":true,"output":"/path/to/out.pdf","pages":1,"paper":"letter","theme":"classic"}
output: absolute path of the PDF that was written.pages: number of pages.themeandpaper: the values used.diagramErrors: one message per Mermaid diagram that failed to render. The PDF is still written.
open
marsdawn open notes.md --json
marsdawn open notes.md:120 --json
marsdawn open notes.md --line 120 --json
marsdawn open . --json
marsdawn open notes.md --folder . --background --json
path:linenames the line to land on. A column after it, as innotes.md:120:8, is ignored. An argument that names a file which exists is always that whole filename, so a file calledweird:12opens as itself.--line <n>names the line for a single file, including a path that itself ends in a colon and digits. It needs exactly one file.- Lines run from 1 to 999999999. Anything else is a usage error.
- Lines were added in marsdawn 0.3.0. MarsDawn 1.0 opens the file at that line.
- A folder argument opens in the window's sidebar instead of as a document, so
marsdawn open .shows the current folder;--folder <path>does the same alongside files. A window's sidebar shows one folder: naming two is a usage error, and so is using--foldertwice, even for the same folder; the same folder given again as an argument counts once.--linewith a folder is a usage error, since a folder has no line. There is no-a: passing it is a usage error that points at--folder. --backgroundopens without bringing MarsDawn to the front, for an agent that opens files while the person works elsewhere. The JSON is the same either way.- Folders and
--backgroundwere added in marsdawn 0.5.1.
Success, exit code 0:
{"app":"/Applications/MarsDawn.app","ok":true,"opened":[{"line":120,"path":"/path/to/notes.md"}]}
opened: one object per file, in the order given.pathis the file's absolute path;lineappears only when a line was asked for.app: path of the MarsDawn app that opened them.
With a folder (marsdawn 0.5.1 and later), exit code 0:
{"app":"/Applications/MarsDawn.app","folder":{"path":"/path/to/project","requested":true},"ok":true,"opened":[{"path":"/path/to/project/notes.md"}]}
folder: present only when a folder was given.pathis its absolute path.requestedis alwaystrue: marsdawn asked MarsDawn to show the folder, and can't tell whether the sidebar shows it, because the app may first ask the person for access. Report it as asked, not as done.- With
--waitabove 0 and MarsDawn 1.0.2 or later,folderalso carriesstatusonce the wait ends:attached,needsUser,declined,failed,attachedDifferentFolder,full,unavailableorunknown. Only alongsideneedsUser, it carrieswaitingFor:confirmationorfolderChoice. Without--wait, or with an app that doesn't report back,folderstays{"path": …, "requested": true}. A--waitoutside 0 to 30 exits 64 witherror: "wait_out_of_range". (marsdawn 0.5.3 and later.) openedis empty when only a folder was given.
marsdawn 0.2.x printed opened as a list of path strings. Check marsdawn --version if you need to handle both.
Open files as Claude Code edits them
An opt-in Claude Code hook: after Claude writes or edits a Markdown file, it opens that file in MarsDawn in the background, once per file per session. It's off unless you add it, one project at a time, because a window you didn't ask for takes attention. It runs a shell command and costs no model tokens.
It needs marsdawn 0.5.1 or later, for --background, and the MarsDawn app.
Save this as .claude/hooks/marsdawn-open.sh in your project, and make it executable with chmod +x:
#!/bin/sh
# Claude Code PostToolUse hook: open a Markdown file Claude just wrote or edited in MarsDawn,
# in the background, once per file per session. Never blocks Claude: every path exits 0.
input=$(cat)
file=$(printf '%s' "$input" | /usr/bin/jq -r '.tool_input.file_path // empty' 2>/dev/null)
session=$(printf '%s' "$input" | /usr/bin/jq -r '.session_id // "unknown"' 2>/dev/null)
case "$file" in
*.md|*.markdown) ;;
*) exit 0 ;;
esac
[ -f "$file" ] || exit 0
# A hook runs with Claude Code's PATH, which may not include Homebrew's.
marsdawn=$(command -v marsdawn || { [ -x /opt/homebrew/bin/marsdawn ] && echo /opt/homebrew/bin/marsdawn; }) || exit 0
[ -n "$marsdawn" ] || exit 0
# One list per session, so a file opens once however often Claude edits it.
seen="${TMPDIR:-/tmp}/marsdawn-hook/$session"
mkdir -p "$(dirname "$seen")"
grep -qxF "$file" "$seen" 2>/dev/null && exit 0
echo "$file" >> "$seen"
"$marsdawn" open --background "$file" >/dev/null 2>&1 || true
exit 0
Then add the hook to .claude/settings.json in the project, or to .claude/settings.local.json to keep it to yourself:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/marsdawn-open.sh" }
]
}
]
}
}
- It runs after Claude's Write and Edit tools. Files that don't end in
.mdor.markdownare left alone. - Each file opens once per Claude Code session, however often Claude edits it. The list lives in
$TMPDIR/marsdawn-hook/, one file per session, so a new session opens the file again. --backgroundkeeps MarsDawn from coming to the front: the window you were working in keeps focus.- It never gets in Claude's way. Every path exits 0, and if marsdawn or the MarsDawn app isn't installed, nothing happens.
- It reads the hook's input with
/usr/bin/jq, which comes with macOS 26, the version the MarsDawn app needs. - To turn it off, remove the entry from the settings file.
Failures
With --json, a failure prints one JSON object on stdout and exits with its code:
{"error":"output_exists","message":"/path/to/notes.pdf already exists. Pass --force to replace it.","ok":false}
2,input_not_found: the input doesn't exist, is a folder, or isn't UTF-8 text; or a--folderpath doesn't exist or isn't a folder.3,app_not_installed: MarsDawn isn't installed. Onlyopenreturns this.4,output_exists: the output file exists. Pass--force.5,export_failed: the export itself failed.6,app_cannot_open_folders: this MarsDawn can't show a folder, so nothing was opened. Onlyopenreturns this.64: usage error, such as an unknown option, an invalid value, a line out of range,--linewith more than one file or with a folder, more than one folder, or-a. This one is printed as text on stderr, even with--json.
JSON Schemas
JSON Schema (draft 2020-12) for every --json result:
- export.v1.json: export success
- open.v4.json: open success, marsdawn 0.5.3 and later, with the folder's
statusandwaitingFor - open.v3.json: open success, marsdawn 0.5.1 and 0.5.2, including a folder shown in the sidebar
- error.v3.json: failure, every command, marsdawn 0.7.1 and later, adding
input_too_large(exit 5) andskill_install_failed(exit 7) - error.v2.json: failure, both commands, marsdawn 0.5.2 to 0.7.0
- open.v2.json: open success, marsdawn 0.3.0 to 0.5.0
- open.v1.json: open success, marsdawn 0.2.x, where
openedwas a list of paths - error.v1.json: failure, both commands, marsdawn 0.5.1 and earlier
Environment variables
MARSDAWN_THEME: the themeexportuses when--themeisn't passed. An unknown value falls back todawnwithout an error.
Requirements
- The tool runs on macOS 15 or later. On Apple silicon, Homebrew installs a prebuilt bottle and nothing else is needed. Building it yourself, on an Intel Mac or from the source, needs Swift 6.2 or later, which comes with Xcode 26 or later.
- The MarsDawn app needs macOS 26 or later.
Install
With Homebrew. On Apple silicon it pours a prebuilt bottle in seconds, with no Xcode needed. On an Intel Mac it compiles marsdawn from source, which takes a few minutes and needs Xcode 26 or later.
brew tap redtear1115/tap && brew install marsdawn
marsdawn --version
Or build it from the source. The first build fetches dependencies and compiles, which also takes a few minutes.
git clone https://github.com/redtear1115/mars-dawn-kit.git
cd mars-dawn-kit
swift build -c release --product marsdawn
.build/release/marsdawn export notes.md --json
marsdawn --version prints the version number, such as 0.3.0, and exits with code 0.
Next
- A one-file skill for agents that read instructions instead of a shell: the marsdawn skill.
- An MCP server that wraps this same
export: marsdawn-mcp. - Why this JSON result stays cheap for an agent's own context: token-efficient review.
- Making a MarsDawn theme with an agent: make a theme with your agent.