Getting Started
Markdown-do (mdd) is a single Go binary for managing TODO.md files with standard markdown checkbox syntax. No server, no lock-in: the file stays readable in any editor even if you never install mdd again.

Installation
Download the latest binary for your platform from the releases page, or build from source:
git clone https://github.com/i-am-fran/markdown-do.git
cd markdown-do
make build
# Binary is at ./build/mdd
# Or install to GOPATH/bin:
make install
Or install directly with Go:
go install github.com/i-am-fran/markdown-do/v3/cmd/mdd@latest
Quick Start
# Add your first task
mdd Buy milk
# List tasks
mdd list
# Complete task #1
mdd complete 1
Task Format
Tasks are standard markdown checkboxes, plus a non-standard third state:
## Features
- [ ] Pending task
- [/] In-progress task (only reachable via toggle when enableInProgress is on)
- [x] Completed task
- A note about the task
- Another note
A task’s trailing indented plain-bullet lines (no checkbox) are its notes — added with mdd annotate <id> "text", they always travel with the task through edit, move, delete, and complete. On save, tasks within each section are automatically reordered: pending first, then completed, keeping active work visible at the top.
Adding Tasks
Adding is the default action — if what you type doesn’t look like one of the commands below, it becomes a new task.
add
mdd <text> and mdd add <text> do the same thing; quotes are only needed if your shell would otherwise split the text into multiple arguments. Task text must be at least 3 characters. New tasks land in the inbox at the top of the file unless you file them into a section with @Section (see Sections below) — and if the file already has an active ID sequence from mdd tag, the new task automatically picks up the next ID in it.
mdd Buy groceries
mdd "Fix login bug @bb"
mdd add "-1, needs more thought first" # forces add; a leading "-" is otherwise read as a flag
Add --path <dir> to add to the TODO file in <dir> instead of the current directory:
mdd add "Renew SSL cert" --path ~/projects/infra
notes
mdd notes <text> adds a line to the file-wide ## Notes section (created automatically if it doesn’t exist yet) — for context that isn’t itself a task, like a reminder or a decision you want on record. This is different from annotate, which attaches a note to one specific task.
-n is a short-hand alias for notes.
mdd notes "API rate limit is 100 req/min"
mdd notes Deploy window is 2-4am UTC
Sections
End a task with @Section to file it there — the section is created automatically if it doesn’t exist yet. Tasks added without a section go to the inbox at the top of the file.
mdd "New feature @Features" # Adds to ## Features (creates if needed)
mdd "Another task" # Adds to the inbox (top of file)
Built-in section aliases, case-insensitive: @ff → Features, @bb → Bugs, @ii → Ideas, @ww → Warnings. Add your own with mdd config set alias.<name> <Section>.
Escaping & Quoting
Prefix a character with \ to keep it literal instead of special. This only applies to add’s text — edit/annotate text is always stored literally.
mdd 'Meet Bob \@bb' # adds a task ending in a literal "@bb" instead of filing it under Bugs
mdd 'Cost is \\$10' # \\ for a literal backslash
Some shells (zsh in particular) treat an unquoted trailing ? or * as a glob pattern and will error with “no matches found” before mdd ever runs — quote task text containing those characters, e.g. mdd "Ping the vendor?".
Viewing & Finding
Read-only commands for seeing what’s in a TODO.md.
list
mdd list prints every task grouped by section in file order, with the inbox shown first. Add -r/--recursive to walk every subdirectory instead of just the current one — in that mode, task numbers become temporary global IDs cached to disk, so a follow-up like mdd complete 14 still resolves to the right task even though it lives in a different file than task 1.
-l is a short-hand alias for list (e.g. mdd -l -r).
| Flag | Description |
|---|---|
-r, --recursive |
List tasks from every TODO.md/todo.md found in subdirectories, grouped by directory |
mdd list
mdd list -r
find
mdd find <keyword> searches every task’s text for a case-insensitive substring match, in the current file by default. Add -r to search every TODO.md in subdirectories too.
-f is a short-hand alias for find (e.g. mdd -f bug -r).
| Flag | Description |
|---|---|
-r, --recursive |
Search every TODO.md/todo.md found in subdirectories |
mdd find bug
mdd find auth -r
Managing Tasks
Commands that change a task’s status or content. All of them take an <id> — a task’s position number from mdd list, or a stable ID tag once you’ve run mdd tag (see Task IDs below).
toggle
mdd toggle <id> cycles a task’s status. By default that’s a simple flip: pending <-> completed. With the enableInProgress setting on, it becomes a 3-way cycle: pending -> in-progress -> completed -> pending.
-t is a short-hand alias for toggle.
mdd toggle 2
complete
mdd complete <id> [id2 id3 ...] marks one or more tasks complete directly, skipping the toggle cycle. Each ID is resolved independently, so mdd complete 1 2 3 still completes #2 and #3 even if #1 doesn’t exist — you get a warning about the one that failed instead of the whole command aborting.
-c is a short-hand alias for complete, and takes multiple IDs the same way (e.g. mdd -c 1 2 3).
mdd complete 1
mdd complete 1 2 3
edit
mdd edit <id> <text> replaces a task’s text outright — it’s a full overwrite, not an append. Multi-word text needs no quotes.
-e is a short-hand alias for edit.
mdd edit 1 Fix the login bug instead
annotate
mdd annotate <id> <text> adds an indented note line under one specific task, shown inline wherever that task is listed. Not the same as notes, which is file-wide rather than attached to a task. Notes travel with the task through edit, move, complete, and delete.
-an is a short-hand alias for annotate.
mdd annotate 1 Needs a second reviewer
remove
mdd remove <id> deletes one task. Prompts for a y/n confirmation first if the confirmDestructive setting is on; add -y/--yes to skip it, which is also handy when calling mdd from a script.
-d is a short-hand alias for remove.
| Flag | Description |
|---|---|
-y, --yes |
Skip the confirmation prompt (only matters when confirmDestructive is on) |
mdd remove 3
mdd remove 3 -y
clear
mdd clear deletes every completed task in the file at once. Same confirmation behavior as remove.
-dc is a short-hand alias for clear.
| Flag | Description |
|---|---|
-y, --yes |
Skip the confirmation prompt (only matters when confirmDestructive is on) |
mdd clear
mdd clear -y
archive
mdd archive moves every completed task into a ## Archive section instead of deleting it, appending “(from <Section>)” to each one so you can still tell where it came from. The Archive section is always kept last in the file, after ## Notes. Unlike remove/clear, this isn’t gated by confirmDestructive — nothing is actually deleted.
mdd archive
undo
mdd undo reverts the last change made to the TODO file. Because the restore is written through the normal save path, running mdd undo again re-applies the change it just undid — it’s a toggle, not a multi-step history.
mdd undo
Task IDs
A task’s position number shifts every time the file is re-sorted or a task above it is removed — fine for a quick mdd complete 1 right after mdd list, less fine if you want to refer back to the same task tomorrow, or hand a task reference to an LLM across a session boundary. Tag the file once and you get a stable ID that survives all of that.
tag
mdd tag <PREFIX> tags every task in the file with sequential IDs: PREFIX-001, PREFIX-002, and so on. PREFIX must be exactly 3 letters (case-insensitive — abc is normalized to ABC). Once a sequence is active, any task you add afterward automatically picks up the next ID in it.
-id is a short-hand alias for tag (e.g. mdd -id ABC). untag has no short alias.
mdd tag ABC # Tags all tasks: [ABC-001], [ABC-002], ...
mdd complete ABC-001 # Reference a task by its stable ID
untag
mdd untag strips every ID tag from the file. Printing “No task IDs to remove” and exiting cleanly if there weren’t any to begin with.
mdd untag
Any command above that takes an <id> — toggle, complete, edit, annotate, remove — accepts either a task’s position number or its stable ID tag, once IDs have been assigned.
Utilities
open
mdd open opens TODO.md in the editor set by the editor setting (see Configuration below).
-o is a short-hand alias for open.
mdd open
lint
mdd lint checks the file for formatting issues and fixes what it can safely fix automatically, printing a line number, description, and status (found or fixed) for everything it touched.
-lint is a short-hand alias for lint.
| Check | Behaviour |
|---|---|
| Empty sections | A ## Header followed by nothing but blank lines is removed entirely |
| Heading spacing | Exactly one blank line enforced above and below every ## Header |
| Blank lines | Runs of 2+ blank lines, including trailing ones at end of file, collapsed to one |
| Empty tasks | A - [ ] with no text after it is removed |
Tasks under ## Notes |
Converted to plain - list items — checkboxes don’t belong in a notes section |
| Empty checkbox | - [] becomes - [ ] |
Uppercase X |
- [X] becomes - [x] |
| Malformed checkbox content | Anything else inside the brackets is normalized to whichever of [ ]/[x] it most resembles |
mdd lint
config
mdd config reads and changes settings without hand-editing config.json — see Configuration below for the full settings table.
| Subcommand | Description |
|---|---|
list |
Print every setting and its current value |
get <key> |
Print one setting’s value |
set <key> <value> |
Change a setting and persist it |
edit |
Open config.json directly in your configured editor, for changes the other three don’t cover (e.g. hand-editing sectionAliases as JSON) |
mdd config list
mdd config get editor
mdd config set editor vim
mdd config set alias.wk Work
mdd config edit
completion
mdd completion bash / mdd completion zsh print a shell completion script to stdout. It tab-completes command verbs (mdd ar → mdd archive) and section tags (@Bu → @Bugs, including your custom aliases and any section already in TODO.md).
source <(mdd completion bash) # add to ~/.bashrc
source <(mdd completion zsh) # add to ~/.zshrc
update
mdd update downloads the latest release binary and swaps it in for the one currently running. The downloaded checksums.txt is verified against an ed25519 signature embedded in the binary before anything is replaced, so a compromised or tampered download is rejected rather than installed.
mdd update
version
mdd version prints the current version. Same as passing -v/--version anywhere in the command.
mdd version
help
mdd help prints the full command reference. Same as passing -h/--help anywhere in the command, or running mdd with no arguments at all.
mdd help
Configuration
Settings
Settings are stored in ~/.config/markdowndo/config.json, editable via mdd config get/set or mdd config edit.
| Setting | Default | Description |
|---|---|---|
showCompleted |
true |
Show completed tasks in lists. |
editor |
"system" |
Editor used by mdd open — system, vim, nano, or default-app. |
enableInProgress |
false |
Enables the - [/] in-progress checkbox state and the 3-way toggle cycle. |
confirmDestructive |
false |
Require a y/n prompt before remove/clear (skip with -y/--yes). |
sectionAliases |
{} |
Custom @alias shortcuts layered on top of the built-in ones. |
If config.json can’t be parsed (invalid JSON, corrupted by hand-editing, etc.), mdd prints a warning to stderr naming the file and falls back to defaults for that run, rather than silently using defaults with no indication anything was wrong.
File Discovery
Markdown-do looks for these files, in order:
TODO.mdin the current directorytodo.mdin the current directory- Creates
TODO.mdif none exists
For recursive operations (list -r, find -r), it searches all subdirectories.
Markdown-do