code-guardian
code-guardian
mcp-name: io.github.LuminariSoftwares/code-guardian
One command that tells you three things about any repo, without running it:
Safe: is it safe to run?
Works: will it work?
Complete: does it have what it needs?
Each is a score out of 100 (higher is better), combined into an Overall score and an A–F grade. Scores come only from deterministic checks with fixed weights: same repo, same version, same output, every time. On top of that:
Orphan code: functions, classes and modules nothing reachable from the project's entry points uses, judged against a small goal contract.
An optional plain-English explanation from a local model, which never adds, removes or rescores a finding.
It parses the code (ast for Python; json, tomllib and configparser for manifests; regular expressions for
secrets) and never executes, imports or installs it. A scan makes
no network calls. Python 3.11+, standard library only. Status: 0.2.0 is the current release. The fourth guardian,
next to stack-guardian,
context-guardian and
tool-guardian.
Install
pip install code-guardian-mcpThe distribution is code-guardian-mcp (code-guardian on PyPI is someone else's project); the commands are
code-guardian and code-guardian-mcp. To run the MCP server without installing anything: uvx code-guardian-mcp mcp.
From a clone (for the tests):
git clone https://github.com/LuminariSoftwares/code-guardian.git
cd code-guardian
python -m venv .venv
.venv\Scripts\activate # Windows; macOS/Linux: source .venv/bin/activate
pip install -e . pytest
python -m pytest -qRelated MCP server: TaskBounty Check
30-second example
tests/fixtures/risky_repo is a small project that trips every scanner. Its key-shaped strings are never stored in
the repository; tests/make_risky.py adds them to a copy at test time:
python tests/make_risky.py risky_repo
code-guardian scan risky_repoReal output:
code-guardian 0.2.0 scan of risky
files: 7 Python, 3 manifests, 2 other (0 skipped, 0 excluded by the goal file)
Safe 0/100 is it safe to run? -237
Works 85/100 will it work? -15
Complete 35/100 does it have what it needs? -65
Overall 33/100 grade F (a Safe check is FAIL (exec.shell, obfuscation, secret.key, tls.off): the grade is at most D)
biggest deductions:
-60 Safe secret.key FAIL x3 first at deploy/id_rsa:1
-40 Safe obfuscation FAIL x1 first at risky/loader.py:7
-30 Safe exec.shell FAIL x1 first at risky/runner.py:7
-25 Safe tls.off FAIL x1 first at risky/network.py:11
-23 Safe install.hook WARN x3 first at package.json:5
findings (6 FAIL, 19 WARN, 7 INFO; egress listed below):
Safe:
FAIL secret.key deploy/id_rsa:1 private-key block in the source; value redacted
WARN install.hook package.json:5 npm postinstall script runs on install
INFO deps.unpinned package.json:8 dependency left-pad has no pin or upper bound
INFO deps.unpinned pyproject.toml:4 dependency flask has no pin or upper bound
WARN obfuscation risky/loader.py:3 base64-like blob of 340 characters
WARN exec.dynamic risky/loader.py:7 exec() runs code built at runtime
FAIL obfuscation risky/loader.py:7 exec() runs decoded or decompressed data
FAIL tls.off risky/network.py:11 TLS certificate checking is switched off (verify=False)
WARN net.bind_all risky/network.py:16 binds to all interfaces (0.0.0.0)
FAIL exec.shell risky/runner.py:7 os.system runs a shell command built from input, env or network
WARN exec.shell risky/runner.py:11 subprocess.run(shell=True) runs a command through the shell
WARN exec.dynamic risky/runner.py:15 eval() runs code built at runtime
FAIL secret.key risky/settings.py:1 sk- key shape (privacy pattern 5) in the source; value redacted
FAIL secret.key risky/settings.py:2 password-like literal assigned to 'password' in the source; value redacted
WARN obfuscation risky/settings.py:3 high-entropy literal of 48 characters
WARN deser.unsafe risky/storage.py:8 pickle.load can run code hidden in the data
WARN deser.unsafe risky/storage.py:12 yaml.load without SafeLoader can build arbitrary Python objects
WARN fs.outside risky/storage.py:16 writes outside the project: /tmp/risky-report.txt (absolute path)
WARN install.hook setup.py:5 class PostInstall overrides the install command
INFO deps.unpinned setup.py:13 dependency requests has no pin or upper bound
WARN install.hook setup.py:14 setup() cmdclass overrides build/install commands
Works:
WARN works.tests_missing . no test files: nothing checks that the code works
Complete:
WARN complete.ci_missing . no CI configuration: nothing runs the tests on each change
WARN complete.license_missing . no licence file: nobody may legally reuse this code
WARN complete.readme_missing . no README: nobody can tell what this is or how to run it
WARN complete.license_undeclared pyproject.toml:1 the package metadata declares no licence
WARN complete.metadata pyproject.toml:1 pyproject.toml [project] is missing: requires-python
INFO deps.declared_unused pyproject.toml:4 dependency flask is declared but never imported
egress inventory (4):
WARN risky/network.py:5 url http://{host}:8080/upload
INFO risky/network.py:6 url https://example.com/docs
INFO risky/network.py:7 ip 100.64.0.5
INFO risky/network.py:11 call requests.post
hygiene (inferred (library public API) entry points: 1): 0 dead, 0 test-only, 0 uncertain
notes:
- code-guardian analyses Python only: 1 file(s) in other languages (.js 1) got the secret scan only
weights used (each check (code + level) deducts its weight once, plus a quarter of the weight per extra occurrence (rounded up), at most 2x the weight; INFO is never scored):
Safe deps.unpinned INFO 0 · deser.unsafe WARN 10 · exec.dynamic FAIL 30 · exec.dynamic WARN 10 ·
exec.shell FAIL 30 · exec.shell WARN 10 · fs.outside WARN 3 · install.hook WARN 15 ·
net.bind_all WARN 5 · net.egress INFO 0 · net.egress WARN 2 · obfuscation FAIL 40 ·
obfuscation WARN 10 · secret.key FAIL 40 · secret.key INFO 0 · supply.lookalike WARN 15 ·
supply.unknown_package FAIL 40 · supply.unknown_package INFO 0 · supply.unknown_package WARN 10 ·
tls.off FAIL 25 · tls.off WARN 10
Works orphan.dead WARN 3 · works.entry_missing FAIL 30 · works.import_unresolved WARN 10 ·
works.relative_missing FAIL 25 · works.syntax FAIL 30 · works.syntax INFO 0 ·
works.test_no_assert WARN 5 · works.tests_missing WARN 15
Complete complete.ci_missing WARN 10 · complete.env_example WARN 10 · complete.license_missing WARN 20 ·
complete.license_undeclared WARN 5 · complete.lockfile WARN 10 · complete.metadata WARN 10 ·
complete.readme_missing WARN 20 · complete.readme_sections WARN 5 · complete.readme_short WARN 10 ·
deps.declared_unused INFO 0
static analysis: every finding is LIKELY; the scores come from fixed weights, never from a modelExit code 1: there are FAIL findings. --json prints the same report as JSON (shape below); --fail-on WARN (or
INFO) lowers the bar for CI.
exit | meaning |
0 | nothing at or above |
1 | findings at or above it |
2 | usage error, a missing PATH, or a bad goal file |
scan gates on every scored finding (Safe, Works and Complete); code-guardian orphans PATH gates on the hygiene
findings.
How the scores work
Every check is a small pure function with its own tests. Every finding has the guardian family's shape (code,
severity, level, node = file:line, summary of at most 120 characters, evidence, try) and confidence
LIKELY: a parse proves the code is there, not that it runs. Each check code belongs to one dimension: Safe, Works or
Complete.
Per check, not per occurrence. A check (one row of the tables below: a code at a level) deducts its weight once, plus a quarter of the weight for each extra occurrence (rounded up), at most twice the weight. Fifty copies of one minor issue cannot zero a score.
Dimension score = max(0, 100 − its deductions). Safe is 100 minus the v0.1 threat score; its weights are the v0.1 weights, unchanged.
Overall = 0.5 × Safe + 0.3 × Works + 0.2 × Complete, rounded half up.
Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 40 · F below 40.
FAIL cap: any Safe finding at FAIL caps the grade at D, whatever the average. One leaked key never hides behind a good README.
INFO findings are never scored. Every report prints the weight table it used.
The weight tables live in code_guardian/score.py (TABLES); tests/test_readme_sync.py fails if the tables in
this README drift from them.
JSON shape (--json, and the MCP scan tool)
{
"tool", "version", "path", "goal_file",
"files": {"python", "manifests", "other", "skipped", "ignored", "excluded", "unparsed"},
"other_languages": {".js": 1, ...},
"anchor": "pyproject.toml", (the file repo-level findings are attached to in SARIF)
"scores": {"safe", "works", "complete", "overall", "grade", "grade_uncapped", "capped", "cap_reason"},
"dimensions": {"safe" | "works" | "complete": {"score", "deducted",
"checks": [{"code", "level", "count", "weight", "deduction", "first"}]}},
"top": [the five biggest deductions, each a check row plus "dimension"],
"weights": {"safe" | "works" | "complete": {"code LEVEL": weight}},
"weights_rule": "...",
"findings": [{"code", "severity", "level", "node", "summary", "evidence", "try", "confidence", "fix"}],
"egress": [{"node", "level", "kind", "target"}],
"hygiene": {"source", "goal", "entry_points", "keep", "findings", "counts", "notes"},
"notes": [...],
"explanation": {"label", "text"} (only with --explain and a model that answered)
}The MCP scan reply adds exit_code. Nothing in the report depends on the clock.
Safe checks
code | what it looks for | level | weight |
| API-key shapes (the privacy workflow's built-in list: | FAIL | 40 |
| a Stripe test-mode key ( | INFO | 0 |
|
| WARN | 10 |
| …when | FAIL | 30 |
|
| WARN | 10 |
| …when the command is built from | FAIL | 30 |
|
| WARN | 10 |
| every URL and IP literal plus every | INFO | 0 |
| a publicly routable IP, or plain | WARN | 2 |
| binding | WARN | 5 |
|
| FAIL | 25 |
| …only under an | WARN | 10 |
|
| WARN | 15 |
|
| FAIL | 40 |
| base64 or hex blobs of 200+ characters, high-entropy string literals | WARN | 10 |
| writes, deletes and moves aimed at absolute paths, | WARN | 3 |
| dependencies with no pin or upper bound ( | INFO | 0 |
net.egress targets that stay in the inventory at INFO: https to a named host, loopback, the local network
(non-global addresses such as 10/8, 192.168/16 and 100.64/10, single-label names like gpu-box, .local, .lan,
.internal, .home.arpa) and reserved documentation names (example.com, .test, .invalid).
The walk skips .git, venv/.venv, node_modules, __pycache__, dist, build and the tool caches, anything
the root .gitignore matches (simple globs; ! negation is ignored), binary files, files over 2 MB and symlinks.
Invented and lookalike packages (Safe)
AI coding assistants sometimes suggest a package that does not exist. Anyone can register that name on PyPI and put anything in it, and attackers do exactly that with the names assistants invent. A package existing is not proof it is safe: these checks flag names worth a second look; they do not vouch for any package.
code | what it looks for | level | weight |
| offline (default): a declared dependency or an unresolved import that is not in the bundled list of known packages; run with | INFO | 0 |
|
| FAIL | 40 |
|
| WARN | 10 |
| one or two edits (insert, delete, change, swap) away from a known package without being it; two edits only for names of 8+ characters, none under 5 | WARN | 15 |
Offline is the default and makes no network call. The bundled list,
code_guardian/data/known_packages.txt.gz, holds about 960 widely used PyPI projects. It is hand-built (tools/known_packages.txt, written for this project and not derived from any download dataset) and verified to exist on PyPI bypython tools/build_known_packages.py --verify. The brief asked for the top ~15,000 projects by downloads; the usual source (hugovk/top-pypi-packages) carries no licence we could confirm, so a smaller hand-built list ships instead. A name missing from it is not suspicious by itself, which is why offline misses are INFO.--onlineaskshttps://pypi.org/pypi/<name>/json(override withCODE_GUARDIAN_PYPI_URL, timeoutCODE_GUARDIAN_PYPI_TIMEOUT, default 10 s) once per name per run. A timeout or error becomes a note; it never fails the scan. Online answers depend on PyPI and on today's date, so--onlineoutput is not reproducible run to run; offline output is.
Works checks
Will it work? Read from the parse and the manifests, never by importing or running anything.
code | what it looks for | level | weight |
| a | FAIL | 30 |
| …when the project's | INFO | 0 |
| an absolute import that is not in the standard library, not a module or package in the repo, and not a declared dependency | WARN | 10 |
| a relative import ( | FAIL | 25 |
| no test files at all ( | WARN | 15 |
| test functions with no | WARN | 5 |
| a | FAIL | 30 |
| dead code from the hygiene section (below), folded in at a low weight, only when entry points exist | WARN | 3 |
How works.import_unresolved decides:
Standard library:
sys.stdlib_module_namesplus the modules removed after 3.11 (distutils,imp,cgi…) and added since (annotationlib,compression), so the answer does not depend on which Python runs the scan.In the repo: any directory name or module stem in the tree (a script's sibling module counts).
Declared: every dependency in
pyproject.toml([project], optional dependencies,dependency-groups,build-system.requires, Poetry, PDM, uv),setup.py(parsed, never run),setup.cfg,requirements*.txt,requirements/*.txtandPipfile. Import names map to distributions through a bundled table of known mismatches (yaml→PyYAML,PIL→Pillow,cv2→opencv-python,sklearn→scikit-learn,bs4→beautifulsoup4,dateutil→python-dateutil…) plus the normalised name,python-X/pyX/X-pythonforms and namespace packages (google→google-cloud-*). It never reads the scanner's own installed packages: they describe this machine, not the repo.Product code only: test files and
docs/are not checked (their dependencies are dev tools, and tests often import modules they write at runtime).Not reported: imports inside either branch of
try: … except ImportError, underif TYPE_CHECKING:, or under anifon a flag such atrysets (if has_simplejson:);setuptoolsand friends insetup.py.A dependency that is only installed because another one pulls it in still counts as undeclared: it works by accident. Undeclared dependencies have one owner, this check; Complete does not report them again.
Complete checks
Does it have what it needs? File presence plus basic parsing.
code | what it looks for | level | weight |
| no | WARN | 20 |
| packaging metadata ( | WARN | 5 |
| no README at the root | WARN | 20 |
| the README is under 200 bytes | WARN | 10 |
| a README of 200+ bytes with no install heading (install, setup, getting started, quick start) or no usage heading (usage, examples, tutorial, run, CLI…) | WARN | 5 |
| no CI configuration ( | WARN | 10 |
|
| WARN | 10 |
| an application with no lockfile ( | WARN | 10 |
| the code reads environment variables ( | WARN | 10 |
| a runtime dependency that nothing in the repo imports (runners, linters, | INFO | 0 |
Lockfiles count only for applications. A repo is an application when it has any of these markers: a
Dockerfile/Containerfile; a __main__.py while the project declares no [project.scripts]; or a module-level
web app object (Flask(…), FastAPI(…), Starlette(…), Quart(…), Sanic(…), Django's
get_wsgi_application()/get_asgi_application()). Markers under examples/, docs/, tests/ and similar folders do
not count. A library (it has [project] and none of these markers) is not penalised for having no lockfile.
.env.example is required only for applications that read environment variables (the same markers as the
lockfile rule), outside tests and examples. Libraries read optional variables such as NO_COLOR all the time and do
not ship .env files; calibration showed every clean library tripping the check otherwise. A read through a variable
that merely holds os.environ is not followed.
Undeclared dependencies are owned by works.import_unresolved; Complete reports only the opposite case,
declared but unused. A repo with no Python source and no manifest has nothing for Works or Complete to check: it
scores 100 there, and the report says so.
Outputs, GitHub Action and pre-commit
flag | prints |
(none) | the text report shown above |
| the full report (shape above) |
| SARIF 2.1.0 for GitHub code scanning: one rule per check code (title, meaning, fix and false-positive notes from the catalogue below; Safe rules carry |
| a Markdown report for a pull-request comment or a job summary |
Every format is deterministic: no timestamps, no absolute paths. --fail-on works the same with each. The MCP
scan tool keeps returning JSON.
GitHub Action (action.yml in this repo, a composite action). It installs code-guardian from the action's own
checkout, writes code-guardian.sarif, adds the Markdown report to the job summary, uploads the SARIF to code
scanning, and then fails the job when a finding is at or above fail-on:
name: code-guardian
on: [push, pull_request]
permissions:
contents: read
security-events: write # for the SARIF upload
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: LuminariSoftwares/code-guardian@v0.2.0
with:
path: .
fail-on: FAIL # or WARN
online: "false" # "true" asks PyPI about unknown dependency namesInputs: path, fail-on, online, sarif-file (default code-guardian.sarif), upload-sarif (default true),
python-version (default 3.11). Outputs: overall, grade, exit-code. Inputs reach the shell only through
environment variables, never by text substitution.
pre-commit (.pre-commit-hooks.yaml). It scans the whole repository, not just the staged files:
repos:
- repo: https://github.com/LuminariSoftwares/code-guardian
rev: v0.2.0
hooks:
- id: code-guardian
# args: [--fail-on, WARN]Check catalogue
Every finding code: what it means, how to fix it, and when it can be wrong. Weights are in the dimension tables above. Two rules apply across Safe:
Test code and documentation are reported at INFO. Safe findings in tests (
tests/,test_*.py,conftest.py, fixture and test-data folders) and in documentation (docs/,.md,.rst,.txt) keep their place in the report but do not score: users run the product, not its tests or its doc samples. Two exceptions keep their level: API-key shapes (a real key is leaked wherever it sits) andexecof decoded data (a payload inconftest.pyruns on everypytest).Calibration. Each false-positive note below comes from scanning five well-known clean projects and the four guardian repositories; see calibration.md.
code | dimension | what it means | how to fix it | false positives |
| Safe | One of code-guardian's own checks raised on this file; the rest of the scan ran. | Report it at github.com/LuminariSoftwares/code-guardian/issues with the file if you can share it. | It is a code-guardian bug, never a finding about your code. |
| Safe | A dependency with no pin and no upper bound. | Pin it (==) or cap it (< next major). | Libraries often prefer open ranges on purpose; INFO, never scored. |
| Safe | pickle, marshal, yaml.load without SafeLoader, or torch.load without weights_only=True can run code hidden in the data. | Load only files you created, or use a data-only format (json, safetensors, yaml.safe_load). | Caches the program writes and reads itself are lower risk. |
| Safe | eval, exec or compile runs a non-literal string, or import loads a module named at runtime; FAIL when eval or exec runs text from input(), sys.argv, request data, the environment or the network. | Use a parser, a lookup table or ast.literal_eval; import modules by name; never run input as code. | Plugin loaders and REPL-style tools use these on purpose. |
| Safe | os.system, os.popen or subprocess with shell=True; FAIL when the command text comes from input, the environment or the network. | Pass an argument list without shell=True; validate any outside value against an allow-list. | Fixed command strings in build scripts are WARN, not FAIL; taint is tracked one function deep. |
| Safe | A write, delete or move aimed at an absolute path, the home directory or a .. escape. | Write under the project or under a path the user passes in. | Tools whose job is to write to /tmp or a user config folder. |
| Safe | setup.py cmdclass or install-command subclasses, package.json preinstall/install/postinstall, or an in-tree build backend. | Read what the hook runs before installing; prefer packages without install-time code. | Projects that compile extensions legitimately customise build commands. |
| Safe | A server binds 0.0.0.0 or ::, so other machines can reach it. | Bind to 127.0.0.1 unless other machines must connect. | Containers usually must bind 0.0.0.0; a test that asserts the bind is refused still matches. |
| Safe | The egress inventory: every URL and IP literal and every network call site. WARN for a public IP, or plain http/ws/ftp to a public name or to a host set at runtime. | Use https, and confirm each destination is expected. | Local-network and documentation hosts stay INFO; a URL in a help string is still inventoried. |
| Safe | exec/eval of base64, zlib, marshal or hex-decoded data (FAIL); long encoded blobs or high-entropy literals (WARN). | Decode the payload offline and read what it does before running anything. | Embedded icons, test vectors and hashes can look like blobs. |
| Safe | An API-key shape (OpenAI-style sk-, GitHub, Google, AWS, Stripe live, Slack, Hugging Face and others), an AWS secret key, a private-key block, a password-like literal or a high-entropy literal in a name ending _KEY or _TOKEN is written into a file (a Stripe test key is INFO). | Move it to an environment variable or a secret store, and rotate it: it is in the history. | Test fixtures and documentation samples; values that call themselves fake (mock, dummy, example) are skipped. |
| Safe | The name is one or two edits away from a known package without being it: the typosquatting pattern. | Check you meant the well-known package. | Real packages with names close to a popular one. |
| Safe | Offline: the name is not in the bundled list of known packages (INFO). Online: PyPI has no such project (FAIL), or it is new or has a single release (WARN). | Check the name is the one you meant; remove names an assistant invented; run with --online. | Offline misses are common for less-known real packages, which is why they are INFO. Existing is not safe. |
| Safe | verify=False, ssl._create_unverified_context, CERT_NONE or check_hostname = False. | Remove it; point at a CA bundle if the certificate is private. | Rare: code that deliberately probes broken servers. |
| Works | A function, class or module nothing reachable from the entry points uses. | Delete it, or list it in keep if something loads it by name. | Code loaded by name (plugins, entry points of other tools); add those to keep. |
| Works | Only tests reach this code; the product never does. | Delete it with its tests, or call it from the product. | Test helpers kept in the package on purpose. |
| Works | getattr/globals/importlib with a computed name, or a string registry, could reach it. | List it in keep if it is loaded by name; otherwise delete it. | It is reported as uncertain precisely because the parse cannot tell. |
| Works | A [project.scripts] (or setup.cfg console script) target module:function is not in the tree. | Point the script at an existing module:function. | Modules generated at build time. |
| Works | An absolute import that is not in the standard library, not in the repo and not a declared dependency. | Declare the distribution that provides it, or guard an optional import with try/except ImportError. | Packages pulled in only by another dependency (they work by accident); import names the mapping table misses. |
| Works | from .x import y points at a module (or a package name) the repo does not have. | Fix the module path or add the missing module. | Modules generated at build time. |
| Works | ast.parse rejects the file, so it cannot be imported or run (INFO when the project needs a newer Python than the one scanning). | Fix the syntax error; port Python 2 code to Python 3. | Files kept on purpose for another interpreter, or templates with a .py name. |
| Works | Test functions with no assert, no pytest.raises/warns, no self.assert* and no call to an asserting helper. | Assert on the result; use pytest.raises for expected errors. | Smoke tests that only check nothing raises; helpers defined in another file. |
| Works | There are no test files at all. | Add a tests/ folder with at least one test per entry point. | Tests kept in another repository. |
| Complete | No CI configuration was found. | Add a CI workflow that runs the tests on each change. | CI configured outside the repository. |
| Complete | The code reads environment variables but there is no .env.example listing them. | Add a .env.example with every variable and a safe placeholder. | Libraries that read only optional, well-known variables (NO_COLOR, TERM). |
| Complete | There is no LICENSE, LICENCE, COPYING or UNLICENSE file at the root. | Add a LICENSE file. | Licences kept only in a subfolder. |
| Complete | Packaging metadata exists but declares no licence. | Add license = "..." (or license-files) to [project]. | Licences declared only by a build plugin. |
| Complete | The repo is an application (a Dockerfile, a main.py without scripts, or a web app object) and has no lockfile. | Commit a lockfile (uv lock, poetry lock or pip-compile). | Libraries are never asked for one; an example app outside examples/ can make a library look like an app. |
| Complete | pyproject.toml [project] lacks name, version (or dynamic) or requires-python. | Add the missing keys to [project]. | None known. |
| Complete | There is no README at the root. | Add a README.md that says what it is, how to install it and how to use it. | README kept under docs/ only. |
| Complete | A README of 200+ bytes with no install heading or no usage heading. | Add an Install heading and a Usage heading. | Headings worded differently from the words the check knows. |
| Complete | The README is under 200 bytes. | Describe what it does, how to install it and how to use it. | A README that only links to full documentation. |
| Complete | A runtime dependency nothing in the repo imports. | Remove it, or note why it is needed (a plugin or a runner). | Plugins and runners loaded by other tools; INFO, never scored. |
Goal contract
code-guardian.goal.toml at the project root (optional) says what the project is for and where it starts:
goal = "One sentence: what this project is for."
entry_points = ["pkg.cli:main", "pkg.server:app"] # module:function, or module:__main__ for its main block
keep = ["pkg.plugins.*"] # loaded dynamically; never reported
exclude = ["tests/fixtures/*"] # path globs the scan skips (the report counts them)code-guardian init # writes a starter file with the inferred entry points; --force replaces one
code-guardian orphans . # hygiene onlyWithout a contract, entry points are inferred from [project.scripts] (and setup.cfg console scripts),
if __name__ == "__main__": blocks, __main__.py files and __all__ of top-level packages, and the report says
"inferred". With no entry points at all, nothing is reported.
The call graph is built with ast from imports, from x import y, attribute use on imported modules and method calls.
Anything not reachable from an entry point is an orphan:
code | meaning | level |
| nothing reachable uses it | WARN |
| only tests reach it | INFO |
|
| INFO |
A method counts as used when its class is used and its name is read as an attribute anywhere reachable; a class with a
base outside the project (a framework) keeps all its methods. A def wrapped in a registering decorator (@app.get,
@click.command, @pytest.fixture) and a module-level __getattr__ are reached with their module, because the
decorator or the interpreter calls them. For a library (inferred entry points, packaging metadata and no
application marker), the public API counts as entry points: every public module, its public functions and classes,
their public methods, and everything a public module re-exports (from ._client import Client,
from ._transports import *); the hygiene source then reads "inferred (library public API)". Orphans go into the hygiene section and never touch
the Safe score.
--explain
CODE_GUARDIAN_LLM_URL=http://127.0.0.1:11434/api/chat CODE_GUARDIAN_LLM_MODEL=qwen3:8b code-guardian scan . --explainCODE_GUARDIAN_LLM_URL is an Ollama /api/chat URL or any OpenAI-compatible /v1/chat/completions URL, detected
by path (a bare host means Ollama, a .../v1 base means OpenAI-compatible). Optional: CODE_GUARDIAN_LLM_KEY
(bearer token), CODE_GUARDIAN_LLM_TIMEOUT (seconds, default 60).
Guarantees, each covered by a test against a stub model on loopback:
It never changes a score. The report with
--explainequals the report without it, plus one block labelledmodel explanation (not part of the score)and one note line.It sends findings only:
code,level,file:lineand the ≤120-character summary, plus the scores, grade and hygiene counts. No source code, no evidence lines, no secret values.It talks to one URL: the one you configured, directly (no proxy). The scan itself never touches the network.
It fails soft: with the URL unset or unreachable, the report is unchanged and one note line says why.
MCP server
code-guardian-mcp (or code-guardian mcp) speaks MCP over stdio with three read-only tools, each with a title
and readOnlyHint: true, destructiveHint: false, idempotentHint: true:
tool | does |
|
| the full report for | false |
| the hygiene section for | false |
|
| true |
A path argument is resolved and must exist. Claude Code:
claude mcp add code-guardian -- uvx code-guardian-mcp mcpAny MCP client:
{
"mcpServers": {
"code-guardian": {
"command": "uvx",
"args": ["code-guardian-mcp", "mcp"],
"env": {"CODE_GUARDIAN_LLM_URL": "http://127.0.0.1:11434/api/chat", "CODE_GUARDIAN_LLM_MODEL": "qwen3:8b"}
}
}
}The env block is only needed for explain.
Limitations
Only Python is analysed. Manifests (
package.json,setup.py,setup.cfg,pyproject.toml,requirements*.txt) are read for install hooks and pins; every other language gets the secret scan only, and the report says which files.Static analysis cannot see runtime behaviour. A finding means the code is there, not that it runs; code that builds its behaviour at runtime (downloaded plugins,
execof fetched text) is flagged where the parse can see it and invisible where it cannot. A grade A is "nothing the scanners recognise", not "proven safe".Python files are parsed by the interpreter running code-guardian. Run it with a Python at least as new as the project's
requires-python; when it is older, a parse failure is reported asworks.syntaxINFO, not FAIL.Taint tracking for
exec.shellFAIL is one function deep: assignments inside the same function, not calls between functions.The orphan graph over-approximates "used" (a method name read anywhere keeps every reached class's method of that name), so it reports less dead code rather than wrong dead code.
.gitignoresupport is simple globs from the root file; negation (!) and nested.gitignorefiles are ignored.
Every module tests itself
python -m code_guardian.scanners --selftest
scanners selftest: 18 checks, 18 passed, 0 failedEach of finding, walk, scanners, manifests, score, goal, orphans, works, complete, supply,
explain, report, cli and mcp_server has one; tests/test_selftests.py runs them all.
License
MIT. See LICENSE.
Available Tools
3 toolsexplainExplain the scan in plain EnglishARead-onlyIdempotent
Runs scan, then asks the local model at CODE_GUARDIAN_LLM_URL for a plain-English summary. Sends the findings only (no source). The score never changes; without a model it returns a note.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | goal contract file; default PATH/code-guardian.goal.toml if present | |
| path | Yes | the project directory to read |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations by disclosing the external dependency (CODE_GUARDIAN_LLM_URL), the data egress boundary (findings only, no source), that the score is not modified, and the degradation path (returns a note without a model). These are exactly the behavioral traits an agent cannot get from readOnlyHint/idempotentHint.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with the core action and followed by the two constraints that matter (what is sent, what happens without a model). No filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description still conveys the return shape (plain-English summary, or a note when no model is configured) and the safe-read nature. Minor gap: it doesn't hint at summary length/format or latency from the model call.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% with only 2 params, so the schema already carries the parameter semantics (including the goal-contract default). The description adds no parameter-level detail, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb+resource: it runs the scan and asks a local model for a plain-English summary of the findings. This implicitly distinguishes it from the sibling `scan` (raw scan output vs. narrated explanation), though it never names the sibling explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied rather than stated: an agent can infer this is the 'explain the scan' path versus the plain `scan` sibling, but there is no explicit when-to-use/when-not or named alternative. Adequate but leaves selection logic to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
orphansFind orphan codeBRead-onlyIdempotent
Functions, classes and modules nothing reachable from the project's entry points uses: dead, test-only or uncertain (dynamic dispatch). Judged against the goal contract when there is one.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | goal contract file; default PATH/code-guardian.goal.toml if present | |
| path | Yes | the project directory to read |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so the safety profile is covered. The description goes further by disclosing the result taxonomy (dead, test-only, uncertain via dynamic dispatch) and that judgment is scoped to the goal contract, which is real added context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single dense sentence that front-loads the definition of an orphan and layers qualifications after it. Every clause earns its place, though the phrasing is compressed to the point of being slightly hard to parse.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only analysis tool with annotations covering safety and no output schema, the description is adequate but not rich: it omits what the result list looks like at a summary level and how it relates to the sibling analysis tools. It is complete enough to invoke but not to choose confidently.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so both parameters are documented in the schema itself. The description reinforces the goal-contract concept that the goal parameter controls, but adds no syntax, default, or format detail beyond the schema. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific analysis task and even defines its core term: code nothing reachable from entry points uses, split into dead, test-only, or uncertain categories. It is clear what the tool produces, but it never distinguishes itself from the scan or explain siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no statement of when to run this versus scan or explain, and no prerequisites beyond the implicit 'point it at a project'. The clause about the goal contract hints at behavior but is not usage guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scanScan a codebaseARead-onlyIdempotent
Is it safe to run, will it work, does it have what it needs? Safe, Works and Complete scores (0-100, higher is better), Overall and an A-F grade, every finding with file:line, the egress inventory and orphan-code hygiene. Deterministic; parses only, never runs the code.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | goal contract file; default PATH/code-guardian.goal.toml if present | |
| path | Yes | the project directory to read | |
| fail_on | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive/closed-world, so the safety profile is covered. The description adds real value on top: it is deterministic, it parses but never executes the target code, and it enumerates the report contents. It does not, however, explain what happens when fail_on trips or whether a run can fail the call.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense but front-loaded: the output claims (Safe/Works/Complete, grade, findings with file:line) come first and the determinism guarantee last. The opening rhetorical question is slightly promotional, but no sentence is wasted and it stays within one compact block.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, so the description carries the return-value burden and does so well, listing scores, grade, findings, egress inventory and orphan hygiene. The remaining gap is operational rather than descriptive: fail_on thresholds and the goal-contract default are undocumented, so an agent cannot predict the call's failure mode.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 67% schema coverage, the schema documents path and goal adequately, so the baseline is 3. The description adds no parameter-level meaning at all and leaves fail_on (INFO/WARN/FAIL) unexplained, which is the parameter whose semantics an agent most needs to choose correctly.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The title plus description make it clear this reads a project directory and emits safety/workability/completeness scores, a grade, and file:line findings. It is specific about the resource and the output shape, but never distinguishes itself from the 'orphans' and 'explain' siblings, even though 'orphan-code hygiene' overlaps with 'orphans'.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no when-to-use guidance: nothing says when to run scan versus orphans or explain, whether it should precede or follow them, or what the goal contract changes. The only usage-adjacent hint is that it 'parses only, never runs the code', which is behavioral rather than situational.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.2.0- First observed
explain - First observed
orphans - First observed
scan
TDQS
Scored across 3 tools
scan and orphans overlap since scan already reports 'orphan-code hygiene' while orphans drills into the same dead-code data, and explain is a wrapper that reruns scan. The descriptions do clarify the distinctions (drill-down vs. narrative), but boundaries are not fully crisp.
All three names are single lowercase words with a consistent style (scan, orphans, explain), which is readable and predictable. There is minor inconsistency in word class (two verbs vs. one noun) and no verb_noun pattern, but the convention is uniform enough.
Three tools is slightly thin but each earns its place: a primary scan, a focused drill-down, and an LLM summary mode. The surface fits the narrow, well-defined purpose of static safety scanning without redundancy.
For a parse-only safety/completeness scanner, the set covers the core workflow (assess safety, inspect dead code, get a plain-English explanation) with graceful degradation when no model is available. Minor gaps exist around configuration/dependency analysis, but core needs are met.
Related MCP Connectors
Security + bug + perf + refactor audit for Python. Returns 0-10 score + MD report.
Scan any public GitHub MCP-server repo for security issues. 37 MCP-specific L1 rules, 8 languages.
Audit GitHub repos for malicious and supply-chain code before you depend on them.
Audit and repair any public GitHub repo. Free survey; paid source, unified diffs and a pull request.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceSecurity scanner for MCP servers and AI-generated code. Detects leaked API keys, PII, prompt injection, and MCP misconfigs with A-F security grades.MIT
- AlicenseAqualityCmaintenanceLocal-only GitHub Actions and CI maintenance scanner for AI-built apps. Exposes scan, explanation, and fix-planning tools to MCP clients; modifies nothing and makes no outbound requests by default.335 npm2MIT

repo-doctorofficial
AlicenseNot gradedqualityCmaintenanceMCP server that produces scored, evidence-cited audits of public GitHub repos via tools for fetching metadata, reading files, scanning git history, and checking hygiene.MIT- AlicenseAqualityAmaintenanceFree deterministic security scan of a public git repository (GitHub, GitLab, Codeberg, Bitbucket): vulnerable dependencies via OSV.dev, secret patterns, and config lint, returned as structured JSON. Tools: scan_repository(url), audit_pricing(). Runs locally over stdio (python3 web/mcp_stdio.py or the Dockerfile) and is also hosted at https://project-feldspar.com/mcp. MIT licence, stdlib-only. Buil2MIT