A Kanban TUI and agent-friendly CLI over a plain markdown TODO.md.
The markdown file is the single source of truth — readable and diffable for
humans, renderable on GitHub, and deterministic for the tool. Humans use the
TUI; AI agents drive the CLI (--json everywhere, stable task IDs, meaningful
exit codes).
macOS or Linux — installs (and updates) the latest release to
~/.local/bin, verifying the checksum:
curl -fsSL https://raw.githubusercontent.com/walm/todomd/main/install.sh | shOr grab a binary from the releases page, or build from source:
go install github.com/walm/todomd@latest # or: go build -o todomd .todomd --version prints the version.
todomd upgrade # download the latest release and replace this binary
todomd upgrade --check # just report what's availableIt verifies the release's published sha256 before swapping the binary, and leaves the old one in place if anything fails. If todomd lives somewhere you can't write, it says so rather than half-upgrading. (Re-running the install script above works too, and is the right move for a source build.)
todomd mentions a newer release only where a human is looking: in the TUI
footer and at the end of todomd --help (on stderr, so --help output itself
is unchanged). No other command ever prints it, so scripts and agents are
unaffected. The check runs at most once a day, in the background, from a
cached answer — and TODOMD_NO_UPDATE_CHECK=1 turns it off entirely.
todomd init # creates TODO.md with Backlog / In Progress / Done
todomd # opens the Kanban TUItodomd add "Fix the parser" --tag parser --priority high --due 2026-08-01 --json
todomd list --priority high --json # what to work on first
todomd list --json # everything, grouped by board
todomd show 3f2a --json # full task detail
todomd update 3f2a --title "New" --priority low --tag a --clear-due
todomd move 3f2a --to "In Progress" --pos 1
todomd done 3f2a
todomd comment 3f2a --author ai "Tried X, going with Y."
todomd delete 3f2a --yes
todomd boards --json
todomd boards delete Review # --force if it still holds tasks
todomd archive --dry-run # what clearing Done would remove
todomd changes --as claude --ignore-author claude --jsontodomd changes answers "what happened since I last looked" without the
agent having to read or diff the whole file. Each consumer names a cursor
with --as; reading advances it (--peek doesn't). The first call just
initializes the cursor. Because it diffs snapshots of the file (stored under
$XDG_STATE_HOME/todomd, default ~/.local/state/todomd, keyed by the
file's path), it catches every source of change: CLI, TUI, $EDITOR,
hand edits, formatters, git pulls.
Events: task_added (includes the full task as detail), task_deleted,
task_moved (from/to), task_updated (fields with old/new per
changed field — renames stay the same task, identity is the ID),
comment_added (the comment). Reorders within a board are not reported.
A consumer is never notified about its own writes. Pass the same --as
name when you write (or set TODOMD_CURSOR once) and that write is folded
into your own cursor, so it isn't reported back to you — while everyone
else's cursor still sees it. Only what you changed is skipped: if a human
comments on the very task you just moved, you still get that comment.
The typical agent loop:
export TODOMD_CURSOR=claude # or pass --as claude to each call
todomd move 3f2a --to "In Progress" # mine: won't come back as an event
todomd comment 3f2a --author claude "done, please review"
# …later…
todomd changes --json # only what others did- File resolution:
--fileflag >TODOMD_FILEenv >TODO.mdsearched from the cwd upward (stopping at the repo root). - IDs are stable 4-char base36 (they survive renames and moves) and may be abbreviated to any unique prefix. Deleted IDs may be reused later — don't hold IDs across deletions.
- Priority is
high,normal(the default) orlow, onadd/updateand as alist --priorityfilter. It's advisory: the suggested convention is to work High first, then Normal, then Low — todomd records it and leaves the ordering to you."priority"is always present in JSON, so an agent never has to infer the default. - Exit codes:
0ok ·1general error ·2task not found ·3ambiguous ID prefix. - Boards are matched case-insensitively and created on demand (new boards land
before
Done).boards delete <name>removes one; an empty board goes straight away, one holding tasks needs--forcebecause its tasks go with it. - Concurrent invocations are safe: every write takes an advisory lock
(kept in
~/.local/state/todomd, never next to your file) and replaces the file atomically. - Any text is safe to pass — titles reject newlines, and description/comment
lines that would read as file structure are escaped on write and unescaped
on parse. The one restriction: multi-line text must not contain an unclosed
| Keys | Action |
|---|---|
h/l j/k |
Navigate columns / cards (g/G first/last) |
H/L |
Move task to previous / next board |
J/K |
Reorder task within its board |
Enter |
Open task detail (modal over the board; q/esc back) |
a / e |
Add / edit task (tab next field, ctrl+s save, esc cancel) |
E |
Edit the task as markdown in $VISUAL/$EDITOR (title, tags, due, description, comments) |
p |
Cycle priority (normal → high → low) |
(in the add/edit form, priority is a ←/→ select) |
|
c |
Comment on task |
d / D |
Delete task (confirm) / move to Done |
X |
Delete the current board (asks first if it holds tasks) |
A |
Mark every card as read (clear all badges) |
/ |
Search: filters as you type (enter keeps it, esc cancels) |
u |
Show only changed/unread cards |
esc |
Clear the active filter |
r |
Reload from disk |
? / q |
Toggle help / quit |
Inside the open task: j/k scroll, ctrl+d/ctrl+u half a page, g/G
jump to the top and bottom, and the title, id, board, tags, priority and due
date stay pinned above the scrolling body. e, E, c and p work there too
and return you to the task afterwards.
🖱️ Mouse works alongside the keys: click a card to select it, click it again to
open; inside the open task the footer hints (e edit · E editor · c comment) are clickable and tapping outside the card closes it. Forms use
the same style (ctrl+s save · esc cancel), and on the board footer
a add, ? help, and q quit are clickable too — every actionable label
underlines on hover. Column headers select their column, and the wheel
scrolls (cards on the board, text in the open task). Terminals need
shift-click to select text for copying while mouse mode is on.
The TUI tracks what changed since you last looked (its own tui change
cursor): cards added by someone else show ● with a green border, cards
updated/moved/commented show ○ with a yellow border, and the status line
counts them on startup. Opening a card marks it read, A marks the whole
board read at once; your own actions never badge; unread state persists across
sessions.
While you idle on the board, the TUI auto-reloads: it stats the file every
2s and refreshes (badging changed cards, keeping your selection) whenever
the file actually changed — so agent activity appears live. Auto-reload
pauses while a task, form, or confirm prompt is open; r still forces a
reload any time.
The TUI auto-detects light/dark background at startup. Set GLAMOUR_STYLE
(dark, light, notty, …) to pin the theme and skip the terminal query —
useful for terminals that don't answer OSC color queries.
Prefer a browser? todomd-web is a
separate application serving the same board over HTTP — one Go binary, no
database, the same TODO.md as the source of truth.
# TODO
Free-form preamble — never touched by the tool.
## Backlog
### Implement markdown parser
<!-- id:3f2a -->
`#parser` `#core` **priority:** high **due:** 2026-08-01
Description: any markdown, multiple paragraphs, code fences, lists.
#### Comments
- **ai** (2026-07-18): Considered goldmark; hand-rolled round-trips better.
- **user** (2026-07-18): Agreed.
## In Progress
## DoneRules, briefly:
##headings are boards (column order = file order);###headings are tasks (card order = file order). Empty boards persist.- The
<!-- id:… -->comment is the task's stable ID (invisible when rendered). Hand-added tasks without one get an ID on the next write. - The metadata line (tags, priority, due) is the first non-blank line below
the ID comment (formatter-inserted blank lines are fine).
**priority:**ishighorlow; the defaultnormalis left out, so ordinary tasks stay clean. - Everything up to
#### Comments/ the next heading is the description, preserved verbatim. - Comments are one list item each:
- **author** (YYYY-MM-DD): text, with continuation lines indented two spaces. - Hand-editing is fine; the tool re-canonicalizes spacing on its next write and reports malformed content with line numbers.
todomd is built for the active work of a project — a few boards, dozens of tasks. It is not meant to hold hundreds of tasks: a board you can't read in one glance has stopped being a board.
- Delete Done tasks once they've been reviewed —
todomd delete <id> --yes(ordin the TUI). Agents should clear confirmed Done items as part of their loop instead of letting the board grow. todomd archiveclears the whole Done board in one go. Because that is bulk and destructive it confirms first, and it refuses unless the tasks would survive somewhere: either the file is committed to git (sogit log -p TODO.mdstill has them) or you pass--to FILEto move them into another markdown board.--dry-runpreviews,--forceoverrides the git check,--yesskips the prompt (required when not on a terminal).- Archive and history are git's job. Keep
TODO.mdin version control and nothing is ever lost: every task, move, and comment lives in the file's history (git log -p TODO.md), which beats any archive board.
todomd follows semver (0.x: minor bumps may break — called out in CHANGELOG.md). Versions come from git tags; goreleaser stamps the binary. To cut a release:
- Add a
## vX.Y.Zsection toCHANGELOG.mdand commit. git tag vX.Y.Z && git push origin main --tags
The release workflow runs the tests, cross-builds darwin/linux (amd64 + arm64) archives, and publishes a GitHub release.
MIT — free to use however you like; please keep the notice, which points back to this repo as the source.

