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 --lockedgit clone https://github.com/pbsladek/timebomb cd timebomb cargo install --path . --lockedtimebomb 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 --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 --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 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 --owner-missing # only fuses with no owner and no blame result 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 jsontimebomb 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.
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.
- 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.