A local-first issue tracker that stores issues as Markdown files inside your project.
Humans and coding agents coordinate work through the files themselves.
No external service, account, or database needed.
Install Β· Quick start Β· Web UI Β· Screenshots Β· Commands Β· Agents Β· Docs
$ beaver create "Login form rejects valid passwords" --label bug --priority high
Created ix2guj Login form rejects valid passwords
.beaver/issues/ix2guj-login-form-rejects-valid-passwords.md
Most issue trackers live outside the codebase, behind a web app and an API. Beaver Backlog keeps project work in the repository, as plain files that travel with your code:
Markdown-first: every issue is a human-readable.md
file with a small YAML header. Read it, edit it, diff it, and review it like any other file.Local by default: issues live on your disk, in your project.** Version-control-friendly**: plain text that Git diffs and merges cleanly.** Agent-friendly**: coding agents read, create, and update issues through the same files as you.** Nothing hidden**: the files are the only source of truth. The CLI and the web UI are thin clients over them; hand-editing an issue file is a first-class operation.
With Go 1.26 or later:
go install github.com/builtbystef/beaver-backlog/cmd/beaver@latest
Or build from a clone:
git clone https://github.com/builtbystef/beaver-backlog.git
cd beaver-backlog
go build ./cmd/beaver
bash
$ beaver init
Initialized empty Beaver Backlog store in /home/you/project/.beaver
$ beaver create "Login form rejects valid passwords" --label bug --priority high
Created ix2guj Login form rejects valid passwords
.beaver/issues/ix2guj-login-form-rejects-valid-passwords.md
$ beaver list
ID PRIORITY STATE ASSIGNEE LABELS TITLE
ix2guj high todo - bug Login form rejects valid passwords
$ beaver start ix2guj
Started ix2guj (claimed for stefan)
$ beaver note ix2guj "root cause: form strips ! before hashing"
Added note to ix2guj as stefan
$ beaver update ix2guj --priority urgent --label regression
Updated ix2guj
$ beaver done ix2guj
Marked ix2guj done
State changes are verbs of their own (start
, done
, cancel
, reopen
);
every other field β title, description, assignee, priority, labels,
relationships β changes through beaver update
.
beaver serve
starts a local web UI on loopback over the same files. It uses
port 2328 by default and scans forward if that port is occupied (override it
with --port
). No daemon and no build step; stop it with Ctrl-C.
Boardβ issues as cards in state columns; drag a card to move it.** List**β the same issues as a table, sharing one filter bar with the board (label, priority, assignee, text search).** Graph**β the dependency graph as a server-rendered picture: layered layout, parent clusters, dependency arrows; pan, zoom, and filter it.Issue pagesβ rendered Markdown descriptions and notes, every field editable, plus creating new issues in the browser.** Doctor**β store health as a page, with the same safe repair asdoctor --fix
.
Open pages notice when the store changes underneath them β a pull, a hand
edit, another actor β and redraw themselves. Every control is a plain HTML
form first, so the UI keeps working with JavaScript disabled; scripts only
add polish on top. The UI follows your system's light or dark theme, and
writes are attributed just like CLI writes (beaver serve --as <actor>
).
| The board | The graph |
|---|---|
| The issue list | |
| An issue page | |
| Dark mode: board | |
| Dark mode: graph |
Each issue is one file in .beaver/issues/
, named <id>-<slug>.md
for readability. The short random ID is the identity; the slug just mirrors the title.
---
id: ix2guj
title: Login form rejects valid passwords
state: done
assignee: stefan
priority: high
labels:
- bug
created: 2026-07-06T20:28:40Z
updated: 2026-07-06T20:28:59Z
---
When a user submits a correct password containing a `!`, the form clears and
shows "invalid credentials". Expected: the login succeeds.
## Notes
**stefan** β 2026-07-06T20:28:59Z
root cause: form strips ! before hashing
The frontmatter is machine-owned (Beaver Backlog keeps it formatted,
and unknown keys you add by hand are preserved verbatim, never interpreted);
the issue body is yours (Beaver Backlog only ever appends notes to it). State is one of
todo
, in-progress
, done
, or cancelled
β cancelled meaning deliberately abandoned, kept visible so nobody re-files it. Any state may move to any other; the tracker records reality rather than enforcing a workflow.
Fourteen of them, and they fit on one screen:
| Command | What it does |
|---|---|
beaver init |
|
| Initialize a store in the current project | |
beaver create "<title>" |
|
Create an issue (--body , --body-file , --label , --priority , --depends-on , --parent ) |
|
beaver list |
|
List issues (--state , --ready , --blocked , --label , --priority , --assignee , --parent , --search ) |
|
beaver show <ref> |
|
| Show an issue, including what it waits on and whether it is ready | |
beaver start <ref> |
|
| Move to in-progress, auto-claiming if unowned | |
beaver done <ref> |
|
| Mark done | |
beaver cancel <ref> |
|
| Deliberately abandon (terminal, but not completed) | |
beaver reopen <ref> |
|
| Return an issue to todo, clearing its assignee | |
beaver update <ref> |
|
| Change any non-state field (see below) | |
beaver note <ref> "<text>" |
|
| Append an attributed, timestamped note | |
beaver delete <ref> |
|
| Delete the file (for junk; the VCS keeps history) | |
beaver doctor |
|
Check store health; --fix repairs what is safe to repair |
|
beaver serve |
|
Serve the local web UI on loopback until interrupted (--port , --as ) |
|
beaver whoami |
|
| Print the actor you resolve as |
update
takes as many fields as you like in one invocation, and writes nothing at all if they net out to no change:
| Flag | What it changes |
|---|---|
--title <text> |
|
| The title, renaming the file to the fresh slug (the ID is fixed) | |
--body <text> / --body-file <path> |
|
The description, leaving the ## Notes section untouched |
|
--assignee <actor> / --unassign |
|
| The assignee | |
--priority <level> |
|
Priority (urgent βlow , or none to clear) |
|
--label <spec> |
|
Labels: bug or +bug adds, -bug removes; repeatable, CSV |
|
--depends-on <spec> |
|
Blocking edges, by ref, with the same + /- syntax |
|
--parent <ref> / --no-parent |
|
| The parent issue |
A <ref>
is an issue's ID, its slug, or its file name β resolved by exact
match only, never by prefix or fuzzy match. Run beaver help
for full usage.
Output format auto-detects: human-readable tables on a terminal, JSON when
piped (override with --format human|json
). Exit codes are stable β 0
success, 1
runtime failure, 2
usage error, 3
issue or store not found.
Every mutation is attributed to an actor β a free-form name; humans and
agents are treated identically. Identity resolves from --as
, then the
BEAVER_BACKLOG_ACTOR
environment variable, then per-machine user config (a
human is prompted once, in a terminal). Set BEAVER_BACKLOG_ACTOR
in an agent's environment and every claim and note it makes is attributed correctly.
A complete issue β title, description, and metadata β is one command: pass a
short description inline with --body
, or pipe multi-line Markdown through
--body-file -
(a path works too) and skip the shell quoting:
$ beaver create "Login form rejects valid passwords" --label bug --priority high --body-file - <<'EOF'
When a user submits a correct password containing a `!`, the form clears and
shows "invalid credentials". Expected: the login succeeds.
EOF
Created t4y1gv Login form rejects valid passwords
.beaver/issues/t4y1gv-login-form-rejects-valid-passwords.md
Routine upkeep is likewise one command rather than a sequence β update
takes every field it changes at once, and reports the result in the same single-issue JSON shape the lifecycle verbs use:
$ beaver update t4y1gv --priority urgent --label regression,-needs-triage --assignee agent-7
Updated t4y1gv
Nothing here needs a terminal or an $EDITOR
. To rewrite a description,
beaver update <ref> --body "<markdown>"
replaces it and leaves the
## Notes
section byte-identical; --body-file <path>
reads it from a file,
and --body-file -
from stdin, which is the way to send multi-line Markdown without shell quoting:
$ beaver update t4y1gv --body-file - <<'EOF'
Submitting a correct password containing a `!` clears the form and shows
"invalid credentials". Expected: the login succeeds.
EOF
Updated t4y1gv
Editing the issue file directly is equally first-class β the files are the source of truth, and that is the interactive path this tool deliberately does not wrap in a command. Three rules keep a hand edit safe:
Leave the It is the append-only coordination journal; rewriting or dropping another actor's entries breaks the contract. Edit the description above it, and add your own entries only through## Notes
section alone.beaver note
.Never change theβ it is the issue's identity, and the filename merely mirrors it.id
fieldFollow up with A hand edit alone does not bump thebeaver note <ref> "<what you changed>"
.updated
timestamp; a note both journals the change for other actors and bumps it.
beaver doctor
is the net under all of it: run it after a hand edit and it
reports anything the edit left behind. Whatever drifts anyway β say a filename
gone stale after a hand-retitle β is lint, not corruption, and
doctor --fix
repairs it.
Beaver Backlog is local-first and has no sync layer, so it cannot lock an
issue. Instead an actor claims one by setting its assignee
field, and that claim travels through the VCS like any other change. It is a signal, not a rule: two actors on different branches can claim the same issue, and the merge surfaces the clash. Push claims early and integrate often.
Give each concurrent agent its own working tree (a separate git worktree
or clone). Two agents sharing one checkout can silently overwrite each other's edits β that configuration is unsupported.
Issues relate through depends_on
and parent
, stored one-sided on the
dependent or child. beaver list --ready
shows what is actionable now (todo,
every dependency done); --blocked
shows what is waiting.
beaver init
writes .beaver/config.yml
, which is committed and shared like the issues. It records the store's format version and nothing else today. Beaver Backlog only ever reads and writes files β it never runs a version-control system, so committing your issue files stays entirely yours to do (and stays free to bundle with the code the issue produced).
Your identity lives in per-machine user config, never in the repository.
Distributed, hand-editable files can drift: filenames out of sync with titles,
dangling references after a bad merge, dependency cycles, typo'd frontmatter
keys. Everyday commands degrade gracefully β an invalid file is skipped with a
warning, never a crash β and beaver doctor
reports everything it finds.
doctor --fix
repairs only what is unambiguous (like drifted filenames) and never removes data.
β the project's language: what an issue, actor, claim, and note precisely mean.docs/GLOSSARY.md
β the conventions this repository uses to track its own work with Beaver Backlog.docs/TRACKER.md
β the modules and the seams between them.docs/ARCHITECTURE.md
β the coding and test conventions used by contributors.docs/CODING_STANDARDS.md
β the architecture decisions behind the design and their tradeoffs.docs/adr/
Beaver Backlog is now at 1.0.0. It will continue to evolve, but compatibility matters: changes to the issue file format, commands, flags, machine-readable output, and other public behavior will aim to remain backward-compatible. Unavoidable breaking changes may still happen, but they will be kept to a minimum and called out clearly.
Contributions are welcome β see CONTRIBUTING.md for how to build, test, and submit changes.