Server data from the Official MCP Registry
A deterministic A-F grade for a repo without running it, plus a verify loop: plan, fix, diff.
About
A deterministic A-F grade for a repo without running it, plus a verify loop: plan, fix, diff.
Security Report
Valid MCP server (1 strong, 1 medium validity signals). No known CVEs in dependencies. Package registry verified. Imported from the Official MCP Registry.
13 files analyzed · 1 issue found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Required
This plugin requests these system permissions. Most are normal for its category.
What You'll Need
Set these up before or after installing:
Environment variable: CODE_GUARDIAN_LLM_URL
Environment variable: CODE_GUARDIAN_LLM_MODEL
How to Install
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-luminarisoftwares-code-guardian": {
"env": {
"CODE_GUARDIAN_LLM_URL": "your-code-guardian-llm-url-here",
"CODE_GUARDIAN_LLM_MODEL": "your-code-guardian-llm-model-here"
},
"args": [
"code-guardian-mcp"
],
"command": "uvx"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
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.
- A verify loop for AI-assisted changes: run it before and after every change to learn what the change fixed, what it broke, what to fix next, and whether the scan itself can be trusted (see The verify loop).
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.3.1 is the current release. The fourth guardian,
next to stack-guardian,
context-guardian and
tool-guardian.
Install
pip install code-guardian-mcp
The 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 -q
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_repo
Real output:
code-guardian 0.3.1 scan of risky
files: 7 Python, 3 manifests, 2 other (0 skipped, 0 excluded by the goal file)
status: COMPLETE
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 at deploy/id_rsa:1, risky/settings.py:1, risky/settings.py:2
-40 Safe obfuscation FAIL x1 at risky/loader.py:7
-30 Safe exec.shell FAIL x1 at risky/runner.py:7
-25 Safe tls.off FAIL x1 at risky/network.py:11
-23 Safe install.hook WARN x3 at package.json:5, setup.py:5, setup.py:14
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 · privacy.pii_log WARN 5 · privacy.pii_prompt WARN 5 · secret.history FAIL 30 ·
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.commented_code INFO 0 · works.dead_flag INFO 0 ·
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 · works.todo_stub WARN 3
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 model
Exit 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 --fail-on (default FAIL), and the scan is COMPLETE |
| 1 | findings at or above it |
| 2 | usage error, a missing PATH, or a bad goal file |
| 3 | incomplete scan: the status is PARTIAL or INVALID (see Scan status) and nothing is at or above --fail-on |
scan gates on every scored finding (Safe, Works and Complete); code-guardian orphans PATH gates on the hygiene
findings.
The verify loop
code-guardian answers four questions about a change, so a developer or a coding agent can run it before and after every edit:
| question | command | MCP tool |
|---|---|---|
| Can I trust this scan? | every report's status: COMPLETE, PARTIAL (grade capped at B) or INVALID (no grade); exit 3 | scan |
| What should be fixed next? | code-guardian plan PATH (each check row's estimated gain if fixed, best first) | plan |
| What did the change fix, and what did it break? | code-guardian diff BEFORE.json AFTER.json (exit 1 when anything regressed) | diff |
| Is this finding new? | code-guardian baseline PATH, then scan --baseline FILE (gate on new findings only) | scan with baseline |
An agent's loop, from the shell:
code-guardian scan . --json > before.json # 1. scan: is the scan COMPLETE? what is the grade?
code-guardian plan . --top 5 # 2. plan: the five fixes worth the most points, with locations
# 3. fix the top item (the plan never edits code)
code-guardian scan . --json > after.json # 4. scan again
code-guardian diff before.json after.json # 5. diff: verdict improved / regressed / mixed / unchanged
verdict: improved (1 fixed, 0 new, 31 unchanged)
before after delta
Safe 0 0 +0
Works 85 85 +0
Complete 35 55 +20
Overall 33 37 +4
grade F F
status COMPLETE -> COMPLETE
fixed (1):
WARN complete.license_missing . no licence file: nobody may legally reuse this code
new (0):
none
matched by fingerprint (code, level, file and the evidence line; line numbers ignored); INFO findings never decide the verdict
The same loop over MCP is scan → plan → edit → scan → diff(before, after), where before and after are the
two scan replies (JSON text) or paths to scan --json files. Findings are matched by fingerprint (code, level,
file and the evidence line, never the line number), so moving code does not count as a change. Everything in the
loop is deterministic, offline and read-only.
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.
- Every deduction cites its evidence. Each check row lists every distinct
file:lineit was counted at (sorted, at most 20, plusmore: how many were left out); the text report shows up to three per row. - Nothing secret is printed. Every output (JSON, text, Markdown, SARIF, MCP) goes through one
redact(): API-key shapes and email addresses are masked as<redacted key>/<redacted email>wherever they appear (a URL literal, a path), and the evidence ofsecret.*andprivacy.*findings is also stripped of any long token-like value.
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.
Scan status
Can I trust this scan? Every report carries a status, the reasons for it, and the outcome of every scanner,
check and the file walk (ran, skipped with a reason, or crashed with the error):
| status | when | effect |
|---|---|---|
COMPLETE | everything ran | none |
PARTIAL | a check crashed or was skipped, a Python file did not parse, or a Python file or manifest was too large (over 2 MB) or unreadable | the grade is capped at B and the report says why; exit 3 unless a finding already gives exit 1 |
INVALID | PATH is unreadable, or there are no Python files and no manifests to judge | no grade (scores.grade is null); exit 3 |
Binary files, symlinks and large non-source files (images, design files) are only ever secret-scanned; skipping them is listed in the notes but does not make a scan PARTIAL.
JSON shape (--json, and the MCP scan tool)
{
"tool", "version", "schema_version": 2, "path", "goal_file",
"status": "COMPLETE" | "PARTIAL" | "INVALID", "status_reasons": [...],
"outcomes": [{"check", "outcome": "ran" | "skipped" | "crashed", "reason"}],
"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",
"locations": [every distinct file:line, sorted, at most 20], "more": N}]}},
"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",
"fingerprint", "new" (only with --baseline)}],
"baseline": {"file", "version", "new", "known"} (only with --baseline)
"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. schema_version 2 only adds
keys to the v0.2 shape (which had no schema_version); every v0.2 key keeps its meaning. scores.grade (and
grade_uncapped) is null for an INVALID scan; capped and cap_reason also cover the PARTIAL cap. SARIF carries
status and status_reasons in the run properties; the text and Markdown reports print them under the scores.
Safe checks
| code | what it looks for | level | weight |
|---|---|---|---|
secret.key | API-key shapes (the privacy workflow's built-in list: sk- (which covers Anthropic's sk-ant-), sk-or-, tvly-, ghp_, github_pat_, nvapi-, gsk_, AIza; plus AWS access key IDs AKIA/ASIA + 16, Stripe sk_live_/rk_live_, Slack xox[abprs]-, Hugging Face hf_ + 30 letters), a 40-character AWS secret key assigned to a name containing aws and secret, private-key blocks, password = "<literal>" (also api_key, secret, access_token…), and any *_KEY / *_TOKEN name holding a long, high-entropy literal. One finding per line. Values are never printed; values that call themselves fake (mock, dummy, example…) are skipped | FAIL | 40 |
secret.key | a Stripe test-mode key (sk_test_) | INFO | 0 |
secret.history | opt-in, scan --history: a key shape (the secret.key API-key shapes, AWS secret keys, private-key blocks) that a commit added and the file no longer has; it may still be valid. Names the short sha and the path, never the value | FAIL | 30 |
exec.dynamic | eval, exec, compile on non-literal input; __import__ with a variable | WARN | 10 |
exec.dynamic | …when eval or exec runs text from input(), sys.argv, request data, os.environ or a network read (the exec.shell taint rule, one function deep; exec(compile(...)) counts at the exec): "runs code typed by the user" | FAIL | 30 |
exec.shell | os.system, os.popen, subprocess.*(..., shell=True) | WARN | 10 |
exec.shell | …when the command is built from input(), os.environ, sys.argv or a network read | FAIL | 30 |
deser.unsafe | pickle.load(s), marshal.loads, yaml.load without SafeLoader, torch.load without weights_only=True | WARN | 10 |
net.egress | every URL and IP literal plus every urllib/requests/httpx/socket.connect call site: the egress inventory | INFO | 0 |
net.egress | a publicly routable IP, or plain http/ws/ftp to a public name or to a host chosen at runtime | WARN | 2 |
net.bind_all | binding 0.0.0.0 or :: | WARN | 5 |
tls.off | verify=False, ssl._create_unverified_context, CERT_NONE, check_hostname = False, hard-coded | FAIL | 25 |
tls.off | …only under an if on a function parameter: the caller opts out (how httpx implements verify=False) | WARN | 10 |
install.hook | setup.py cmdclass or install-command subclasses, package.json preinstall/install/postinstall, an in-tree build backend | WARN | 15 |
obfuscation | exec/eval of base64/zlib/marshal/hex-decoded data | FAIL | 40 |
obfuscation | base64 or hex blobs of 200+ characters, high-entropy string literals | WARN | 10 |
fs.outside | writes, deletes and moves aimed at absolute paths, ~, or .. escapes | WARN | 3 |
privacy.pii_log | a personal-data name (email, phone, ssn, dob/birth, a person's address, passport, credit_card/card_number, iban, ip_address; whole words, so emails_sent and server_address do not count) flows into logging.*, a logger method or print (the exec.shell taint rule, one function deep) | WARN | 5 |
privacy.pii_prompt | the same names flow into an LLM prompt: a messages= or prompt= argument, or a .chat(, .complete( or .generate( call | WARN | 5 |
deps.unpinned | dependencies with no pin or upper bound (pyproject.toml, setup.py, setup.cfg, requirements*.txt, package.json) | 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.
Secrets in the git history (opt-in)
code-guardian scan . --history [--history-depth 500]
A key deleted from a file is still in every clone's history. --history runs one read-only git command (an argument
list, never a shell): git log -p --no-color --no-ext-diff --no-textconv --relative --format=%H --max-count=N -- .
with the scanned repo's fsmonitor and signature-checking config switched off, so the repo cannot make the scan run a
program. It checks only the lines commits added, with the secret.key key-shape rules, reads at most the last N
commits (default 500) and at most 64 MB of log, and never modifies the repository. A key still present in the current
file is left to secret.key; the oldest commit that added a value is the one reported. Without git, or when PATH is
not a repository, the history check is skipped with a note (and the scan is PARTIAL, since what was asked did not
run). The scan itself stays offline.
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 |
|---|---|---|---|
supply.unknown_package | offline (default): a declared dependency or an unresolved import that is not in the bundled list of known packages; run with --online to check it exists | INFO | 0 |
supply.unknown_package | --online: PyPI has no such project (likely invented by an AI assistant; anyone could register the name) | FAIL | 40 |
supply.unknown_package | --online: the project's first release is under 90 days old, or it has only one release | WARN | 10 |
supply.lookalike | 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 |
|---|---|---|---|
works.syntax | a .py file that ast.parse rejects (Python 2 code, a typo); it still gets the secret scan | FAIL | 30 |
works.syntax | …when the project's requires-python is newer than the Python running code-guardian (it cannot tell) | INFO | 0 |
works.import_unresolved | 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 |
works.relative_missing | a relative import (from .x import y) that points at a module the repo does not have | FAIL | 25 |
works.tests_missing | no test files at all (tests/, test_*.py, *_test.py, tests.py, conftest.py) | WARN | 15 |
works.test_no_assert | test functions with no assert, no pytest.raises/warns, no self.assert* and no call to a local helper that asserts; once per file, with the count | WARN | 5 |
works.entry_missing | a [project.scripts] (or setup.cfg console script) target module:function whose module or function is not in the tree | FAIL | 30 |
orphan.dead | dead code from the hygiene section (below), folded in at a low weight, only when entry points exist | WARN | 3 |
works.todo_stub | a function whose body is only pass, ... or raise NotImplementedError (a docstring allowed) and an entry point reaches it; not abstract methods, Protocol classes, @overload or a method a subclass in the repo overrides | WARN | 3 |
works.commented_code | 5+ consecutive comment lines that parse as Python (found with tokenize + ast.parse); once per file, with the count of blocks | INFO | 0 |
works.dead_flag | a module-level ALL_CAPS boolean (DEBUG = False) that no file reads, imports or names | INFO | 0 |
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 |
|---|---|---|---|
complete.license_missing | no LICENSE, LICENCE, COPYING or UNLICENSE file at the root | WARN | 20 |
complete.license_undeclared | packaging metadata ([project], Poetry, setup.cfg, setup()) declares no licence (license, license-files or a License :: classifier) | WARN | 5 |
complete.readme_missing | no README at the root | WARN | 20 |
complete.readme_short | the README is under 200 bytes | WARN | 10 |
complete.readme_sections | 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 |
complete.ci_missing | no CI configuration (.github/workflows/, .gitlab-ci.yml, .circleci/, azure-pipelines.yml, .travis.yml, Jenkinsfile…) | WARN | 10 |
complete.metadata | pyproject.toml [project] without name, version (or dynamic) or requires-python | WARN | 10 |
complete.lockfile | an application with no lockfile (uv.lock, poetry.lock, Pipfile.lock, pdm.lock, pixi.lock, requirements.lock, or a fully pinned requirements.txt) | WARN | 10 |
complete.env_example | the code reads environment variables (os.environ, os.getenv, dotenv) and there is no .env.example (or .env.sample, .env.template, example.env) | WARN | 10 |
deps.declared_unused | a runtime dependency that nothing in the repo imports (runners, linters, types-* and pytest-* plugins are skipped) | 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.
Baseline: gate on new findings only
An existing project rarely starts clean. Write a baseline once, commit it, and the gate then fails only on findings that are new since:
code-guardian baseline . # writes ./code-guardian.baseline.json; commit it
code-guardian scan . --baseline code-guardian.baseline.json --fail-on WARN
- Each finding gets a
fingerprint: sha256 ofcode | level | path | evidence line, where the evidence line is the source line the finding points at, stripped, whitespace collapsed and redacted. Line numbers are left out, so moving code up or down does not make a finding new; editing the line does. The baseline file holds the sorted fingerprints and the version that wrote them, nothing else. - With
--baseline, every finding carriesnew: true|false, the report saysN new, M known, and--fail-oncounts new findings only. A finding fixed since the baseline simply disappears;scannever rewrites the baseline file (runcode-guardian baselineagain to accept the current state). - A baseline never hides the real grade. The scores, the grade and every finding are computed and shown exactly as without one; only the exit code changes.
- Same line twice counts twice: a third copy of a known line is new.
- SARIF results carry the fingerprint as
partialFingerprints["codeGuardian/v1"]andnewin their properties.
Diff and repair plan
code-guardian scan . --json > before.json
code-guardian plan . # what to fix next: Markdown (--json for JSON), --top N rows (default 10)
# ...edit...
code-guardian scan . --json > after.json
code-guardian diff before.json after.json # what the change fixed and broke; exit 1 when anything regressed
diff BEFORE.json AFTER.json compares two scan --json reports (0.3 or newer) by fingerprint: fixed, new
and unchanged findings, per-dimension score deltas, and a one-line verdict: improved, regressed, mixed
or unchanged. A new WARN/FAIL finding, or a status that got worse (COMPLETE → PARTIAL → INVALID), is a
regression; INFO findings are listed but never decide the verdict. Exit 1 when anything regressed (regressed or
mixed), 2 when a report cannot be read.
plan PATH is the repair plan. For each check row it re-runs the scoring on the finding list without that row:
the exact Overall points the row is worth, labelled estimated gain if fixed (the plan never touches the code, so
fixing more or less than the whole row changes the real gain). Rows are ranked by that gain, ties by the fewest
locations, and each comes with the catalogue's "how to fix it" and every location. When a dimension is floored at
0 (more than 100 points deducted), no single fix in it moves Overall yet: its rows show +0 and the plan says how
many points must go first.
Outputs, GitHub Action and pre-commit
| flag | prints |
|---|---|
| (none) | the text report shown above |
--json | the full report (shape above) |
--sarif | 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 security-severity), one result per finding. Repo-level findings (no file) are attached to the root manifest or README, because code scanning needs a file for every result. Validated in CI against the official schema |
--markdown | 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.3.1
with:
path: .
fail-on: FAIL # or WARN
online: "false" # "true" asks PyPI about unknown dependency names
Inputs: path, fail-on, online, baseline (a committed baseline file: gate on new findings only),
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.3.1
hooks:
- id: code-guardian
# args: [--fail-on, WARN]
# or, with a committed baseline, fail only on new findings:
# - id: code-guardian-new
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.
Documentation truncated — see the full README on GitHub.
Reviews
No reviews yet
Be the first to review this server!
More Security MCP Servers
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
MCP Marketplace
Freeby mcp-marketplace · Developer Tools
Search and install MCP servers from inside your AI client.
mcp-creator-python
Freeby mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.
FinAgent
Freeby mcp-marketplace · Finance
Free stock data and market news for any MCP-compatible AI assistant.
by Lharries · Communication
Read, search, and send WhatsApp messages through your AI assistant
Memory
Freeby Modelcontextprotocol · Productivity
Knowledge graph-based persistent memory across sessions
