Open any codebase older than a year and grep for TODO. You'll get dozens of
hits. Now answer me one question about any of them:
How long has that been there?
You can't. The comment says // TODO: validate the amount before charging and
gives you nothing — no date, no name, no sense of whether it was written last
Tuesday or by someone who left the company in 2021. So it just sits there,
quietly graduating from "I'll get to it" into permanent, load-bearing tech debt
that everyone is now too scared to touch.
The information is right there in git. git blame knows exactly who wrote that
TODO and when — but nobody runs it on their TODOs, because it's one line at a time
and the output is a wall of hashes.
So I built todoage — a zero-dependency CLI that does it for the whole tree.
npx todoage
AGE TAG LOCATION AUTHOR TEXT
418d FIXME src/auth/session.js:71 Alice Dev refresh tokens before they expire
402d TODO src/billing/charge.js:12 Alice Dev validate the amount before charging
311d HACK src/api/proxy.ts:88 Bob Maintainer works around the upstream 1.4 bug
9d TODO src/ui/cart.tsx:140 Carol wire up the empty-cart state
summary: 4 markers · 3 older than 90d
It scans for TODO / FIXME / HACK / XXX / BUG, runs git blame on every
hit, and sorts oldest-first. Rows older than your threshold come out red —
so that 402-day-old "validate the amount" in the billing code is suddenly
impossible to ignore, which is the entire point.
Why not tickgit or todo-tree?
This idea isn't new, which is why it bugged me that nothing shipped it.
tickgit (324★) promised blame +
age tracking for TODOs… and went quiet in 2020 having never delivered that part.
VS Code's todo-tree is great, but it solves visibility inside the editor —
there's no CLI you can point at a repo in CI for a JSON report or a build gate.
So todoage is deliberately the missing piece: age + author, on the command
line, CI-friendly.
It's built to gate a build
The flag I actually use day-to-day:
# fail CI if any TODO has been rotting for more than a quarter
npx todoage --max-age 90d --fail-on-stale
Exit 0 if you're clean, 1 if anything's stale. Drop that in a workflow and
your debt now has a deadline instead of a graveyard. --max-age takes plain days
or 12w / 6m / 1y.
A few more you'll reach for:
# only the FIXMEs Alice left behind
todoage --tags FIXME --author alice
# feed a dashboard or a Slack bot
todoage --json | jq '.items[] | select(.stale)'
It doesn't cry wolf
A naive grep -i todo matches todomvc, fixme_helper, and "todo" in prose.
todoage only fires on markers in an actual comment context (//, #, /* */,
<!-- -->, --, ;, leading *) and only on the case-sensitive uppercase
tag — so your identifiers stay quiet and the report stays trustworthy.
Not in a git repo? It still lists every marker; age and author just show ?.
Install
It ships on both registries — half the world's code is Node, half is Python:
npx todoage # Node — zero deps
pipx run todoage # Python — pure stdlib
Both ports are tested against the same input→output vectors, so they produce
byte-for-byte identical output. Pick whichever runtime is already on the box.
A few design notes
-
One pure core, two runtimes.
scanLine,ageDays, andisStaleare pure functions — no I/O, no clock, no git. A shared vectors table drives both the Node and Python suites, which is why the two builds can be proven identical. -
Age math never touches a
Date/datetime. It's integer milliseconds → whole days. Deterministic, timezone-free, identical across languages. -
Read-only and stateless. It blames, prints, and exits. No cache, no config
file, nothing written to your tree.
npx/pipxit on demand and forget it.
Try it / break it
It's MIT and tiny. I'd love to know the oldest TODO it surfaces in your repo —
paste me the age. And honestly: what's your current excuse for that one comment
you've been scrolling past for two years?
Top comments (0)