Skip to content

pbsladek/timebomb

Repository files navigation

timebomb

CI Crates.io License: MIT

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.


Fuse format

// 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.

Default triggers

TODO, FIXME, HACK, TEMP, REMOVEME, DEBT, STOPSHIP, WORKAROUND, DEPRECATED, BUG

Tags are matched case-insensitively. The full set is configurable via .timebomb.toml.

Fuse status

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

Installation

Pre-built binaries (fastest)

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.exe

Via cargo

cargo install timebomb --locked

From source

git clone https://github.com/pbsladek/timebomb cd timebomb cargo install --path . --locked

Commands

sweep — scan and detonate in CI

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 --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 5

sweep is the only command that exits non-zero. All other commands are informational and always exit 0.

manifest — list all fuses

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 integer

Terminal 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 

defuse — interactively resolve detonated fuses

timebomb defuse # walk through each detonated fuse timebomb defuse ./src

For 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.

plant — insert a new fuse

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 --yes

delay — bump a deadline

timebomb 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"

disarm — remove a fuse

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 confirmation

intel — breakdown by owner, tag, or month

timebomb 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

tripwire — manage the git pre-commit hook

timebomb tripwire set --yes # append timebomb block to .git/hooks/pre-commit timebomb tripwire cut --yes # remove only the timebomb block; leave other content intact

The hook block written by tripwire set:

# BEGIN timebomb timebomb sweep --since HEAD . # END timebomb

Installing twice is idempotent. Cutting removes only the marked block; if the file becomes empty it is deleted.

fallout — compare two report snapshots

timebomb fallout report-jan.json report-feb.json timebomb fallout --format json report-jan.json report-feb.json

Reads 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).

bunker — ratchet enforcement

timebomb bunker save # snapshot current detonated/ticking counts timebomb bunker show # compare live counts to the saved baseline

bunker 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 = 5

completions — shell completion scripts

timebomb completions bash # print bash completion script timebomb completions zsh # print zsh completion script timebomb completions fish # print fish completion script

Pipe 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.fish

Output formats

Terminal (default)

DETONATED 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.

JSON (--format json)

{ "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": [...] }

CSV (--format csv, manifest only)

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.

GitHub Actions (--format github)

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.


Configuration

.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.

Environment variables

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.

CI integration

GitHub Actions

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.

Pre-commit hook

timebomb tripwire set --yes

Or manually in .git/hooks/pre-commit:

#!/bin/sh set -e timebomb sweep --since HEAD .

Releases

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.


Scanner behavior

  • Walk: Recursive directory walk via walkdir. Symlinks are not followed.
  • Exclusions: Paths matching any exclude glob are skipped before opening files.
  • Extension filter: Only files whose extension matches the extensions list 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.

Exit codes

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

Development

Requires Rust 1.80+.

cargo build cargo test cargo clippy -- -D warnings cargo fmt --check

License

MIT — see LICENSE for details.

About

Scan source code for deadline-tagged fuses and fail when they detonate.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages