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.

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

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 armdd 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 opensystem, 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.