wiringguard
Prove the guard you added is still called.
You added a permission check. An audit-log call. A thumbnail step that stops a video going out with whatever frame the platform picks. Six months later a refactor leaves the import in place and drops the call. Every test still passes — the tests exercise the happy path, and the guard was never the thing under test.
pip install wiringguard
# pyproject.toml
[[tool.wiringguard.rules]]
name = "every upload sets a custom cover"
kind = "must-call"
where = "uploaders/*.py"
function = "upload_*"
call = "ensure_cover"
$ wiringguard
uploaders/youtube.py:205: [every upload sets a custom cover]
upload_video() no longer calls ensure_cover() — the name still appears in the
file, but only as an import or a mention, never as a call
1 rule(s) | 1 violation(s)
Why not grep
Because grep passes. That output above is from a real file where the string ensure_cover
is still present — in a docstring — while the call itself had been replaced. A watchdog
built on if "ensure_cover" in source reports green over code that no longer does the thing,
and it reports green forever.
The insidious version is even quieter: the refactor keeps from covers import ensure_cover
at the top and removes the one call. Now the symbol is genuinely imported, your editor shows
no warning, the linter shows no unused import if something else touches it, and every text
search finds it.
wiringguard parses the file. A call is a call.
The two kinds of rule
must-call — the guard is still invoked
[[tool.wiringguard.rules]]
name = "writes go through the audit log"
kind = "must-call"
where = "app/repository/*.py"
function = "save_*"
call = "audit.record"
function accepts a glob, or "*" for every function in the matched files. If the function
was renamed or deleted, that is reported too — a rule quietly guarding nothing is the
failure this tool exists to end, not a pass.
only-in — nobody grew a second path
[[tool.wiringguard.rules]]
name = "one place may publish"
kind = "only-in"
call = "client.videos.insert"
files = ["uploaders/youtube.py", "uploaders/legacy_*.py"]
A second module that quietly grew its own copy of a privileged operation is how an ungated side door gets born. This finds it.
Rules match on the suffix of a dotted call path, with intermediate calls collapsed:
client.videos().insert(...) matches videos.insert or client.videos.insert, and does
not match db.insert. Name as much of the path as you need to be unmistakable.
In CI
- run: pip install wiringguard
- run: wiringguard
| exit | meaning |
|---|---|
0 |
every rule still holds |
1 |
something came unwired |
2 |
could not check — no config, an empty ruleset, a bad path |
2 is deliberately separate. A tool whose whole subject is "a check that silently stopped
checking" must never let "I could not look" be read as "I looked and it was fine". For
the same reason, an empty ruleset is refused rather than exiting 0, and files that fail to
parse are reported instead of skipped.
How it compares
| scope | |
|---|---|
import-linter, pytest-archon |
which modules may import which |
semgrep |
general pattern matching, one pattern at a time |
wiringguard |
which functions must call what, and who may call it at all |
Import contracts do not tell you whether the function is actually invoked. This is the call-site half of the same idea, in about a hundred lines of AST.
Configuration
wiringguard.toml at the project root, or a [tool.wiringguard] section in pyproject.toml.
Vendored directories (.venv, site-packages, node_modules, caches, build, dist) are
skipped. Sources are read as utf-8-sig, so a byte-order mark does not turn a check into a
silent skip.
Requirements
Python 3.9+. No dependencies on 3.11+ (tomli below that). Linux, macOS and Windows.
License
MIT
Metadata
Release files for wiringguard 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 | |
|---|---|---|---|
| wiringguard-0.1.0.tar.gz | 11.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wiringguard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.5 kB
Release files / wiringguard-0.1.0.tar.gz
| Download URL | wiringguard-0.1.0.tar.gz |
|---|---|
| Size | 11.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0b8be8ec5c878b82f89120215c865e798a75bc2f1d681ee5fc7e8b63e2499647
|
|
BLAKE2b-256 checksum How to use checksums |
8ed48ec001637540c305e78024539d375cfde73a8b7bbf1150cd24c00413fa09
|
| 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 / wiringguard-0.1.0-py3-none-any.whl
| Download URL | wiringguard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98b14af1708b97cd46f79bbbc40be95dea5f6dc3e122e8a4a88811a17e9c5095
|
|
BLAKE2b-256 checksum How to use checksums |
0d73c7cf057a57abe6cc8c66038f86b310358c38ed0951caa793518d7602f6a7
|
| 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