Markdown-do

Getting Started

Markdown-do (mdd) manages the TODO.md file in your project from the terminal. It’s a single binary with no server, database or account. Tasks are plain markdown checkboxes, so the file stays readable in any editor even if you never run mdd again.

A terminal recording touring mdd: add, list, find, toggle, complete, edit, annotate, notes, lint, clear, archive, tag and untag, undo, remove, recursive list and find, shell completion, config and open

Installation

With Go installed:

go install github.com/i-am-fran/markdown-do/v3/cmd/mdd@latest

Without Go:

  1. Download the binary for your system from the latest release: mdd-darwin-arm64 or mdd-darwin-amd64 for a Mac, mdd-linux-amd64 or mdd-linux-arm64 for Linux, mdd-windows-amd64.exe for Windows.
  2. Rename it to mdd, make it executable (chmod +x mdd) and move it to a folder on your PATH.

From source:

git clone https://github.com/i-am-fran/markdown-do.git
cd markdown-do
make install    # or make build, which puts the binary in ./build/mdd

To update later, run mdd update (see update).

First steps

Run mdd in the folder of a project. It uses the TODO.md there (or todo.md), and creates TODO.md the first time you add a task.

mdd Reply to Sam about the invoice     # add a task to the inbox
mdd "Fix the date parser @bb"          # add a task under ## Bugs
mdd list                               # number and list every task
mdd complete 1                         # tick off task 1

mdd on its own prints a short banner with the most-used commands. mdd help prints all of them.

Task format

Tasks are standard markdown checkboxes, plus a third state for work in progress:

## Features

- [ ] Pending task
- [/] In-progress task
- [x] Completed task
  - A note about the task
  - Another note

Indented plain bullets under a task (no checkbox) are its notes. You add them with mdd annotate, and they move with the task when it’s edited, completed, archived or removed.

mdd always reads [/] correctly, but toggling only goes through it when the enableInProgress setting is on.

On every save, tasks in each section are reordered: open tasks first, completed ones after them. Completed tasks stay in the file until you clear or archive them.


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) — 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
-a, --active Only open tasks (pending and in progress)
--done Only completed tasks
mdd list
mdd list -r
mdd list -a

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).

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 at the end of the file, just above ## 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 Settings).

-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 Settings for the full 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. Running mdd with no arguments prints a shorter banner with the version and the most-used commands.

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:

  1. TODO.md in the current directory
  2. todo.md in the current directory
  3. Creates TODO.md if none exists

For recursive operations (list -r, find -r), it searches all subdirectories.


All features

The file

  • Plain markdown checkboxes: - [ ], - [x], and - [/] for in progress
  • ## Sections, with an inbox at the top for tasks without one
  • Uses TODO.md or todo.md in the current folder, creates TODO.md if there’s neither
  • Open tasks sorted above completed ones in each section on every save
  • Notes under a task as indented bullets, kept with it through every change
  • A file-wide ## Notes section, always kept at the end
  • Nested checklist items keep their indentation
  • Atomic saves with a lock file, so a crash or a second mdd can’t corrupt the file

Adding

  • mdd <text> adds a task, no quotes needed
  • mdd add <text> forces an add when the text looks like a command
  • @Section files a task under a section, creating it if needed
  • Built-in shortcuts: @ff Features, @bb Bugs, @ii Ideas, @ww Warnings
  • Your own shortcuts with mdd config set alias.<name> <Section>
  • \@ for a literal @, \\ for a literal backslash
  • --path <dir> adds to another folder’s TODO file
  • New tasks pick up the next ID once the file is tagged
  • Task text must be at least 3 characters
  • A mistyped single-word command (mdd lis) suggests the real one instead of adding a task
  • mdd notes <text> adds a line to ## Notes

Viewing and finding

  • mdd list groups tasks by section, inbox first, with notes inline
  • -a/--active for open tasks only, --done for completed only
  • -r lists every TODO.md in subfolders, grouped by folder
  • Numbers from a recursive list keep working in the next command
  • mdd find <keyword>, case-insensitive, with -r for subfolders

Managing

  • toggle flips pending and completed, or cycles through in progress
  • complete takes several IDs at once and skips any that don’t exist
  • edit replaces a task’s text
  • annotate adds a note under one task
  • remove deletes one task, clear deletes every completed task
  • Optional y/n prompt before deleting, skipped with -y
  • archive moves completed tasks to ## Archive, adding “(from Section)”
  • undo reverts the last change; run it again to redo

Task IDs

  • mdd tag ABC gives every task an ID like ABC-001
  • Any three-letter prefix, normalised to capitals
  • Every command that takes a task number also takes its ID
  • mdd untag removes them all

Utilities

  • lint removes empty sections, empty tasks and extra blank lines
  • lint fixes heading spacing, [], [X] and malformed checkboxes
  • lint turns checkboxes under ## Notes into plain bullets
  • open opens the file in your editor
  • Tab completion for bash and zsh: commands and section names
  • update installs the latest release after checking its signature
  • Short aliases for every command: -l, -f, -t, -c, -e, -an, -d, -n, -o, -dc, -lint, -id
  • -h and -v work anywhere in the command
  • A banner with the most-used commands when you run mdd on its own

Settings

  • mdd config list, get, set and edit
  • Show or hide completed tasks
  • Editor: system ($EDITOR), vim, nano or the default app
  • In-progress state for toggling
  • Confirm before deleting
  • A broken config.json prints a warning and falls back to defaults

More

Limits

  • mdd reads and writes one kind of file: markdown checkboxes under ## headings. Other markdown in the file is kept, but mdd lint may tidy blank lines around it.
  • Saves are atomic, but two mdd commands started at the same moment on the same file can both read it before either writes. The second save wins.
  • undo keeps one step. Running it twice puts the change back.

Support

Markdown-do is free. If it helps you, you can buy me a coffee. Found a bug? Please open an issue.

Changes are listed in the changelog. Markdown-do is MIT licensed.