Snapshot your project's environment over time and find out what actually broke it.
Project description
whatbroke
It worked yesterday. Why is it broken today?
whatbroke records your project's environment over time — dependencies,
lockfiles, env vars, config files, container base images, runtime versions — so
that when something breaks you can see exactly what moved, instead of guessing.
30 seconds
A real session — werkzeug 3.0 removed werkzeug.urls.url_quote, which broke a
great many Flask apps in 2023. Reproduce it yourself with
scripts/record-demo.sh.
$ whatbroke snapshot --label "green: callbacks working"
Snapshot #1 captured (126 entries)
# ...a week later, a dependency floats and the app stops working:
$ python app.py
Traceback (most recent call last):
File "app.py", line 1, in <module>
from werkzeug.urls import url_quote
ImportError: cannot import name 'url_quote' from 'werkzeug.urls'
$ whatbroke snapshot --label "red: ImportError"
$ whatbroke diff
Diff: #1 (green: callbacks working) -> #2 (red: ImportError)
Python dependencies (1 changed)
~ werkzeug 2.2.3 -> 3.0.1
Config files (1 changed)
~ docker-compose.yml content changed (+1/-1 lines) sha256:19325d3ea1f8
Nothing in the source changed. The diff finds it in one command.
Quickstart
Four commands, under a minute:
pipx install whatbroke # or: pip install whatbroke
cd your-project
whatbroke init # detects your package managers, writes a config
whatbroke snapshot # capture the environment as it is right now
Then later, once something has broken:
whatbroke snapshot
whatbroke diff # what changed between the last two snapshots
Diff: #1 (working state, 7 minutes ago) → #2 (broken state, just now)
Python dependencies (2 changed, 1 added, 1 removed)
~ requests 2.31.0 → 2.32.3
~ urllib3 2.0.7 → 2.2.1
+ gunicorn 21.2.0
- flask 3.0.0
Config files (3 changed)
~ Dockerfile content changed (+1/-1 lines) sha256:843cf881543a
~ docker-compose.yml content changed (+2/-2 lines) sha256:3293327d0668
~ requirements.txt content changed (+3/-3 lines) sha256:4270126a4293
Container base images (1 changed)
~ Dockerfile#0 (builder) python:3.11-slim → python:3.12-slim
To capture history automatically from here on:
whatbroke install-hook # snapshot on every commit
Why this exists
The failure mode is familiar. A test that passed on Friday fails on Monday. You
didn't change anything. git log is clean. And yet.
Something did change — just not in your source. A transitive dependency floated to a new minor. A Docker base image tag moved under you. A teammate added an env var to the deploy config. Your colleague's machine has a different Python patch version.
git bisect can't help, because git doesn't track any of it. Version control
gave us a rigorous answer to "what changed in the code?" and nothing
comparable for "what changed around the code?". whatbroke is that missing
history.
When the diff is long
Hand it the error and let Claude rank the suspects:
export ANTHROPIC_API_KEY=sk-ant-...
whatbroke explain --error "$(cat error.log)"
The diff plausibly explains this error.
1. urllib3 2.2.1 -> 2.0.7 [high] python-deps
why urllib3 2.0 predates the Retry re-export the traceback imports.
check pip install 'urllib3==2.2.1' and re-run the failing import
2. requests 2.31.0 -> 2.32.3 [medium] python-deps
why requests 2.32 widened its urllib3 pin, likely pulling the older one in.
check pip install 'requests==2.31.0' and see if pip moves urllib3 with it
It is instructed to say so when the diff doesn't explain your error, rather than inventing a plausible-sounding culprit.
Everything except explain works offline. No API key, no account, no telemetry.
When you have a repro command
Let it binary-search your history for the breaking change:
whatbroke bisect --command "pytest tests/test_api.py" --yes
Verifying the good and bad ends behave as claimed...
testing #8 (bad end) FAIL exit 1
testing #1 (good end) PASS exit 0
testing #4 PASS exit 0
testing #6 FAIL exit 1
testing #5 FAIL exit 1
Environment restored to snapshot #9
First bad snapshot: #5 (last good was #4)
...followed by the diff between those two snapshots.
⚠️ bisect modifies your environment
To test an old snapshot it has to become that snapshot: it checks out old commits and reinstalls dependencies. Run it in a container or a disposable venv.
- Requires
--yes. Use--dry-runto print the exact plan and change nothing.- Refuses to start on a dirty working tree, naming the files at risk.
- Takes a restore point snapshot first and restores to it afterwards — including on Ctrl-C — returning you to your branch, not a detached HEAD.
- Warns if
whatbrokeis installed into the environment it's about to modify.- A snapshot it can't restore is skipped, not blamed. If skips leave a gap it reports the remaining candidates rather than guessing between them.
If a run is interrupted, recover with
whatbroke restore <id> --yes.
Commands
whatbroke init |
Scaffold .whatbroke/config.toml, detecting your package managers |
whatbroke snapshot [-l LABEL] |
Capture the current environment |
whatbroke list |
Snapshots, newest first |
whatbroke show <snap> |
Dump one snapshot |
whatbroke diff [A] [B] |
What changed (defaults to the last two) |
whatbroke explain --error "..." |
Rank the changes by likely blame, via Claude |
whatbroke bisect --command "..." --yes |
Binary-search for the breaking change |
whatbroke restore <snap> --yes |
Put the environment back |
whatbroke install-hook |
Snapshot automatically on every commit |
whatbroke prune --older-than 30d --yes |
Bound the store's size |
Snapshot references accept ids or shortcuts: latest, latest~2, oldest, -3.
Add --json to diff, explain and bisect for machine-readable output, and
--category to narrow to one kind of change.
What gets captured
| Python | poetry.lock, pdm.lock, uv.lock, requirements.txt, or pip freeze |
| Node | package-lock.json, pnpm-lock.yaml, yarn.lock — resolved versions, not a file hash |
| Rust / Go | Cargo.lock, go.sum |
| Env vars | Names always; values only where you allow-list them |
| Config files | Contents + hash for files you list, secrets scrubbed |
| Containers | FROM lines, with the real image digest when Docker is available |
| System | OS, architecture, and versions of the runtimes this project actually uses |
Everything lives in .whatbroke/store.db (SQLite — see SCHEMA.md).
whatbroke init adds it to your .gitignore; keep it there, because a
committed store gets overwritten by an ordinary git checkout.
Git, Docker and the various package managers are all optional. whatbroke uses what it finds and tells you what it skipped.
Configuration
.whatbroke/config.toml, generated by whatbroke init:
[env]
capture_values = ["NODE_ENV", "CI", "PORT"]
ignore = ["TMP*"]
extra_secret_patterns = []
[files]
track = [".env.example", "docker-compose.yml", "tsconfig.json"]
max_bytes = 262144
[python]
source = "auto" # auto | poetry | pdm | uv | requirements | pip-freeze | none
[ai]
model = "claude-sonnet-5" # WHATBROKE_MODEL overrides this
Secrets
whatbroke treats "never write a secret to disk" as a hard requirement, not a best effort:
- Env var names are always recorded; values never are unless you explicitly allow-list the name.
- A name matching a secret pattern (
*_KEY,*_TOKEN,*_SECRET,*PASSWORD*,DATABASE_URL, …) is never captured. Allow-listing it does not override this — that is deliberate and not configurable. - A value that looks like a credential (
sk-…,ghp_…, a JWT, a PEM key, a URL with inline credentials) is withheld regardless of its name. - Tracked config file contents are scrubbed of secret-looking assignments before storage.
- Withheld values are recorded as a salted hash, with a random salt per
store — so a diff can say "
DATABASE_URLchanged" while the digest is worthless to anyone who obtains the file. whatbroke explainsends the diff to the Anthropic API. Because the store never holds a secret, the request cannot leak one.
Found a way around this? Please report it privately — see CONTRIBUTING.md.
Requirements
Python 3.11+. Linux, macOS and Windows.
Contributing
Bug reports from real broken environments are the most useful thing you can send. New ecosystem collectors are the easiest code contribution — see CONTRIBUTING.md.
pip install -e ".[dev]"
pytest
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whatbroke-0.1.0.tar.gz.
File metadata
- Download URL: whatbroke-0.1.0.tar.gz
- Upload date:
- Size: 84.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7aaa2b70ba9f63eaf650f75caee592060d660d3ec8c84dbb10d92fc99c897dc9
|
|
| MD5 |
008618661728295d232251459bfc6515
|
|
| BLAKE2b-256 |
b8ab46b8c48421e3dcff1a64056836421e71b1ce6f6ef79eb591d4feefc2b59a
|
Provenance
The following attestation bundles were made for whatbroke-0.1.0.tar.gz:
Publisher:
publish.yml on ngari-qds/whatbroke
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whatbroke-0.1.0.tar.gz -
Subject digest:
7aaa2b70ba9f63eaf650f75caee592060d660d3ec8c84dbb10d92fc99c897dc9 - Sigstore transparency entry: 2216172681
- Sigstore integration time:
-
Permalink:
ngari-qds/whatbroke@c4def6ed69460ad377da24b50114ddf78ac56a1a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ngari-qds
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4def6ed69460ad377da24b50114ddf78ac56a1a -
Trigger Event:
push
-
Statement type:
File details
Details for the file whatbroke-0.1.0-py3-none-any.whl.
File metadata
- Download URL: whatbroke-0.1.0-py3-none-any.whl
- Upload date:
- Size: 66.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
738a9478f623745ea45a35ef3f0d8ea5285ecd5101b7573d32a70ef8eaff0e6a
|
|
| MD5 |
b47fdcbd889a4239f0670f179dd6970e
|
|
| BLAKE2b-256 |
4375e6e43446d992e956d5ec6b85816aeaadb615ee3028d66ff78690aa20f33f
|
Provenance
The following attestation bundles were made for whatbroke-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ngari-qds/whatbroke
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whatbroke-0.1.0-py3-none-any.whl -
Subject digest:
738a9478f623745ea45a35ef3f0d8ea5285ecd5101b7573d32a70ef8eaff0e6a - Sigstore transparency entry: 2216172760
- Sigstore integration time:
-
Permalink:
ngari-qds/whatbroke@c4def6ed69460ad377da24b50114ddf78ac56a1a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ngari-qds
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4def6ed69460ad377da24b50114ddf78ac56a1a -
Trigger Event:
push
-
Statement type: