silentbugs
Find the bugs that pass your tests and your linter.
Not the ones that crash. The ones where a command fails, returns an empty string, and your code reads that empty string as a legitimate answer — then deletes the directory.
pip install silentbugs
silentbugs .
scripts/repo_guard.py:118:19: SB001 unreachable-returncode
`subprocess.run(...).stdout` reads the output with no way left to check the return
code: a command that fails returns empty, and the empty value is indistinguishable
from a legitimate answer. Assign the result and check `.returncode`, or pass check=True.
| unmerged = subprocess.run(["git", "log", f"master..{folder}"], capture_output=True).stdout
That line is from a real incident. The folder was not a branch, so git exited 128 with empty stdout, and the guard read the empty string as "zero unmerged commits" and moved on to delete the folder. The safety check had never worked once. Nothing raised, no test failed, and no linter said a word.
Why not just use ruff
You should use ruff. This runs next to it. Measured against ruff --select ALL (every
stable rule ruff has, not a default subset), on the exact shapes below:
| code | ruff --select ALL |
silentbugs |
|---|---|---|
subprocess.run(cmd, check=False).stdout |
— | SB001 |
subprocess.run(cmd, text=True) — no encoding |
— | SB002 |
ast.parse(open(p, encoding="utf-8").read()) |
— | SB003 |
while not ready(): time.sleep(20) |
— | SB004 |
Ruff's PLW1510 flags a missing check= argument — which is why the first row writes
check=False explicitly. The argument is now present, the linter is satisfied, and the
return code is still unreachable. That gap is the whole point of this tool.
The rules
Each one comes from a production incident, not from a style opinion.
SB001 unreachable-returncode
Reading .stdout or .stderr straight off a subprocess call leaves no result object to
ask about the return code, so failed and empty become the same value — and code always
picks the optimistic reading. Quiet when check=True, check_output or check_call is
used, since those raise. Quiet when the call takes **kwargs, because guessing would be
worse than missing one.
SB002 text-without-encoding
text=True without encoding= decodes using the locale codepage — cp1252 on Windows, not
UTF-8. One accented character or emoji in the output raises UnicodeDecodeError inside the
reader thread: the return code still arrives, but stdout comes back as None. The
error message vanishes precisely when someone needs to read it.
SB003 ast-parse-without-bom-tolerance
Source read with encoding="utf-8" and handed to ast.parse raises SyntaxError when the
file starts with a UTF-8 byte-order mark. Scanners wrap that parse in
except SyntaxError: continue, so the file is skipped silently and the tool reports a clean
pass over code it never looked at. Use encoding="utf-8-sig".
SB004 unbounded-polling-loop
A while loop that sleeps, has no break/return/raise, no counter, no deadline, and a
condition its own body can never change. If the state never arrives it runs forever without
ever failing. One of these span for 21 hours before anybody noticed.
while True: is deliberately not flagged — a daemon that runs forever is doing its job.
That exclusion came from measurement: before it, every single hit of this rule on a real
773-file repository was a daemon main loop. Seven out of seven false positives, which is how
a rule gets switched off on its first day.
Adopting it on an existing codebase
A mature repository will have findings. Record them and hold the line from here:
silentbugs . --write-baseline # accept what exists today
silentbugs . --baseline # exit 1 only on something new
The baseline is keyed on content, not line numbers, so adding a comment above a finding does not resurrect it. Delete an entry once you fix it; if the bug comes back, so does the report.
In CI
- run: pip install silentbugs
- run: silentbugs . --baseline
| exit | meaning |
|---|---|
0 |
scanned, nothing new |
1 |
findings |
2 |
could not look — a path does not exist, or an unknown rule id |
2 is separate on purpose: "I could not look" must never be readable as "I looked and it
was clean". That is the same class of bug the tool reports, and it would be embarrassing to
ship it.
Silencing one line
result = subprocess.run(cmd, text=True) # silentbugs: ignore SB002
A bare # silentbugs: ignore silences every rule on that line. The comment works anywhere in
a multi-line call.
Options
| flag | effect |
|---|---|
--baseline [PATH] |
ignore findings recorded in the baseline (default silentbugs-baseline.json) |
--write-baseline [PATH] |
record current findings as accepted, exit 0 |
--select SB001,SB002 |
run only these rules |
--json |
machine-readable output with fingerprints |
--list-rules |
print the rules and exit |
What this is not
Not a linter replacement, not a formatter, not a type checker, and not a security scanner. Four rules, each one earned. If a rule ever fires on code that is fine, that is a bug in the rule — open an issue and it gets tightened or dropped.
Requirements
Python 3.9+. No dependencies. Linux, macOS and Windows.
License
MIT
Metadata
Release files for silentbugs 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| silentbugs-0.1.0.tar.gz | 18.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| silentbugs-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.4 kB
Release files / silentbugs-0.1.0.tar.gz
| Download URL | silentbugs-0.1.0.tar.gz |
|---|---|
| Size | 18.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bca78c1ede59550548a9acb6594f399791350572300335573a1818ba62d4cdb2
|
|
BLAKE2b-256 checksum How to use checksums |
058225bcc780fd9433ea164d81bdfe6a3ea5cda7be76e3f48c08fbffbed10a3a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.
Transparency logRelease files / silentbugs-0.1.0-py3-none-any.whl
| Download URL | silentbugs-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b833cbc3cd72d1d1bcc4d75542a20ce251a3636117480c9c83dae043476c0f34
|
|
BLAKE2b-256 checksum How to use checksums |
8d3e2c193656897b9e04372ded633e8b8e99bcccdc20cc1561111ff751d772ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.
Transparency log