Scan source code for deadline-tagged fuses and fail when they detonate.
The problem it solves: // TODO: remove this after the migration gets written with good intentions and stays forever. timebomb makes the deadline explicit and machine-enforceable. When the date passes, the build fails — forcing a fix or a conscious decision to extend the deadline.
// TODO[2026-06-01]: remove this feature flag once the experiment ends
# FIXME[2026-03-15][alice]: workaround for upstream bug, revert after upgrade
-- HACK[2025-12-31]: temporary shim, drop this column after migration
Syntax: TAG[YYYY-MM-DD]: message or TAG[YYYY-MM-DD][owner]: message
The tag must be immediately followed by [date] with no space. Plain // TODO: fix this comments (no bracket-date) are ignored entirely, so you can adopt timebomb incrementally without touching existing annotations.
The scanner is language-agnostic — it matches the pattern anywhere on a line regardless of comment syntax (//, #, --, ;;, %, *, anything). No language-specific parsers.
TODO, FIXME, HACK, TEMP, REMOVEME, DEBT, STOPSHIP, WORKAROUND, DEPRECATED, BUG
Tags are matched case-insensitively. The full set is configurable via .timebomb.toml.
Each fuse is classified relative to the current date, which is derived once at startup and threaded through the entire scan (so long runs across midnight are consistent):
| Status | Condition |
|---|---|
detonated |
Date is in the past |
ticking |
Date is within the fuse_days warning window |
inert |
Date is beyond the warning window |
Download the latest release binary for your platform from GitHub Releases:
# Linux x86_64
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-linux-x86_64 \
-o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb
# macOS Apple Silicon
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-macos-aarch64 \
-o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb
# macOS Intel
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-macos-x86_64 \
-o /usr/local/bin/timebomb && chmod +x /usr/local/bin/timebomb
# Windows x86_64 (PowerShell)
Invoke-WebRequest https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-windows-x86_64.exe `
-OutFile timebomb.execargo install timebomb-cli --lockeddocker run --rm -v "$PWD:/work" -w /work pwbsladek/timebomb:latest sweep .The image is built from Docker Hardened Images: a DHI Rust builder and a distroless DHI static runtime.
git clone https://github.com/pbsladek/timebomb
cd timebomb
cargo install --path . --lockedmake docker-build
make docker-smoke
make docker-pushDefaults push pwbsladek/timebomb:<Cargo.toml version> and pwbsladek/timebomb:latest. Override with IMAGE, IMAGE_TAG, or PLATFORMS:
make docker-push IMAGE=pwbsladek/timebomb IMAGE_TAG=0.7.0 PLATFORMS=linux/amd64,linux/arm64GitHub Actions also pushes the Docker image on release when release-please creates a release tag, using DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.
timebomb sweep # scan current directory
timebomb sweep ./src # scan a specific path
timebomb sweep --fuse 30d # also flag fuses ticking within 30 days
timebomb sweep --fuse 30d --fail-on-ticking # exit 1 on ticking fuses too
timebomb sweep --since HEAD # only check fuses on lines changed since HEAD
timebomb sweep --blame # enrich unowned fuses via git blame
timebomb sweep --format json # machine-readable output
timebomb sweep --format github # GitHub Actions workflow commands
timebomb sweep --tag FIXME # only sweep fuses with this tag
timebomb sweep --owner alice # only sweep fuses owned by alice
timebomb sweep --message oauth # only sweep fuses whose message mentions oauth
timebomb sweep --no-inert # hide inert fuses from output
timebomb sweep --quiet # suppress all output (exit code only)
timebomb sweep --summary # print only the summary line
timebomb sweep --agent-summary # compact deterministic summary for AI agents
timebomb sweep --fix-plan json # machine-readable remediation plan
timebomb sweep --output report.json # also write a JSON report to a file
timebomb sweep --max-detonated 0 # override ratchet ceiling for this run
timebomb sweep --max-ticking 5sweep is the only command that exits non-zero. All other commands are informational and always exit 0.
timebomb armory # top detonated and ticking fuses
timebomb armory --oldest # show only the single most urgent fuse
timebomb armory --count # print only the active fuse count
timebomb armory --json # machine-readable prioritized list
timebomb armory --limit 5 # show the five most volatile fuses
timebomb armory --owner alice # only Alice's active fuses
timebomb armory --tag FIXME # only active FIXME fuses
timebomb armory --message oauth # only active fuses whose message mentions oauth
timebomb armory --fuse 14d # include fuses ticking within 14 daysarmory ranks detonated fuses first, with the most overdue at the top, then ticking fuses by soonest deadline. It always exits 0.
timebomb explain src/auth/login.rs:42
timebomb explain src/auth/login.rs:42 --path .
timebomb explain src/auth/login.rs:42 --blameexplain scans the project, finds the fuse at FILE:LINE, and prints the annotation with a short remediation menu. It exits 0 when a fuse is found and exits 2 if the target line has no fuse.
timebomb manifest # all fuses, sorted by date ascending
timebomb manifest --detonated # only detonated
timebomb manifest --ticking 14d # only ticking within 14 days
timebomb manifest --format json
timebomb manifest --format csv # CSV output for spreadsheets / scripting
timebomb manifest --blame
timebomb manifest --owner alice # filter by owner
timebomb manifest --tag TODO # filter by tag
timebomb manifest --message oauth # filter by message text
timebomb manifest --owner-missing # only fuses with no owner and no blame result
timebomb manifest --path-only # print unique files containing matching fuses
timebomb manifest --no-inert # hide inert fuses
timebomb manifest --file src/auth.rs # filter to a specific file (supports globs)
timebomb manifest --file "src/auth/**" # glob filter
timebomb manifest --file src/auth.rs --file src/db.rs # multiple files
timebomb manifest --between 2026-01-01 2026-06-30 # date range filter
timebomb manifest --sort date # sort by expiry date (default)
timebomb manifest --sort file # sort by file path then line
timebomb manifest --sort owner # sort by owner then date
timebomb manifest --sort status # sort detonated → ticking → inert
timebomb manifest --next 10 # show only the 10 soonest fuses
timebomb manifest --count # print only the count as a plain integerTerminal output includes a compact age column showing days until expiry or overdue:
DETONATED src/auth/login.rs:42 TODO[2025-01-15] -433d [alice] remove legacy oauth flow
TICKING src/db/schema.sql:108 FIXME[2026-04-08] +15d drop temp_users table
INERT src/api/handler.rs:77 HACK[2099-01-01] +26946d revisit when platform ships
timebomb defuse # walk through each detonated fuse
timebomb defuse ./srcFor each detonated fuse, defuse prompts:
DETONATED src/auth/login.rs:42 TODO[2025-01-15]: remove legacy oauth flow
[e] Extend to new date
[d] Delete line
[s] Skip
Choice:
Extend prompts for a new date and rewrites the annotation in-place. Delete removes the line. Files are updated in a single bottom-up pass per file to avoid line-shift bugs.
timebomb plant src/auth/login.rs:42 "remove after migration" --date 2026-06-01
timebomb plant src/auth/login.rs:42 "remove after migration" --in-days 90
timebomb plant src/auth.rs "remove oauth" --search legacy_auth --tag FIXME --owner alice --yestimebomb delay src/auth/login.rs:42 --date 2026-09-01
timebomb delay src/auth/login.rs:42 --in-days 30 --reason "blocked on upstream fix"timebomb disarm src/auth/login.rs:42
timebomb disarm --all-detonated # remove every detonated fuse in the scan path
timebomb disarm --all-detonated --yes # skip confirmationtimebomb intel # count fuses grouped by owner and tag
timebomb intel --by owner
timebomb intel --by tag
timebomb intel --by month # timeline view grouped by expiry month
timebomb intel --by tag --format json
timebomb intel --message oauth # only count fuses whose message mentions oauthtimebomb tripwire set --yes # append timebomb block to .git/hooks/pre-commit
timebomb tripwire cut --yes # remove only the timebomb block; leave other content intactThe hook block written by tripwire set:
# BEGIN timebomb
timebomb sweep --since HEAD .
# END timebombInstalling twice is idempotent. Cutting removes only the marked block; if the file becomes empty it is deleted.
timebomb fallout report-jan.json report-feb.json
timebomb fallout --format json report-jan.json report-feb.jsonReads two JSON reports produced by timebomb sweep --format json and shows how fuse debt changed between them — newly detonated, resolved, and delayed (deadline bumped without fixing).
timebomb bunker save # snapshot current detonated/ticking counts
timebomb bunker show # compare live counts to the saved baselinebunker save writes .timebomb-baseline.json:
{
"generated_at": "2026-03-22T10:00:00Z",
"detonated": 3,
"ticking": 5
}When this file exists, timebomb sweep automatically loads it and exits 1 if the current detonated or ticking count exceeds the baseline — preventing debt from growing while not requiring everything to be fixed at once.
Hard ceilings can also be set in .timebomb.toml independently of the baseline file:
max_detonated = 0
max_ticking = 5timebomb completions bash # print bash completion script
timebomb completions zsh # print zsh completion script
timebomb completions fish # print fish completion scriptPipe to your completions directory to enable tab-completion for all subcommands and flags:
# zsh
timebomb completions zsh > ~/.zsh/completions/_timebomb
# bash (user-level, no sudo required)
mkdir -p ~/.local/share/bash-completion/completions
timebomb completions bash > ~/.local/share/bash-completion/completions/timebomb
# bash (system-wide, requires sudo)
timebomb completions bash | sudo tee /etc/bash_completion.d/timebomb
# fish
timebomb completions fish > ~/.config/fish/completions/timebomb.fishDETONATED src/auth/login.rs:42 TODO[2026-01-15] -433d remove legacy oauth flow
TICKING src/db/schema.sql:108 FIXME[2026-04-01] +8d drop temp_users table
INERT src/api/handler.rs:77 HACK[2099-01-01] +26946d revisit when platform ships
Swept 142 file(s) · 17 fuse(s) · 1 detonated · 1 ticking · 15 inert
The age column (-Xd / +Xd) shows how many days overdue or until expiry. With --blame, unowned fuses show the git blame author as [~name]. Explicit [owner] brackets are shown as-is and are never overwritten.
Respects NO_COLOR.
{
"swept_files": 142,
"total_fuses": 17,
"detonated": [
{
"file": "src/auth/login.rs",
"line": 42,
"tag": "TODO",
"date": "2026-01-15",
"owner": null,
"message": "remove legacy oauth flow",
"status": "detonated"
}
],
"ticking": [...],
"inert": [...]
}file,line,tag,date,owner,status,message
src/auth/login.rs,42,TODO,2026-01-15,,detonated,remove legacy oauth flow
Fields containing commas or quotes are quoted per RFC 4180.
timebomb: failed
swept_files: 142
total_fuses: 17
detonated: 2
ticking: 1
inert: 14
next_action:
- fix src/auth/login.rs:42 TODO[2025-01-15][alice]: remove legacy oauth flow
This format is intentionally compact and deterministic so agents can paste it into task reports or PR comments.
{
"status": "failed",
"actions": [
{
"kind": "review_detonated",
"file": "src/auth/login.rs",
"line": 42,
"target": "src/auth/login.rs:42",
"tag": "TODO",
"date": "2025-01-15",
"owner": "alice",
"status": "detonated",
"message": "remove legacy oauth flow",
"command": "timebomb delay src/auth/login.rs:42 --date YYYY-MM-DD --reason \"...\""
}
]
}The fix plan is non-mutating. It only includes detonated and ticking fuses.
Emits workflow commands that appear as inline PR annotations:
::error file=src/auth/login.rs,line=42::TODO detonated on 2026-01-15: remove legacy oauth flow
::warning file=src/db/schema.sql,line=108::FIXME ticking until 2026-04-01: drop temp_users table
Auto-detected when GITHUB_ACTIONS=true is set.
.timebomb.toml in the project root:
# Tags to scan for
triggers = ["TODO", "FIXME", "HACK", "TEMP", "REMOVEME", "DEBT", "STOPSHIP", "WORKAROUND", "DEPRECATED", "BUG"]
# Flag fuses expiring within this many days as ticking (0 = disabled)
fuse_days = 14
# Glob patterns to exclude from scanning
exclude = [
"vendor/**",
"node_modules/**",
"*.min.js",
".git/**",
]
# File extensions to scan. If empty, all non-binary files are scanned.
extensions = ["rs", "go", "ts", "js", "py", "rb", "java", "sql", "tf", "yaml", "yml"]
# Ratchet ceilings: sweep fails if live count exceeds these values.
max_detonated = 0
max_ticking = 5| Key | Type | Default | Description |
|---|---|---|---|
triggers |
[string] |
see above | Tags to match (case-insensitive) |
fuse_days |
integer | 0 |
Days before expiry to enter ticking status |
exclude |
[string] |
["vendor/**","node_modules/**","*.min.js",".git/**"] |
Glob exclusions |
extensions |
[string] |
see defaults | Extensions to scan; empty means all non-binary |
max_detonated |
integer | — | Hard ceiling; sweep exits 1 if exceeded |
max_ticking |
integer | — | Hard ceiling; sweep exits 1 if exceeded |
CLI flags override config file values. If no config file is found, built-in defaults apply silently.
| Variable | Description |
|---|---|
TIMEBOMB_FUSE_DAYS |
Default fuse warning window (e.g. 14 or 14d). Overridden by --fuse. |
NO_COLOR |
Disable terminal color output when set. |
GITHUB_ACTIONS |
When true, auto-selects GitHub Actions output format. |
name: timebomb
on:
push:
pull_request:
schedule:
- cron: '0 9 * * *' # daily sweep even without a push
jobs:
timebomb:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install timebomb
run: |
curl -sSL https://github.com/pbsladek/timebomb/releases/latest/download/timebomb-linux-x86_64 \
-o /usr/local/bin/timebomb
chmod +x /usr/local/bin/timebomb
- run: timebomb sweep --fuse 14d --fail-on-ticking--format github is inferred automatically from GITHUB_ACTIONS=true, so workflow command annotations appear in the PR diff without any extra flags.
timebomb tripwire set --yesOr manually in .git/hooks/pre-commit:
#!/bin/sh
set -e
timebomb sweep --since HEAD .Releases are automated via release-please. Every merge to main is inspected for Conventional Commits:
| Commit type | Version bump |
|---|---|
fix: |
patch |
feat: |
minor |
feat!: or BREAKING CHANGE: footer |
major |
release-please opens a release PR that bumps Cargo.toml and drafts the changelog. Merging that PR creates the git tag and GitHub release automatically.
The crates.io package is timebomb-cli, but the release component stays timebomb so release tags remain vX.Y.Z and the installed executable remains timebomb.
- Walk: Recursive directory walk via
walkdir. Symlinks are not followed. - Exclusions: Paths matching any
excludeglob are skipped before opening files. - Extension filter: Only files whose extension matches the
extensionslist are scanned. An empty list disables the filter. - Binary detection: The first 8 KB of each candidate file is checked for null bytes (
\x00). Files containing any are skipped silently. - Parallel scan: After the serial walk phase collects candidates, files are scanned in parallel via
rayon. The compiled regex is shared across all worker threads. - Invalid dates: A fuse with an unparseable date (e.g.
TODO[2026-13-45]) emits a warning to stderr and is skipped; the scan continues. - Sort: Results are sorted by date ascending so the most urgent fuses appear first.
| Code | Meaning |
|---|---|
0 |
Clean — no detonated fuses (or counts within baseline/ceilings) |
1 |
Detonated fuses found, ticking threshold exceeded with --fail-on-ticking, or ratchet ceiling breached |
2 |
Configuration or runtime error |
Requires Rust 1.80+.
cargo build
cargo test
cargo clippy -- -D warnings
cargo fmt --checkMIT — see LICENSE for details.