![]() |
Your todos herded. An interactive todo board that runs standalone in any terminal, or as a herdr plugin in a split, tab, overlay, or zoomed pane. Backed by a plain markdown file: greppable, hand-editable, syncable. |
- install
- usage
- subtasks
- boards
- global view
- launch filter
- command api
- stats
- agentic tools
- configuration
- storage
- herdr integration
- develop
No setup required — everything defaults under ~/.config/shepherd/ (or
$XDG_CONFIG_HOME/shepherd/ when that is set):
| What | Path |
|---|---|
| Default board | ~/.config/shepherd/todo.md |
| Named board | ~/.config/shepherd/boards/<name>.md — via --board <name> |
| Archive | sibling of the board: archive.md / <name>-archive.md |
| Config (optional, shared) | ~/.config/shepherd/config.toml |
Overrides: $SHEPHERD_TODO_FILE (exact board file), $SHEPHERD_CONFIG (config
file). See storage.
Requires a Go toolchain to build.
Homebrew (recommended):
brew install jwarykowski/tap/shepherdLocal dev, from a checkout:
go build -o bin/shepherd .
herdr plugin link .Published (public repo + GitHub topic herdr-plugin) — herdr runs the
[[build]] step for you:
herdr plugin install jwarykowski/shepherd| key | action |
|---|---|
j / ↓, k / ↑ |
move |
space |
toggle done (on a parent, cascades to its subtasks; the last subtask done completes the parent) |
tab |
cycle status (open → in-progress → done → open); see statuses config |
h / m / l |
set priority high / medium / low |
g |
set category |
T |
set tags (space- or comma-separated; empty clears) |
t |
set due date — today, tomorrow, 3d/2w/5m/1y, or DD-MM-YYYY (empty clears) |
s |
set defer/start date (same formats as due; item shows dimmed with starts Nd until then) |
L |
set a reference link (url) |
o |
open the selected item's link in the browser |
a |
add item (inline syntax below); inherits the selected item's category unless the text names one |
S |
add a subtask to the selected item |
u |
edit item (or subtask) text |
d |
open detail view (shows every field) |
v |
cycle view: category / priority / tag / table |
F |
hide / show the footer help grid (the jwarykowski/shepherd · version line stays); hidefooter config sets the default |
A |
toggle the global view across all boards |
b |
open the board picker — every board with done/total counts; enter jumps, a creates a board, r renames, A archives, x deletes (confirmed), d shows detail (name, dir, paths, counts) for the selected board (rename/archive/delete don't apply to the default board); e toggles the archived-boards view where u unarchives the selected board |
e |
browse the archive (read-only; all boards in the global view; esc to leave) |
/ |
filter (text/note/category/tags/due/defer/link — also greps archive.md) |
U / ctrl+r |
undo / redo (multi-level) |
w |
save now (the header shows ● unsaved / ● saved) |
ctrl+e |
open the markdown file in $EDITOR |
, |
open settings — edit view, density, autosave, categories, statuses, footer; changes save to config.toml |
x |
delete item |
c |
sweep all done items to archive.md |
C |
archive the selected item to archive.md (whole items only — dimmed on a subtask row) |
? |
full help page |
q |
save + quit |
The detail view is also an editor: the same field keys work there — u text,
h/m/l priority, g category, T tags, t due, s defer, L link, tab
status — and saving returns you to the detail view rather than the list. Plus
n edit note · space toggle · o open link · esc back · q save + quit. Its
footer is the same labelled key grid as the board's, narrowed to the keys that
act on the one item (fields · dates · item · go).
Rows carry two flush-right values: the subtask progress (1/2) when the item
has subtasks, else the due/defer label — then, pinned far right, whichever
grouping axis the headers don't already name. The category view shows the
priority there, the priority view shows the category, and the tag view shows the
priority. Status needs no width at all: the box shape carries it (○ open,
◐ a named status, ✓ done).
Inline quick-add — a, then one line:
deploy api @work #api !h due:tomorrow defer:1w link:https://…. @word sets
category, #word adds a tag (tags:a,b sets the whole set), !h/!m/!l
priority, due:<preset> the due date, defer:<preset> a start/defer date,
link:<url> a reference, status:<name> a status, and note:<text> a note
(holds spaces, takes the rest of the line — put it last); everything else is the
task text.
Items are ordered by category, then priority, then soonest due, grouped
under headers. Overdue open
items are pinned to a ⚠ overdue group at the top. New items get a created
timestamp; due items show a relative label
(due 3d, overdue 2d in red). Edits save on quit, autosave after a short
idle pause (autosave seconds, default 60; 0 disables), or on demand with
w; the header shows ● unsaved / ● saved. The board reloads on-disk changes
automatically when you have no unsaved edits, so external edits (or a dotfile
sync) show up on their own.
Any item can hold one level of subtasks — the steps that make up a task. Each subtask is a full item (its own done state, priority, due date, …); it just lives nested under its parent.
On the board, subtasks render indented beneath their parent, and the parent
shows a done/total badge:
○ ship cli edit 1/3 high
○ parse tokens medium
✓ wire into Run
○ update usage
S— add a subtask to the selected item (same quick-add syntax asa).- On a subtask row every per-item key works as it does on a parent:
spacetoggle,tabstatus,utext,h/m/lpriority,tdue,sdefer,Llink,oopen link,xdelete. Overdue/defer labels show on the row. dopens the subtask's detail view — its own fields plus aparentline naming the task it belongs to; edit its note there withn, same as a parent.g(category) is the one exception — it's parent-only, since a subtask shares its parent's board; it's dimmed in the footer on a subtask row. Set a subtask's fields at creation from the CLI too:shepherd sub <n> "text @home !h due:tomorrow defer:1w link:https://…".
Completion cascades both ways: completing a parent completes all its subtasks, and completing the last open subtask completes the parent (reopening one reopens the parent). From the CLI:
shepherd sub 2 "chop onions !m" # add subtask to item 2
shepherd done 2.1 # complete subtask 1 of item 2
shepherd done 2 # complete item 2 and all its subtasks
shepherd rm 2.1 # remove just that subtasklist --json nests them under each item's subtasks array (each with a 1-based
index within the parent). stats counts top-level items only — subtasks are
decomposition, not separate board work.
Each named board gets its own file at
~/.config/shepherd/boards/<name>.md; with no board selected you're on the
default todo.md. config.toml is shared across every board.
shepherd --board web # open the web board
SHEPHERD_BOARD=web shepherd # same, via envNames are a simple slug — letters, digits, . _ -. The archive is
per-board (boards/web.md → boards/web-archive.md); see
storage. This also works from the command API (below) and as a
herdr pane entrypoint:
[[panes]]
id = "shepherd-work"
title = "todo: work"
placement = "tab"
command = ["./bin/shepherd", "--board", "work"]See every board at once — the default plus all named boards — in one read-only
view. Launch with --all, or press A from any board to toggle in and out
(your board is saved first, and A again drops you back on it).
shepherd --all # aggregate of every boardv cycles five groupings: board → category → priority → tag → table. In the
board grouping each board is a header; in the others every row carries a
[board] tag (the table gets a board column). It's read-only by design —
editing stays on the focused board, so the aggregate is never written back.
/ filters across boards (including by board name).
Only the keys that read or navigate work here — j/k, d, v, /, e, b,
o, F, ?, A/esc to leave, q to quit. Every editing key is inert and
shown dimmed in the footer, on the board and in the detail view alike.
The command API mirrors it: shepherd list --all (see command api).
--board gives a board its own file; --filter is a saved view over one
board — start it pre-filtered by text/note/category/tags/due/defer/link:
./bin/shepherd --filter work # or: SHEPHERD_FILTER=work ./bin/shepherdWhen the filter names a category (one you've configured or already use), items
you add while it's active inherit that category — so a task added on a
--filter work board lands in work and stays in view. This is the fallback:
the selected item's own category is inherited first, and an inline @category
overrides both. A filter that isn't a category leaves new items uncategorized.
The two combine: shepherd --board web --filter '!h'.
shepherd --version prints the version and exits.
For scripts and agentic tools that can't drive the TUI. A leading non-flag
argument switches shepherd from the board to a one-shot command that reads or
mutates a board file and exits — the binary owns the file format, so writes are
always valid. Items carry a stable id (in list --json); mutating verbs take
either that id or the 1-based list index. Agents should use the id — the index
shifts as the board reorders, the id never does. Mutations serialise under a
board lock, so parallel processes never lose one another's writes.
shepherd list [--json] # show items with their index
shepherd list --all [--json] # aggregate across every board (read-only)
shepherd list --filter home # only items matching the query, real indexes kept
shepherd watch # stream board changes as NDJSON until killed
shepherd watch --interval 500ms # poll faster (default 1s)
shepherd boards [--json] # list boards with done/total counts (* = current)
shepherd boards --archived # list archived boards instead
shepherd board rename web webapp # rename a board (and its archive sibling)
shepherd board archive webapp # stash a board under boards/archived/ (reversible)
shepherd board unarchive webapp # restore an archived board
shepherd board delete webapp --force # delete a board (and its archive sibling)
shepherd board delete webapp --dry-run # preview the delete without writing
shepherd stats [--json] [--all] # board metrics as charts (--json = numbers)
shepherd stats --legend # explain every chart and the aging numbers
shepherd stats --no-color # charts without ANSI color
shepherd add "buy milk @home !h due:tomorrow"
shepherd add "buy milk" -q # add quietly (no confirmation line)
shepherd add "buy milk" --json # echo the new item (incl. its id) as JSON
shepherd sub 2 "chop onions !m" # add a subtask to item 2 (id or index)
shepherd edit 2 "@work !h due:friday" # merge tokens onto item 2 (2.1 edits a subtask)
shepherd done 2 # mark item 2 done (cascades to its subtasks)
shepherd done 2 5 7 # mark several done in one atomic write
shepherd done 2f3a…c1 --json # mark by id; echo the result as JSON
shepherd undone 2.1 # reopen subtask 1 (also reopens the parent)
shepherd edit 2 "status:in-progress" # set item 2's status (status:done|open recognised)
shepherd edit 2 "note:waiting on infra" # set item 2's note (edit 2 "note:" clears it)
shepherd edit 2 "#api #docs" # add tags (tags:a,b replaces the set, tags: clears)
shepherd rm 2 # remove item 2 (rm 2.1 removes just the subtask)
shepherd rm 2 5 --dry-run # preview removing several without writing
shepherd archive 2 # move item 2 off the board into archive.md (whole items only)Global flags (any command):
-q,--quiet— silence a mutation's confirmation line (list/statsdata is never suppressed).--no-input— accepted for script-compat; this API never prompts.-h,--help— print a command's flags, exit 0.
Exit codes: 0 success · 2 usage/input error (bad flag, unknown command,
unknown ref) · 1 runtime/IO failure. A mistyped command suggests the
closest real one.
done/undone/rm take one or more refs (id or index) and apply them as a
single atomic write; they're safe to repeat, since re-marking a done item keeps
its original completion stamp. A subtask is addressed by its own id or a dotted
n.m reference (subtask m of item n); see subtasks for the
cascade rules. --json on any mutating verb echoes the resulting item(s) in the
list --json shape and reports failures as {"error":…} on stdout.
edit <n[.m]> "<tokens>" sets only the fields its tokens carry — @category,
#tag/tags:, !h/!m/!l, due:, defer:, link:, status:, note: — and replaces the
text only when plain words are present. A bare key clears its field
(edit 2 "@ due:"); note: holds spaces and takes the rest of the line, so put
it last (edit 2 "!h note:call the bank").
list --filter <q> matches text/note/category/tags/due/defer/link and keeps each
item's real board index, so done/rm on a filtered listing still hit the
right item.
watch streams a board's changes as NDJSON until killed — a coordinating agent
reacts instead of polling. The first line is a {"type":"snapshot","items":[…]}
baseline, then one line per change keyed by the item's stable id:
{"type":"added|updated|removed","item":{…}} (item shape = list --json).
Change detection is mtime polling (--interval, default 1s); it's read-only
and never blocks a writer.
Flags go after the verb. Add --board <name> (or set $SHEPHERD_BOARD)
to target a named board instead of the default:
shepherd add "ship v2 @work !h" --board web
shepherd list --board webadd accepts the same quick-add tokens as the board: @category, #tag
(tags:a,b replaces the set), !h/!m/!l priority,
due:<today|tomorrow|+3d|15-07-2026>, defer:, link:, status:, and note:
(takes the rest of the line). Agents should read with
list --json (stable machine shape) and mutate with add/edit/done/rm;
an open board picks up the change within ~2s. edit is the single setter for
every field, including status: (any name, like a free-form @category;
status:done/status:open are the terminal/default ends, with done/undone
as shorthands) — the status field appears in list --json.
list --all --json adds a board field per item so you can tell which board
each came from.
[
{ "id": "019f7390…d901", "index": 1, "done": false, "priority": "H",
"text": "buy milk", "category": "home", "tags": ["errand"],
"created": "10-07-2026 13:40",
"defer": "2026-07-11", "due": "2026-07-15", "link": "https://…",
"note": "", "completed": "" }
]shepherd stats summarises a board as terminal charts — completion, due and
urgency, priority load, aging, status mix, throughput and backlog trend (drawn
with ntcharts).
Done-based counts include the archive, so throughput survives a sweep. Only top-level items are counted — subtasks don't appear in any chart.
| Flag | What |
|---|---|
--all |
aggregate every board, and add a by-board breakdown |
--json |
the raw numbers instead of charts, for scripts |
--legend |
explain every chart and the aging numbers |
--no-color |
charts without ANSI colour |
Colour drops itself when stdout isn't a terminal, $NO_COLOR is set, or
$TERM=dumb; --no-color forces it off.
The backlog health line under the charts is the part worth watching: oldest open, avg open, stale >30d (open items created over 30 days ago) and time to done (mean create→complete). A rising net +N/mo on the backlog chart means
the board is growing faster than it's being cleared.
The command API is the whole integration — any agent that can run a shell drives shepherd with the same verbs. Discovery is layered:
- Any tool:
shepherd helpprints the contract. That's the universal fallback; nothing else is required. - Claude Code: symlink the bundled skill in once —
Available in every project; Claude invokes it when a request relates to your todos (the skill's description is what it matches on).
ln -s "$PWD/skills/shepherd" ~/.claude/skills/shepherd
- Cursor / Codex / Zed / others: paste
AGENTS.mdinto the tool's global rules / instructions slot.
All three point at the same shepherd CLI — no per-tool server, no MCP.
Optional config.toml at $XDG_CONFIG_HOME/shepherd/config.toml (defaults to
~/.config/shepherd/config.toml) — shared across every board (override with
SHEPHERD_CONFIG):
view = "category" # category (default) | priority | tag | table
density = "compact" # compact (default) | comfort
autosave = 60 # seconds idle before writing; 0 disables
categories = ["work", "home", "personal"] # tab-cycles in the category prompt
statuses = ["open", "in-progress", "done"] # tab cycles item status in the list
hidefooter = false # true starts with the footer help grid hidden (F toggles at runtime)view— default grouping/layout on launch (vstill cycles at runtime).density—comfortadds outer padding and blank lines between rows.autosave— idle seconds before an unsaved board is written to disk (default 60);0disables it, so onlywand quit save.categories— presstabin the category prompt (g) to cycle through them.statuses— ordered listtabcycles through in the list;doneis always kept and forced last. Defaults to["open", "done"]. Intermediate statuses persist as astatus:line and show a◐glyph;shepherd statsbreaks items down by status in this order.hidefooter— start with the footer help grid hidden for a cleaner board (thejwarykowski/shepherd· version line stays);Ftoggles it at runtime.
Edit these in the running board with , (settings): enter cycles view/density/footer and edits autosave/categories/statuses, and each change is written straight back to config.toml. shepherd owns the file — a settings save rewrites the managed keys.
herdr pane placement (placement / direction) lives in the same file — see
herdr integration.
Layout is the table at the top: the default todo.md, a shared config.toml,
and one boards/<name>.md per named board (selected with
--board/$SHEPHERD_BOARD). Override the exact board file with
$SHEPHERD_TODO_FILE.
Dates are stored ISO (YYYY-MM-DD) so they sort correctly, but shown and
entered day-month-year / DMY (DD-MM-YYYY). Metadata rides as indented
sub-lines:
- [ ] (H) ship the release
id: 019f7390ff8b0e369b6ed5540057c717
created: 10-07-2026 13:40
defer: 2026-07-11
due: 2026-07-15
category: work
tags: api, docs
status: in-progress
link: https://github.com/org/repo/pull/1
note: block on the migration firstid is a stable, opaque handle minted when an item is first written (a legacy
board without ids gets them the next time shepherd saves it); it's how scripts
and agents address an item across reorderings. completed (a timestamp) is
added automatically when an item is marked done and cleared if it's reopened.
Subtasks nest as further-indented checklist lines — two spaces for
the - [ ], four for their own metadata — written after the parent's metadata:
- [ ] (H) ship cli edit
created: 10-07-2026 13:40
category: shepherd
- [ ] (M) parse tokens
- [x] wire into Run
due: 2026-07-15status is the named intermediate status (tab cycles it — see the statuses
config). It rides as a sub-line only on non-done items with a status past the
first; a plain open item (- [ ]) and a done item (- [x]) carry no status:
line, so existing files stay unchanged.
Pressing c sweeps every done item off the board and appends it to a
sibling archive file (todo.md → archive.md, boards/web.md →
boards/web-archive.md, created on first use). C archives just the
selected item instead, whatever its status — whole items only, so it's a no-op
(and dimmed in the legend) on a subtask row, matching the shepherd archive <ref> CLI. Same markdown format as todo.md, so it's greppable and
hand-editable. It's append-only — shepherd never rewrites or prunes it; trim it
yourself if it grows. The board doesn't display the archive, but / filtering
also greps it and shows matches in a separate section, so archived items stay
findable.
The board opens as a split, tab, overlay, or zoomed pane. Five actions:
| Action | Placement |
|---|---|
open |
from config (split if unset) |
open-split |
split |
open-tab |
tab |
open-overlay |
overlay |
open-zoomed |
zoomed |
open resolves placement from (highest first): env
SHEPHERD_PLACEMENT/SHEPHERD_DIRECTION → config.toml → split/right.
direction (right/left/up/down) applies to split only.
Set the config.toml defaults (in the shared
~/.config/shepherd/config.toml):
placement = "split" # split (default) | tab | overlay | zoomed
direction = "right" # split only: right (default) | left | up | downopen-split / open-tab / open-overlay / open-zoomed force one placement
regardless of config; SHEPHERD_PLACEMENT / SHEPHERD_DIRECTION override too.
[[keys.command]]
key = "prefix+shift+o"
type = "plugin_action"
command = "jwarykowski.herdr-shepherd.open" # placement from config
description = "shepherd"
[[keys.command]]
key = "prefix+shift+t"
type = "plugin_action"
command = "jwarykowski.herdr-shepherd.open-tab"
description = "shepherd (tab)"
[[keys.command]]
key = "prefix+shift+y"
type = "plugin_action"
command = "jwarykowski.herdr-shepherd.open-overlay"
description = "shepherd (overlay)"
[[keys.command]]
key = "prefix+shift+z"
type = "plugin_action"
command = "jwarykowski.herdr-shepherd.open-zoomed"
description = "shepherd (zoomed)"See CONTRIBUTING.md.


