Work Tracker is a small, agent-friendly status ledger for parallel and long-running work. A Rust CLI and read-only HTML dashboard share one SQLite database, so a human or agent can recover the current state and the context that led there.
- Transaction-safe updates from concurrent agents using SQLite WAL mode.
- Stable human-readable commands and machine-readable
--jsonoutput. - Immutable creation, note, update, status-transition, and deletion history.
- A default
listthat shows only actionable work, with--allfor the full ledger. - Automatic Work Directory capture, explicit overrides, and exact directory filters.
- A Daily View containing everything updated today and every actionable item.
- A Todo View for date-based commitments, with carryover, deadlines, and explicit priorities.
- Soft deletion with a 60-day readable retention window and automatic purge.
- A localhost-only, read-only dashboard suitable for SSH tunneling.
- Verified manual SQLite snapshots, optional daily backups, and recovery to a saved snapshot.
Rust and just are required.
just build
just test
just installjust install installs work-tracker through Cargo for the current user. Other workflows:
just # list workflows
just check # format check, Clippy, and tests
just release # optimized binary in target/release/
just uninstallThe database is created automatically at $XDG_DATA_HOME/work-tracker/work-tracker.db, or at ~/.local/share/work-tracker/work-tracker.db when XDG_DATA_HOME is unset.
work-tracker add "Watch company CI" \
--description "SQL and medium suites are queued" \
--status waiting \
--actor codex-ci \
--note "submitted at PR head abc123"
work-tracker list
work-tracker today
work-tracker show 1
work-tracker note 1 "Runner assigned; results expected in 30 minutes" --actor codex-ci
work-tracker status 1 active --actor codex-ci --note "analyzing failures"
work-tracker history 1
work-tracker status 1 done --actor codex-ci --note "all required checks passed"Use WORK_TRACKER_ACTOR to avoid repeating --actor:
export WORK_TRACKER_ACTOR=codex-ci
work-tracker status 1 waiting --note "queued behind 12 builds"If neither is set, the CLI uses the current USER, then unknown as a last resort.
| Command | Purpose |
|---|---|
add |
Create a Work Item and capture its Work Directory; initially pending unless selected otherwise |
show ID |
Show one item, including a soft-deleted item |
list |
List actionable items; --all adds done and cancelled, --status selects one status |
today |
Show items updated today plus all actionable items |
todo [today] / todo --days N |
Show commitments through N calendar days, including today |
update ID |
Change title, description, planned/due dates, priority, or Work Directory |
status ID STATUS |
Apply an idempotent status transition |
note ID MESSAGE |
Preserve context without changing status |
delete ID |
Soft-delete an item for 60 days |
history ID |
Show the complete immutable history; --limit N keeps the newest N |
search TERM... |
Find items whose title, description, or notes contain every term |
brief |
Print a bounded resume brief for one Work Directory |
hook session-start |
Emit the brief as a Claude Code SessionStart hook; always exits 0 |
path |
Show the SQLite database path |
backup |
Save a complete, verified SQLite snapshot |
backups |
List and verify snapshots, newest first |
restore SNAPSHOT --to PATH |
Recover a snapshot into a new database |
serve |
Host the read-only dashboard |
doctor |
Check the database, schema, integrity, backups, and actor without writing |
The statuses are pending, active, waiting, blocked, done, cancelled, and deleted. The first four are actionable: they are what list shows by default, and they remain in the Daily View even when they were not updated today.
Run work-tracker COMMAND --help for all options.
list answers "what is still open" and today answers "what happened today":
work-tracker list # actionable items only
work-tracker list --all # every item, including done and cancelled
work-tracker list --status done # exactly one status
work-tracker list --details # show-style details for each selected item
work-tracker today # actionable items plus anything updated todayBoth views order work by attention first: blocked, active, waiting, pending, then finished work, most recently updated first within each group. --all and --status cannot be combined. The human-readable list output ends with a footer that names --all whenever finished items are hidden; --json output is always a plain array. Deleted items stay hidden from every list unless you pass --include-deleted or --status deleted.
list --details prints each selected Work Item using the same formatter as
show ID, with a blank line between items. Existing filters, ordering, limits,
and the actionable footer still apply. --details --json produces the same JSON
as --json alone; plain list keeps its compact table.
show and list --details display every column in SQLite's work_items table,
including deleted_at and unset fields (-; an unset Work Directory is
unknown). Detailed timestamps preserve fractional seconds and show the local
UTC offset. History Entries remain available through history ID.
Before show, list --details, or list --json prints anything, a runtime check
compares the live SQLite columns with the detail fields. Each returned item's
serialized fields must also match. Missing or unknown fields cause exit status
1 with a diagnostic on standard error, rather than silently returning incomplete
details. The schema check runs even when the list is empty. JSON field names,
null values, and the list array shape are preserved.
add automatically records the directory where the command runs. An agent
running the command elsewhere can supply its project or worktree explicitly:
work-tracker add "Investigate failure"
work-tracker add "Watch CI" --workdir /path/to/project-worktree
work-tracker list --here --json
work-tracker list --workdir /path/to/project-worktree --json
work-tracker update 1 --workdir /path/to/another-worktree --note "Work moved"
work-tracker list --without-workdir
work-tracker update 1 --clear-workdirAssignments require an existing directory. Paths are stored as absolute physical paths: relative arguments resolve from the command's current directory and symlink aliases share an association. Significant spaces are preserved; paths must be valid UTF-8. The CLI records its actual working directory, including a subdirectory, without discovering a Git root.
Plain list stays global. --here, --workdir PATH, and --without-workdir are
mutually exclusive directory selectors; combine any one with existing status,
deletion, and limit options. Matching is exact, so a parent directory does not
include its descendants. Directory selection happens before the limit. Daily
and Todo Views retain their global selection rules.
Existing items start with an unknown directory and remain visible globally.
Assign them with update --workdir, or clear an association with
--clear-workdir. Effective changes record the old and new value in history;
repeating the same assignment or clear adds no history. Creation records its
directory in the creation entry. Deleted items remain immutable.
Removing a worktree preserves its saved association. list --workdir accepts
the saved absolute path even after removal; for missing paths, it resolves an
existing ancestor and normalizes the remaining components. Use the saved
physical path when a symlink alias has also been removed. Directory reads never
move or rewrite an association.
show and the dashboard display Work Directory. Work Item JSON adds the stable
workdir field, a string or null, across commands and views; compact human
list/Today/Todo columns retain their existing layout.
The directory migration upgrades SQLite's schema version to 3 while preserving existing fields and history. Version-2 binaries reject an upgraded ledger; all clients of that ledger need the new binary. Older field-specific writes that permit access preserve the additional column.
Human-readable Todo, list, today, and item output uses color automatically when stdout is a terminal. Headers are bold; active/done statuses are green, waiting is yellow, blocked and overdue dates/labels are red, high Priority is bold magenta, and carryover labels are yellow. Timestamps and secondary text are dim. Labels remain readable without color.
Pipes and redirected output stay plain. A nonempty NO_COLOR or TERM=dumb
disables automatic color. Use the global --color auto|always|never option to
override detection; an explicit always overrides those environment settings.
JSON output is always uncolored, including when --color always is given.
work-tracker todo --color never
work-tracker list --color always | less -RThe companion Todo shortcuts also accept --color once their separately
managed wrapper is updated.
work-tracker add "Prepare benchmark presentation" \
--planned 2026-10-06 --due 2026-10-07 --priority high
work-tracker todo today
work-tracker todo --days 3
work-tracker todo --days 5 --json
work-tracker todo today --all
work-tracker update 1 --planned 2026-10-08 --note "Work rescheduled; deadline unchanged"
work-tracker update 1 --clear-planned --clear-due --priority normaltodo alone means today. An N-day horizon includes today and the following N−1 calendar days, including weekends and holidays, using Asia/Seoul (KST, UTC+09:00), independent of the server timezone. Dates use YYYY-MM-DD with years 0001–9999. Days must be positive and the end date representable. today and --days cannot be combined.
A pending, active, waiting, or blocked item appears when either its Planned Date or Due Date is on or before the window's end. Unfinished past plans carry forward; missed deadlines remain overdue. Each item appears once. Items without either date stay in the ordinary backlog even when high priority. --all also includes done and cancelled items when either date falls within the window, in a separate Done / cancelled section. Finished items do not carry forward or receive overdue markers. Deleted items remain excluded.
Actions (pending/active) and Blocked / waiting are separate sections. Within each section, sort by high/normal/low Priority, earliest Due Date, earliest Planned Date, then ID; missing dates sort last. Due today is not overdue. A Planned Date after a Due Date is allowed and does not hide the missed deadline. Reading never reschedules work.
Dates are optional and independent; normal is the default Priority. Omitted update flags preserve existing values; --clear-planned and --clear-due remove dates explicitly. Effective changes include field-level history atomically, and repeated identical updates do not add history. show prints scheduling fields. Existing list and today semantics stay unchanged.
Todo reads the local ledger without contacting GitHub or another service. UPDATED means ledger activity, not a live external check. Empty views succeed with an explanatory message. --json returns an object with window (start, end, days, timezone), actions, blocked_waiting, and finished (empty without --all). Each row contains the Work Item fields plus overdue and carried_over; scheduling fields are planned_date, due_date, and priority. Existing Work Item JSON fields and list/today array shapes are preserved with additive scheduling fields.
The companion chezmoi shortcuts todo-today, todo-3days, and todo-5days are symlinks to a work-todo wrapper, dispatching by invocation name. The wrapper includes --all by default, including for direct work-todo invocation. Shortcuts forward --json, --database, and --color, preserve command exit status, and reject horizon overrides. They are installed separately from this Rust binary; the updated wrapper requires a binary supporting todo --all.
Opening an existing ledger adds nullable dates and normal Priority without changing its items, statuses, or history. Migration checks actual columns under a transaction because legacy binaries reset SQLite's version marker. Old field-specific writes preserve the additional columns. Newer unknown schema versions are rejected. Keep a SQLite-consistent backup before upgrading a shared live ledger; ordinary retention housekeeping still applies on open.
Save a snapshot manually and list the available recovery points:
work-tracker backup
work-tracker backups
work-tracker backups --jsonBy default, snapshots go in backups/ beside the selected database. For the
default ledger, this is ~/.local/share/work-tracker/backups/. Each file has a
UTC timestamp and a unique suffix. Use --directory PATH or
WORK_TRACKER_BACKUP_DIR to choose another directory for backup and backups.
Use separate directories for separate ledgers.
Each snapshot contains every Work Item, Status, field, and History Entry present at one consistent point during the backup. This includes done, cancelled, and deleted work still stored in SQLite. Backup captures committed WAL data while other agents continue working. It does not migrate the source database, purge expired deletions, or add History Entries. Missing source databases fail instead of creating an empty ledger.
The command verifies SQLite integrity, foreign keys, and the Work Tracker schema
before publishing a completed snapshot. Incomplete files use .partial and are
excluded from backups. Concurrent backups produce separate files. On Unix,
new backup directories have mode 0700, and snapshots have mode 0600.
Existing directories keep their permissions. Snapshots are never automatically
deleted, so monitor free disk space. If a backup fails, earlier snapshots remain
available. backups reports a corrupt recognized snapshot as an error with its
path rather than silently listing it as usable.
Install the new binary with just install. The supplied systemd user units use
~/.cargo/bin/work-tracker, the default database, and the default backup directory.
If you use WORK_TRACKER_DB, XDG_DATA_HOME, a custom Cargo installation, or a
different backup directory, edit ExecStart in the copied services before starting
them. The services do not inherit your shell environment. Quote paths containing
spaces using systemd unit syntax.
From the repository directory:
install -Dm644 systemd/work-tracker-backup.service ~/.config/systemd/user/work-tracker-backup.service
install -Dm644 systemd/work-tracker-backup-failure.service ~/.config/systemd/user/work-tracker-backup-failure.service
install -Dm644 systemd/work-tracker-backup.timer ~/.config/systemd/user/work-tracker-backup.timer
# Edit both copied .service files here if needed.
systemctl --user daemon-reload
systemctl --user start work-tracker-backup.service
systemctl --user enable --now work-tracker-backup.timer
systemctl --user list-timers work-tracker-backup.timerThe timer saves one snapshot at 03:00 in the server's local timezone each day.
Persistent=true triggers a missed run when the user manager starts again.
If the backup service fails, OnFailure= starts
work-tracker-backup-failure.service, which adds a blocked, high-priority Work
Item titled "Work Tracker backup failed". It appears in today, list, todo,
and the dashboard. Each failed run adds its own Work Item.
For backups while logged out, enable lingering for your user if the server's
policy permits it:
loginctl enable-linger "$USER"Check the latest result and any failures:
systemctl --user status work-tracker-backup.service
journalctl --user -u work-tracker-backup.service -n 30 --no-pager
work-tracker backupsTo stop future daily backups while keeping snapshots:
systemctl --user disable --now work-tracker-backup.timerChoose a file from work-tracker backups. Recover it into a new path:
work-tracker restore /path/to/snapshot.db --to ~/.local/share/work-tracker/recovered.db
work-tracker --database ~/.local/share/work-tracker/recovered.db list --all --include-deleted --limit 100000
work-tracker --database ~/.local/share/work-tracker/recovered.db history 1restore checks the snapshot and refuses an existing destination, even with
--database set. It also refuses SQLite sidecars at the destination. The recovered
database initially matches the snapshot, including IDs and timestamps. Ordinary
commands apply current migrations and the usual 60-day deletion retention, so
expired Deleted Work Items can disappear on the first normal open. Done and
cancelled Work Items have no expiry.
To resume work, stop the dashboard and pause agents that use the old ledger. Set the recovered path for every client, then restart them:
export WORK_TRACKER_DB="$HOME/.local/share/work-tracker/recovered.db"
work-tracker path
work-tracker todayPersist that setting in your shell or service configuration. Update the explicit
--database path in the daily backup service and its failure service, and reload
systemd as well. Changing one shell does not redirect existing agents or a running
dashboard. Keep the previous live database until you have checked the recovered
ledger.
Recovery returns the whole ledger to the selected snapshot's statuses and notes. Later changes stay in the previous database and are absent from the recovered copy. This does not rewind Git repositories, GitHub, CI, or files mentioned by a Work Item. Daily backups give roughly daily recovery points while successful backups run each day. Take a manual snapshot before an important change for a closer recovery point.
Local snapshots protect against accidental ledger changes or loss of the live
database. Copy completed .db snapshots to a separate machine or disk to
protect against losing this server. Preserve older copies there as well. For
example, from another machine:
rsync -av your-linux-server:.local/share/work-tracker/backups/ ./work-tracker-backups/ --include='*.db' --exclude='*'Only copy completed snapshots. A raw copy of the live database can miss
committed changes in its WAL. Test recovery from an external copy with restore
and show before relying on that destination.
To also save each daily snapshot on a physically separate disk, add a second
ExecStart= line after the first in work-tracker-backup.service. systemd runs
the lines in order and fails the service if either fails:
ExecStart=%h/.cargo/bin/work-tracker --database %h/.local/share/work-tracker/work-tracker.db backup --directory /mnt/other-disk/work-tracker-backupsEvery command accepts global options before or after the subcommand:
work-tracker --json today
work-tracker show 1 --json
work-tracker --database /srv/work-tracker/team.db list --jsonWORK_TRACKER_DB selects the shared database without repeating --database. Successful commands exit with status 0. Command validation uses exit status 2; runtime and lookup errors use exit status 1 and emit {"error":"..."} to standard error when --json is enabled.
To keep agent context small, read an index first and fetch details only for the items you need:
work-tracker today --compact --json # index: no descriptions
work-tracker list --here --compact --json
work-tracker show 42 --json # full item
work-tracker history 42 --limit 10 # newest 10 entries, oldest first--compact keeps id, status, title, workdir, priority,
planned_date, due_date, and updated_at, and is still a plain array. It
affects JSON only and cannot be combined with list --details. history --limit N keeps the newest N entries in chronological order; the
human-readable form starts with a line counting the omitted entries.
work-tracker search timeout medium # every term must match
work-tracker search 트래커 --json
work-tracker search CBRD-27781 --include-deleted --limit 50Each term must appear, case-insensitively, in the item's title, description,
or any note. Matching is by substring, so Korean words match with attached
particles. Done and cancelled items are included; deleted items need
--include-deleted. Results use the attention order of list and default to
20. --json returns the --compact fields plus matched_in (title,
description, or note) and a one-line snippet around the first term.
brief prints the Actionable Work Items of one exact Work Directory (the
current directory by default) with each item's latest note, within a
character budget:
work-tracker brief # current directory, 9000 characters
work-tracker brief --workdir PATH --max-chars 4000
work-tracker brief --json # {"context": "...", "shown": N, "omitted": N}Items keep the attention order of list. An item that does not fit with its
note is shown without it; when items stop fitting, a final line counts the
rest. Descriptions are never included.
hook session-start runs brief for a Claude Code SessionStart hook. It
reads the hook's JSON from standard input, briefs the hook's cwd, and prints
hookSpecificOutput.additionalContext. It returns an empty context for
resumed sessions, for a missing database, and on any error, and it always
exits 0, so a tracker problem never blocks the agent. Errors go to standard
error as one line. Register it in ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [{ "type": "command", "command": "work-tracker hook session-start", "timeout": 10 }]
}
]
}
}The default budget stays below the 10,000 characters at which Claude Code replaces hook output with a file reference.
For multiple agents, point every process at the same database file on the same Linux host. SQLite serializes writes, waits up to five seconds for a busy writer, and keeps reads responsive through WAL mode. Do not put the database on a filesystem that does not support SQLite locking semantics.
work-tracker doctor
work-tracker doctor --json # {"ok": bool, "checks": [{name, status, detail, required}]}doctor opens the database read-only, so it never migrates or creates it.
Required checks are database (exists and opens), schema (not newer than
this binary), and integrity (PRAGMA quick_check); any failure exits with
status 1. journal (WAL mode), backup, and actor only warn. The backup
check warns when there is no snapshot or the newest is older than 48 hours,
twice the daily timer interval, which catches a timer that never runs.
--directory or WORK_TRACKER_BACKUP_DIR selects the snapshot directory as
for backups.
delete changes the status to deleted, records the actor and reason, and sets purge_after to exactly 60 days after deletion. Deleted items remain available through show, history, list --include-deleted, or list --status deleted. They cannot be changed. Opening the tracker lazily purges items whose retention window has expired, including their history.
Start the read-only server on the Linux host:
work-tracker serve
# or: just serveIt binds to 127.0.0.1:8787 by default. From your PC, create an SSH tunnel and open http://127.0.0.1:8787:
ssh -L 8787:127.0.0.1:8787 your-linux-serverA different socket can be selected explicitly with work-tracker serve --bind 127.0.0.1:9000. Binding to a non-loopback address exposes an unauthenticated dashboard and should only be done behind appropriate network controls.
The dashboard answers only requests whose Host names localhost or a
loopback IP, on any port, so a tunnel to another local port still works. Other
hostnames get 403. This blocks DNS rebinding, where a web page you visit
re-points its own hostname at 127.0.0.1 and reads the dashboard through your
tunnel. To open it under another name, pass --allow-host NAME (repeatable).
Every response also forbids framing (X-Frame-Options: DENY,
frame-ancestors 'none').
The track-work skill is maintained in the my-skills collection (skills/track-work/). Install it for Claude Code and Codex with:
npx --yes skills@1.7.0 add vimkim/my-skills --global --skill track-work --agent claude-code codex --yesThe skill teaches agents to check the tracker with doctor, search for and create a Work Item before long-running work, record meaningful notes and transitions, resume from brief, the compact Daily View, and history --limit, and preserve the final outcome.
See AGENTS.md for architecture and invariants, and docs/adr/ for recorded decisions. CLAUDE.md is a symlink to the same guidance so both agent environments receive one canonical instruction file.