Skip to main content

valvur

A fully offline security scanner for AI-generated code. Your source never leaves your machine — and you can prove it.

Status: 0.3.0 — published and installable; v1.0.0 follows the usability gate. pip install valvur · ghcr.io/maverickhq/valvur


Why this exists

Every serious code-security scanner sends your code — or metadata about it — to someone else's servers. For regulated industries and data-residency jurisdictions that is not a preference to negotiate; it ends the procurement conversation. valvur runs entirely on your machine, with networking switched off and your source mounted read-only.

At the same time, AI now writes a fast-growing share of production code, and it fails in ways classic scanners were never built to catch: hallucinated dependencies that attackers pre-register, poisoned agent instruction files, hidden Unicode directives.

Offline scanning, built for how AI-generated code actually breaks.

The three claims

This is the introduction. docs/EVALUATING.md is the audit — the measured first run, the verification commands, the three statuses, and a plain list of what valvur does not claim. Read that one sceptically.

1. It cannot exfiltrate your code — and you can verify it

No account, no API key, no telemetry: nothing to opt out of. On the default offline profile the scanners run in containers with --network=none, and the host process that launches them opens no socket either. Both halves are checked by one command:

python3 scripts/verify-offline.py /path/to/your/repo

On Linux the OS can deny the whole process tree the network, no privileges needed: unshare -rn valvur scan --profile offline. Every run records in run.json exactly what left the machine — on offline, the word nothing.

2. Security checks built for AI-generated code

  • Hallucinated dependencies (slopsquatting). LLMs invent package names; attackers register them. No advisory database can catch it — the package is new, not known-bad. valvur checks that every declared dependency exists, offline, against a local index of every name on PyPI, npm, RubyGems, Packagist and crates.io (6.3 million names, exact, published daily and signed). On full it also asks how old each one is, whether it is one edit from something popular, and — for npm, whose download counts are public — whether a package under 90 days old has under 1,000 downloads a month: new and unadopted, the slopsquat signal itself, reported at high. PyPI publishes no counts without a third party, so there it is age alone.
  • Agent-config auditing. CLAUDE.md, AGENTS.md, .cursorrules, .mcp.json, skills and prompt files, and the clients' own folders — .kiro/ (steering, MCP settings, hooks), .claude/, .cursor/, .roo/, .continue/, .clinerules, .aider.conf.yml — scanned for injected directives, hidden Unicode (zero-width, bidi, tag characters), unpinned @main MCP refs, blanket autoApprove and permission-bypass flags, and hooks that run a shell command on an event: a committed Kiro hook, Claude Code hook or aider lint-cmd makes every agent that opens the repository execute it, and is reported at high with the command as fenced evidence.
  • A small set of Opengrep rules, which are not the claim. Two pinning rules (tag-pinned actions, mutable git refs) that fire on most real repositories; a sink inventory at INFO — eval, exec, shell=True, unsafe yaml.load, string-built SQL — which is what fires on real code; and four taint rules from a model call to those sinks and to innerHTML, with sources for the Anthropic, OpenAI (chat completions, Responses API, pre-1.0), Gemini, LangChain, litellm and ollama SDKs. The taint rules fire on every planted flow in our fixture (fifteen, across those six families) and have never fired on real code: measured on thirteen real repositories, including two that execute model output — smolagents and pandas-ai route it across a class boundary, and Opengrep's taint tracking is intra-procedural, so it does not see the flow while the inventory names both exec sites. They ship ranked low; the checks above carry this section.

What is covered, and what is not, is stated on every scan rather than left to infer:

ecosystem exists? known CVEs?
Python, npm, Ruby, PHP, Rust offline, from the index needs requirements*.txt, uv.lock, poetry.lock / a lockfile
JVM, Go full only — no offline index exists for either registry pom.xml, go.mod on their own

A manifest with no lockfile beside it, or a manifest nothing here reads (a lone Pipfile, say), is a coverage note: the run reads inconclusive rather than clean, and names why. That reporting is the part we consider non-optional. A licence valvur could not read — dependency licences absent from a lockfile, a LICENSE no signature matches — is a note too, listed and counted, but it casts no doubt: a security verdict of clean stays clean over it, and no gate threshold sees it.

3. Ten things that matter, not four hundred findings

Findings are ranked by whether attackers are actually exploiting them — CISA KEV (including the ransomware-campaign flag) and FIRST EPSS — not by CVSS theatre. Development-only dependencies are demoted; transitive vulnerabilities come with the path and the direct package to bump.

Finding CVSS EPSS KEV Severity-sorted Ranked here
CVE in a dev-only test library 9.8 CRITICAL 0.04% No #1 #40
CVE in your production web framework 6.5 MEDIUM 92% Yes #40 #1

For AI coding agents — the primary way in

Add valvur to your agent's MCP configuration:

{ "mcpServers": { "valvur": { "command": "uvx", "args": ["--from", "valvur", "valvur-mcp"] } } }

Then ask it to scan. The server is stdio only — no listener, no port — and every tool it exposes is read-only: valvur can never change your code. Nothing has to run first: a first scan pulls the image, the vulnerability database and the name index itself and says so on scan_status — measured 2026-09-13 from an empty machine, 110 seconds to a complete result, one tool call.

Add this to your project's CLAUDE.md or AGENTS.md, so the agent uses what it has:

## Security scanning
This project uses valvur. Scan with the `valvur` MCP tools if you have them:
call `scan`, then `scan_status` until it reports DONE, then follow its `Next:`
lines; if it reports FAILED, call `doctor` and relay what it says. Otherwise
run `valvur scan`. Results
appear in `.security-scan/`: read SUMMARY.md, then REMEDIATION.md. Never
commit `.security-scan/`. Never add suppressions without explicit human
approval. Propose fixes for approval — do not apply them and rescan
autonomously.

For developers

pip install valvur          # or: uv tool install valvur
valvur update               # the image, the vulnerability database and the name index, once
valvur scan                 # offline by default; --profile full adds the networked checks
valvur doctor               # if anything above did not work: what this machine is missing, and the fix

The first valvur update pulls the image (about 240MB), the vulnerability database (118MB) and the name index (34MB, one signed artifact, built daily) — a minute or two. Later updates take seconds; run valvur update --if-stale from a hook or cron, it costs one file read when current. It is optional before the first scan: a scan that finds any of the three absent fetches it and says so — on the terminal, and over MCP on scan_status, "fetching the vulnerability database (119MB) — the first run only" — rather than sitting silent or failing. A stale one is never refreshed by a scan; the warning stands and you decide. If the published index cannot be reached, valvur update walks the five registries directly instead, which takes about seven minutes once; a scan does not.

Results land in .security-scan/:

.security-scan/
├── SUMMARY.md          ← start here. Bounded, leads with anything that failed
├── REMEDIATION.md      ← ranked proposal, with dependency paths and upgrade targets
├── findings.json       ← complete, normalised, schema-versioned
├── results.sarif       ← SARIF 2.1.0 for your IDE
├── sbom.cdx.json       ← CycloneDX SBOM
├── run.json            ← what ran, which versions, how long each took, what was skipped and why
└── raw/                ← untouched per-tool output, so you can verify us

The folder ignores itself, so results are never committed. Secrets are redacted in every artifact, raw/ included. You decide which fixes to apply and when to rescan — there is no autonomous loop. Suppressions (with mandatory expiry dates) and [scan] exclude paths live in a committed .security-scan.toml, and every exclusion is reported with what it cost.

In CI, valvur scan exits zero whenever the scan itself worked — findings are the job, not a failure — and valvur gate turns the result into one exit code:

valvur scan . && valvur gate . --fail-on high --no-inconclusive

In GitHub Actions that is one line — MaverickHQ/valvur-action installs the shim, fetches and caches what a scan needs, scans, uploads results.sarif to code scanning and runs the gate; this repository's own release gate uses it on every commit:

- uses: MaverickHQ/valvur-action@v0
  with: { fail-on: high, no-inconclusive: "true" }

The gate fails on an incomplete run, on a lapsed suppression, on an active finding at or above --fail-on (any is every one; it is what valvur's own release gate uses), and with --no-inconclusive on a scan whose data was too old to be evidence or that never inspected part of the tree. Under GitHub Actions each reason is an annotation. valvur cache says what is on disk, how old and how large; valvur cache --clear removes it.

What actually does the scanning

valvur builds no detection engine. Six open source scanners do that and deserve the credit; valvur adds orchestration, one findings model, exploit-aware ranking, and the checks under claim 2.

Tool Licence Does
Trivy Apache-2.0 Dependency vulnerabilities
Gitleaks MIT Secrets, including git history
OSV-Scanner Apache-2.0 Dependencies against OSV.dev (full only: it sends lockfile names and versions out). Measured on twelve real repositories: 121 Go standard-library advisories on the one Go project, keyed on go.mod's go directive, which Trivy reports only from binaries; 1 disputed advisory on a Python project; 0 on the other ten; and on a thirteenth, 110 against the lower bounds of an unpinned requirements.txt — versions nobody installs, dropped with the count since 25.3 (the file is a coverage note instead). About a second a scan.
Opengrep LGPL-2.1 Static analysis
Checkov Apache-2.0 Infrastructure misconfiguration
Syft Apache-2.0 SBOM, and the dependency licences read from it

Exploit intelligence comes from CISA KEV and FIRST EPSS — public primary sources, auditable and mirrorable. No proprietary database; nothing to lock you in.

What it deliberately does not do

  • No reachability analysis. We do not prove a vulnerable function is called.
  • No autonomous fixing. You choose the fixes.
  • No penetration testing. No DAST, no exploitation, no scanning of deployed systems.
  • No code-quality analysis. Security only.
  • We will not out-detect commercial SAST. We win on trust, breadth in one artifact, and prioritisation — not on engine depth.

Platforms

macOS, Linux — Docker or Podman Supported, tested on every commit against both runtimes
linux/amd64 and linux/arm64 Both, from 0.2.0. 0.1.0rc1 was published arm64 only — a defect, not a policy
Windows via WSL2 Supported — inside WSL valvur is running on Linux
Native Windows Not claimed. Untested, and valvur says so at startup
SELinux-enforcing hosts (RHEL, Fedora) Supported, with one deliberate friction: valvur will not relabel your source tree unless you set VALVUR_SELINUX_RELABEL=1. Details in EVALUATING.md

Air-gapped? The database, the name index and KEV all live outside the image and each has a mirror setting, measured end to end. See docs/AIR-GAPPED.md.

Docker Desktop's memory. A scan starts eight Scanner containers at once; the fleet peaks around 500 MB on Linux, but Docker Desktop's VM has its own limit, and a container it cannot fit is killed with exit 137 and reported as a failed Scanner. valvur scan --jobs 2 runs two at a time; VALVUR_JOBS=2 in the MCP server's environment does the same for an agent. A scan an agent no longer wants is stopped with the scan_cancel tool — containers killed, nothing written — as Ctrl-C does on the command line.

Contributing, and reporting problems

A finding you disagree with — especially one valvur missed — is a bug worth reporting. CONTRIBUTING.md has the setup and the short list of things refused on principle; SECURITY.md is for suspected vulnerabilities, which for a security tool include a false clean result; CHANGELOG.md is what changed.

Licence

Apache-2.0 — see LICENSE; chosen over MIT for the explicit patent grant. Bundled scanners keep their own licences, listed above. valvur adds no GPL or AGPL component; the Alpine base carries GPL userland as every Linux container does, and the published SBOM discloses all of it (ADR-0005).

Release files for valvur 0.3.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 valvur 0.3.0
File Size Uploaded
valvur-0.3.0.tar.gz 869.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for valvur 0.3.0
File Interpreter ABI Platform
valvur-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.1 MB

Release files / valvur-0.3.0.tar.gz

Download URL valvur-0.3.0.tar.gz
Size 869.1 kB
Tags Source
SHA-256 checksum
How to use checksums
731e1539229f7c7fd1696b82fe6b7c4a8518c615c38ecf6f6ac1c7569277a6b4
BLAKE2b-256 checksum
How to use checksums
2a4f8f5b122b4438ad088ded1689b6511d7eb7d4f990bda83efbd1b087bf5d35
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 Sep 20, 2026.

Transparency log

Release files / valvur-0.3.0-py3-none-any.whl

Download URL valvur-0.3.0-py3-none-any.whl
Size 223.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ac1b168a2bb2fd3777557151a4ef8850d2c39c4654515d3dfd39a15230ad1572
BLAKE2b-256 checksum
How to use checksums
3ec02373b2e162d88b15c41ec73078a61fecdea3e9e50166479836a6b017bb1d
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 Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

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