Backlog.md is the default task tracker on my repositories. It keeps each task as a Markdown file under backlog/ in the repository and drives it from a CLI, so agents read and write it the same way I do and it travels with the code. Most of what I’ve learned setting it up is about what an agent can break, and nearly all of it is decided in the first command.
This is written against Backlog.md 1.53.0. Several of the behaviours below changed in patch releases, so check backlog --version and the relevant --help before trusting any of it.
Three settings you only choose once
The task prefix, the zero-padding width and the backlog directory are fixed at init. Pick them before running anything:
backlog init "<ProjectName>" --defaults \
--task-prefix <letters> --zero-padded-ids 4 \
--backlog-dir backlog --config-location folder \
--agent-instructions agents \
--check-branches false --include-remote false \
--install-claude-agent false
printf '@AGENTS.md\n' > CLAUDE.md
The prefix is letters only. A digit is rejected, so a short code like m7c fails, and draft, doc and decision are reserved. Never keep the default task: once an agent pastes an ID from one repository into another, task-12 could be anything. A real prefix gives RKB-0012, which can only mean one board.
Zero padding isn’t retroactive. Turn it on after the ninth task and the board carries ABC-9 and ABC-0010 side by side forever, sorting wrongly in every listing. The directory can’t move either: init refuses to relocate it, and my own tooling hardcodes backlog/docs.
--agent-instructions agents writes upstream’s instruction block into AGENTS.md between markers, and CLAUDE.md becomes a one-line import of it, so Claude Code and Codex both read one file. That block belongs to the tool. Anything repository-specific written inside its markers is gone the next time the block is refreshed, so the repo’s own rules sit outside them.
The config I edit by hand
List values can’t be set through backlog config set, so backlog/config.yml is the one Backlog file I edit by hand:
statuses: ["To Do", "In Progress", "Parked", "Done"]
definition_of_done: ["just check"]
auto_commit: false
Parked matters most. It means attempted, blocked and left with a note on where to resume, which is not the same as To Do. A long unattended run produces exactly that state, and if the board flattens it back to To Do the resume point is lost and the next run starts from scratch. Done has to stay last, because the final status is the terminal one.
Parked is the only status with a way back, and the note it keeps is what makes that return cheaper than starting again from To Do.definition_of_done carries the repository’s real gate, read from its CI rather than guessed. The v1.53.0 defaults documentation says new tasks inherit it by default, with --no-dod-defaults available to opt out. I keep the gate on the task. auto_commit is off because whatever runs the agents owns every commit, and the auto-commit path has had two bugs where it staged more than it should.
Flags that destroy another session’s writes
The v1.53.0 CLI reference distinguishes setting notes from appending them. --notes, --plan and --final-summary replace the whole section. Each has an --append-notes style form that adds to it, and the bare form is the one that bites: two agents on adjacent work, one writes notes, the other sets its notes with the bare flag a minute later, and the first agent’s notes are gone. I never use a bare flag that has an append variant, and a global PreToolUse hook, backlog-guard.py, denies the bare forms in every repository on both Claude Code and Codex.
Set-valued flags have the same problem across calls. A second task edit replaces whatever the first one set for --dep, --label, --ref, --assignee and the acceptance criteria. Repeating a flag inside one call adds (--dep A --dep B gives both); a second call throws the first away. The hook can’t see across separate tool calls, so this one relies on agents using the additive forms, --add-label and --add-ref, or doing everything in one call.
Finishing a task is also one call, so an interrupt can’t leave finished work looking unfinished:
backlog task edit ABC-0007 --check-ac 1 --check-ac 2 -s Done
Never hand-edit task markdown. Sections are delimited by HTML comment markers, and a broken marker silently drops the section with exit 0. The data stays in the file until the next CLI write overwrites it, and backlog doctor only repairs duplicate IDs, so there’s nothing to recover it with. From 1.54.0 the CLI refuses to replace a legacy section whose boundaries are ambiguous instead of deleting what follows it, which stops that overwrite but not the hand edit that caused it. Two agents on one task is the same failure from a different direction: the concurrency fix in 1.50 covers the edit path only, not reordering, draft saves, the web UI or doc updates.
Upstream traps
check_active_branchesdefaults to true and breaks ordinary branch work. It decides whether a task is an ambiguous duplicate by content hash, so a task with an uncommitted edit, or a different copy on any active branch, returns a 409 whiledoctorreports the board clean. Saving a task in the web UI is enough to reproduce it. Hence--check-branches falseat init.- IDs are sequential and collide across clones and worktrees. Upstream declined a collision-free mode.
doctorrepairs duplicate task IDs only; duplicate doc and decision IDs resolve silently by title order, so retitling a doc can re-point its ID. - Archiving releases an ID for reuse, and there’s no
task delete, onlyarchive, which leaves a file to remove before committing. - Decisions only have
createandlist, with sections only the web UI can fill and no way to supersede one. I keep durable reference in docs and treat tasks as the unit of work. - 1.54.0, released on 16 October, changes the two traps above: archiving keeps the ID,
task deleteremoves an archived task for good, decisions gain a statusedit,archiveanddelete, andtask completeis gone. - The v1.53.0 README makes CLI instructions the default and says MCP “remains supported” for anyone who prefers a connector, and 1.54.0 still adds MCP operations. The maintainer’s own pull requests call it the legacy MCP adapter, kept in step with a CLI-first workflow, so I don’t build on it. My agents use the CLI.
backlog config get definition_of_doneanswers “Unknown config key” even though the setting works, because the CLI namespace is camelCase. The failinggetlooks like an ignored setting; a throwaway task shows the real Definition of Done.- A shallow
config.ymlcan enlist the board in a repository’s own build. A Home Assistant add-on repository discovers add-ons by findingconfig.ymltwo levels down, so it builtbacklogas a fifth add-on and failed CI with five missing-field errors. I run afindfor shallowconfig.*files beforeinitand use--config-location rootif anything matches.
What goes on the board
backlog/ is committed, so tasks, docs and decisions never carry account identifiers or personal data: no email addresses, handles, account IDs, device names or addresses. Write the shape, not the instance. On a public repository the sweep widens to installation, tenant and stack IDs and internal hostnames, which are unremarkable in a private repository and don’t belong on a public board.
Moving a repository off GitHub Issues imports open work as tasks and closed work as one index doc, never as Done tasks. Board IDs follow creation order, so they can never match the #123 numbers already cited in commits and code, and a pile of imported Done tasks buries what the board is for, which is what’s left to do. An empty board is a legitimate outcome of a migration.
Some repositories stay boardless on purpose. The agent tooling itself, the loop protocol, the transcript catalogue and the pi extensions, is spread across public and private repositories whose tasks usually span a public release and a lock bump somewhere else, so all of it is tracked on one board in a private repository instead of four.
How a loop uses the board
In a v3 loop the board is the queue. Before each wave the root runs loop-lint queue, which fails if a queued task is excluded by the charter, already Done or labelled needs-owner. A task the loop can’t finish is parked with one of five reasons: owner, authority, dependency, defect or evidence-later. An evidence-later park lands the work and labels the task verify:<date> for a check that can only run later. Each landing commit carries a Task: <ID> trailer, and the closeout marks every landed task Done with --check-ac and -s Done in one call and appends one note to every parked task.
What it doesn’t solve
The failure I need the guards to catch is an agent stopping partway through finishing a task: the work is done and the box isn’t ticked. Upstream’s instruction block opens with a CRITICAL_INSTRUCTION nudge on every request. That is an instruction, not a measurement of how often an agent follows it. The board is only as current as the hooks and the loop’s own closeout make it, and I expect to keep tuning those.