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-cli --locked

Via Docker

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

From source

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

Build and push the container image

make docker-build
make docker-smoke
make docker-push

Defaults 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/arm64

GitHub Actions also pushes the Docker image on release when release-please creates a release tag, using DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.


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

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

armory — prioritize active fuses

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.

explain — focus an agent on one fuse

timebomb explain src/auth/login.rs:42
timebomb explain src/auth/login.rs:42 --path .
timebomb explain src/auth/login.rs:42 --blame

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

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

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
timebomb intel --message oauth          # only count fuses whose message mentions oauth

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.

Agent summary (sweep --agent-summary)

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.

Fix plan (sweep --fix-plan json)

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

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.

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

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