# timebomb
[](https://github.com/pbsladek/timebomb/actions/workflows/ci.yml)
[](https://crates.io/crates/timebomb-cli)
[](LICENSE)
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):
| `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](https://github.com/pbsladek/timebomb/releases/latest):
```bash
# 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
```bash
cargo install timebomb-cli --locked
```
### From source
```bash
git clone https://github.com/pbsladek/timebomb
cd timebomb
cargo install --path . --locked
```
---
## Commands
### `sweep` — scan and detonate in CI
```bash
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 --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.
### `armory` — prioritize active fuses
```bash
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 days
```
`armory` ranks detonated fuses first, with the most overdue at the top, then ticking fuses by soonest deadline. It always exits 0.
### `manifest` — list all fuses
```bash
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 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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
timebomb intel --message oauth # only count fuses whose message mentions oauth
```
### `tripwire` — manage the git pre-commit hook
```bash
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`:
```sh
# 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
```bash
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
```bash
timebomb bunker save # snapshot current detonated/ticking counts
timebomb bunker show # compare live counts to the saved baseline
```
`bunker save` writes `.timebomb-baseline.json`:
```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:
```toml
max_detonated = 0
max_ticking = 5
```
### `completions` — shell completion scripts
```bash
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:
```bash
# 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)
# 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`)
```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](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) 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:
```toml
# 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
```
| `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
| `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
```yaml
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
```bash
timebomb tripwire set --yes
```
Or manually in `.git/hooks/pre-commit`:
```sh
#!/bin/sh
set -e
timebomb sweep --since HEAD .
```
---
## Releases
Releases are automated via [release-please](https://github.com/googleapis/release-please). Every merge to `main` is inspected for [Conventional Commits](https://www.conventionalcommits.org/):
| `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`.
---
## 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
| `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+.
```bash
cargo build
cargo test
cargo clippy -- -D warnings
cargo fmt --check
```
---
## License
MIT — see [LICENSE](LICENSE) for details.