Skip to main content

silentbugs

ci pypi python

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)

Source distribution for silentbugs 0.1.0
File Size Uploaded
silentbugs-0.1.0.tar.gz 18.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for silentbugs 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page