
Dependency-free static analyzer for zk circuit soundness bugs in o1js/Mina zkApps and Noir circuits
Community package:
o1js-scanis listed in the official o1js Community Packages directory.
Latest: 0.22.0 — a local MCP server lets Claude Code, Cursor and other AI coding assistants run the scanner while you write a zkApp:
pip install 'o1js-scan[mcp]'. It runs on your machine over stdio and uploads nothing. A scan that analyzed no files is an error, not a clean result, and a result with no findings says it is not a security guarantee. The core scanner still has no dependencies. See CHANGELOG.
A fast, dependency-free static analyzer for zk circuit soundness bugs in:
.ts / .js) — Kimchi circuits from @method bodies.nr) — Aztec's Rust-like ZK DSL (including aztec-nr-shaped patterns)The security-critical bugs usually aren't in the proving system — they're in the
application's own constraints: witnesses the prover controls but the circuit
never binds. o1js-scan is the under-constrained-signal scanner for Circom's
cousins in the Mina and Noir ecosystems.
See also: gnark-safety (gnark circuits), vk-guard (verification-key regression).
pip install o1js-scan
# or: pipx install o1js-scan
# or: npm install -D o1js-scan
o1js-scan path/to/zkapp # o1js + Noir (auto)
noir-scan path/to/circuits # same binary — Noir-friendly alias
noir-scan . --lang noir --fail-on high --sarif noir.sarif
Given a vault whose withdraw amount is a prover-controlled witness that is
never bound to on-chain state:
$ o1js-scan examples/vulnerable_vault.ts --include-examples
LOW O1JS_UNCONSTRAINED_RECIPIENT vulnerable_vault.ts:23 fn=withdraw Recipient `to` is prover-chosen in `withdraw`
HIGH O1JS_UNCONSTRAINED_WITNESS vulnerable_vault.ts:23 fn=withdraw Unconstrained witness `amount` flows to send_amount in `withdraw`
o1js-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)
$ echo $?
1
--include-examples is needed here only because the demo file lives under
examples/, which the path classifier downgrades by default so that a repo's
own sample code cannot fail its build. The same contract in your src/ reports
HIGH with no flag.
The HIGH finding is the drainable bug. The fixed contract
(examples/safe_vault.ts) drops it and exits 0, keeping only the informational
LOW on the prover-chosen recipient:
$ o1js-scan examples/safe_vault.ts --include-examples
LOW O1JS_UNCONSTRAINED_RECIPIENT safe_vault.ts:23 fn=withdraw Recipient `to` is prover-chosen in `withdraw`
o1js-scan: 1 finding(s) [1 low] in 1 of 1 file(s) — passes (--fail-on high)
$ echo $?
0
See examples/ for the o1js and Noir vulnerable/fixed pairs.
pip install o1js-scan
For an isolated global CLI install, use pipx:
pipx install o1js-scan
For Node/npm-based Noir, Aztec, or o1js app repositories, install the npm wrapper:
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high
The npm package is a thin wrapper around the same Python analyzer and requires
Python 3.8+ on PATH (python3 or python). Set O1JS_SCAN_PYTHON to choose a
specific interpreter.
Or from source:
git clone https://github.com/auditinfra-io/o1js-scan
cd o1js-scan
pip install -e .
No third-party Python dependencies. Python 3.8+. The noir-scan console script is
installed alongside o1js-scan (same entry point), including through the npm
wrapper.
# scan a directory (recursively; skips node_modules, target/, .git, …)
o1js-scan path/to/project
# Noir-only / o1js-only
noir-scan circuits --lang noir
o1js-scan src --lang o1js
# scan a single file
o1js-scan src/MyContract.ts
noir-scan src/main.nr
# machine-readable output for CI
o1js-scan src --json
# SARIF 2.1.0 for GitHub code scanning (writes o1js-scan.sarif by default)
o1js-scan src --sarif
noir-scan . --lang noir --sarif noir.sarif
# choose which severity fails CI (critical|high|medium|low|none; default high)
o1js-scan src --fail-on medium
# progressive/power-user gate (equivalent to --fail-on medium)
o1js-scan src --strict
# test code is excluded by default (both backends); opt back in
o1js-scan src --include-tests
# example code is downgraded to LOW by default; keep original severity
o1js-scan src --include-examples
# a path with no o1js / Noir source exits 2; opt out where that is expected
o1js-scan packages/ --allow-empty
o1js-scan --version
Exit code is 1 when a finding at or above the --fail-on level (default
high) is present and 0 otherwise — so you can drop it straight into CI.
With the default, a low/medium finding (including the informational recipient
rule below) does not fail the build; use --fail-on none to only report,
or --strict (a shorthand for --fail-on medium) to gate more strictly while
still treating low-severity findings as advisory. The two options are mutually
exclusive so CI configuration cannot be ambiguous. A missing scan path exits 2
with an error on stderr, so a typo can't silently pass CI as a clean run. So does
a path that exists but holds no o1js or Noir source the analyzer can read (for
example --lang noir pointed at an o1js project); pass --allow-empty where
that is legitimate. Every run
prints a one-line summary (counts by severity and the gate verdict) to stderr.
Test code is excluded by default — both backends. Tests deliberately build invalid values and bad transactions to prove the asserts reject them, so a finding there is the point of the test rather than a circuit bug. A file counts as test code when:
*.test.ts / *.spec.ts (and the .js/.jsx/.tsx/.mjs
/.cjs variants), or *_test.nr / test_*.nr;test/, tests/, __tests__/, spec/ or __mocks__/
directory;#[test] / #[test(...)]
attribute, or sits inside a mod test { … } / mod tests { … } block —
block-scoped, so a test module at the foot of a production file does not
silence the rest of it.Pass --include-tests to report them.