Open-source smart contract security CI for Foundry projects.
contract-guard-ci is a small, automation-friendly security workflow for EVM teams. It wraps deterministic tools first—Foundry, Slither, and later Echidna/Medusa—and uses AI only for triage/explanation, never as the sole source of truth.
- Do: PR-level smart contract security checks, diff-aware reporting, SARIF/JSON/Markdown outputs, team policy baselines.
- Do not: claim to replace human audits, hold user funds, run payment infrastructure, custody keys, or perform legal/compliance decisions.
- First customer: small EVM protocol teams using Foundry that need continuous pre-audit safety checks.
- CLI probes repository/tool readiness.
- GitHub Action runs on pull requests.
- Foundry tests and Slither static analysis are normalized into one report.
- Advisory invariant templates help teams start Foundry invariant testing without claiming audit completeness.
- Scaffold-only Echidna/Medusa hooks define local fuzzer commands without executing external fuzzers by default.
- False-positive review records generate auditable non-high baseline candidates.
- Local repo policy files define deterministic scan defaults without enabling private uploads or AI triage.
- Optional AI triage payloads start with deterministic redaction and never call external AI by default.
- Future: PR comments, AI-assisted triage, live fuzzer execution, paid team dashboard.
python3 -m venv .venv
. .venv/bin/activate
pip install -e .
contract-guard plan --repo . --jsonRun local smoke:
./scripts/contract_guard.sh smokeIf you maintain or review a Foundry/EVM codebase, feedback is welcome in the public design-partner issue: #1.
Start with docs/design-partner-feedback.md for safe trial commands and what not to share publicly. Please do not post private source snippets, secrets, raw private-repo logs, or vulnerability claims about third-party repositories.
For a public-safe local beta flow:
- Use
docs/beta-install.mdfor local install, dry-run scans, GitHub Actions outputs, baseline/policy setup, and beta limitations. - Run the local fixture bundle in
docs/dogfood-readiness.mdbefore trying a real Foundry repo. - Use
docs/demo-case-study.mdfor the public DeFi vault demo and developer-call narrative. - Use
docs/public-foundry-dogfood-candidates.mdto choose a public read-only dogfood repo. - Use
docs/aderyn-integration-feasibility.mdfor the optionalcontract-guard scan --include-aderynanalyzer path; default scans still do not enable extra analyzers. - Use
docs/beta-design-partner-checklist.mdfor broader partner fit, onboarding commands, permission boundaries, and feedback signals. - Keep
docs/commercial-boundary.mdas the boundary for any hosted dashboard, self-hosted runner, pricing, or paid beta discussion.
The beta path stays local-runner first: no hosted uploads, no private snippet sharing, no live AI provider calls, no payment/custody scope, and no audit-completeness claims by default.
Generate a local-first evidence bundle before audits, releases, upgrades, or risky pull requests:
contract-guard evidence-pack \
--repo . \
--output-dir /tmp/contract_guard_evidence_pack \
--repo-label your-repo-label \
--format markdownThe pack writes Markdown/JSON/SARIF artifacts locally and keeps deterministic tool output as the source of truth. By default it requires no hosted private-code upload, sends no private snippets, includes no raw stdout/stderr in public summaries, makes no live AI provider call, and makes no audit/compliance/guarantee claim.
See docs/pre-audit-evidence-pack.md for the public evidence-pack walkthrough and docs/ci-supply-chain-safety.md for the local workflow safety checker.
Run a deterministic local scan:
contract-guard scan --repo . --format json
contract-guard scan --repo . --format markdown
contract-guard scan --repo . --format sarifFor pull-request style scoping, use changed-only mode:
contract-guard scan \
--repo . \
--changed-only \
--diff-base origin/main...HEAD \
--format markdownThe GitHub Action writes three deterministic outputs under contract-guard-reports/:
scan.json— policy-gated machine report.scan.md— PR-comment style deterministic evidence, without raw stdout/stderr.scan.sarif— GitHub code scanning upload.
AI triage is intentionally not included in these outputs. Deterministic tool evidence stays separate from any future advisory explanation layer.
Baseline suppression is optional and explicit:
cp .contract-guard-baseline.example.json .contract-guard-baseline.json
contract-guard scan --repo . --baseline-file .contract-guard-baseline.jsonRules:
- Baselines match by exact finding
id, or by exactcheck+file+start_line. - Only non-high Slither findings can be suppressed.
- High-severity findings are never suppressed by baseline, even if listed.
- Suppressed findings are kept separately in the machine report; they are not deleted.
Default CI policy fails on active high-severity Slither findings with low-or-better confidence:
contract-guard scan \
--repo . \
--changed-only \
--baseline-file .contract-guard-baseline.json \
--fail-on-severity high \
--fail-on-confidence lowUse --fail-on-severity none only when generating non-gating artifacts such as Markdown or SARIF after the policy-gated JSON scan has already captured the exit status.
Generate deterministic Foundry invariant starting points:
contract-guard invariants --profile erc20 --contract Token --format markdown
contract-guard invariants --profile vault --contract Vault --format json
contract-guard invariants --profile access-control --contract GovernorProfiles currently cover common ERC20 accounting, vault/ERC4626-style accounting, and access-control/paused-state checks. These templates are advisory starting points only: they are not proofs, audits, or guarantees, and each snippet must be adapted to the protocol's actual handlers, roles, assets, and expected edge cases.
Generate local-runner fuzzer command scaffolds without executing Echidna or Medusa:
contract-guard fuzz-hooks --tool all --target test/invariants/InvariantTest.sol --contract InvariantTest
contract-guard fuzz-hooks --tool echidna --target test/invariants/TokenInvariant.t.sol --contract TokenInvariant --format json
contract-guard fuzz-hooks --tool medusa --target test/invariants/VaultInvariant.t.sol --contract VaultInvariantThese hooks are deterministic scaffolds only. Before enabling live execution, pin the fuzzer version, review the generated command/config against that version, and keep fuzzer output separate from optional AI explanation. A fuzzing hook is not a proof, audit, or guarantee of safety.
Generate an auditable baseline candidate for a non-high finding:
contract-guard false-positive \
--id slither:unchecked-transfer:contracts/Token.sol:7 \
--check unchecked-transfer \
--file contracts/Token.sol \
--start-line 7 \
--severity medium \
--confidence high \
--reason "Known safe return-value wrapper in SafeERC20 adapter." \
--reviewer security-team \
--expires 2026-12-31 \
--format jsonThe command does not edit the baseline file. It emits a deterministic review record and, only for non-high findings with required review metadata, a copyable baseline_candidate. High-severity findings always return blocked_high_severity and no baseline candidate, preserving the CI rule that new high-risk issues stay visible.
Validate local project defaults:
cp .contract-guard-policy.example.json .contract-guard-policy.json
contract-guard policy --repo . --policy-file .contract-guard-policy.json --format markdownThe policy file currently covers scan defaults (changed_only, diff_base, baseline path, failure thresholds), report formats, scaffold-only fuzzing hook preferences, and safety boundaries. The normalized policy always keeps AI triage disabled by default, private snippet upload disabled, hosted uploads disabled, and raw stdout/stderr out of reports.
Build a local, redacted advisory payload without calling external AI:
contract-guard ai-triage-payload \
--check reentrancy-eth \
--file contracts/Vault.sol \
--start-line 42 \
--severity high \
--confidence medium \
--description "Vault withdraw sends before state update." \
--format markdownSnippet text is dropped by default. If a team explicitly asks to include context, --include-redacted-snippet emits only a deterministically redacted snippet and still reports send_private_snippets=false and external_ai_call_made=false. Deterministic analyzer evidence remains separate from any future advisory AI explanation.
Generate a local advisory explanation scaffold from the same redacted payload shape:
contract-guard ai-triage-explain \
--check reentrancy-eth \
--file contracts/Vault.sol \
--start-line 42 \
--severity high \
--confidence medium \
--description "Vault withdraw sends before state update."This explanation scaffold is not deterministic evidence, not a proof, not an audit, and not a guarantee. It is a local template for review questions and test suggestions; future live AI provider integration must keep the same evidence/advisory separation.
Validate the provider opt-in boundary before any live integration exists:
contract-guard ai-triage-config --format markdown
contract-guard ai-triage-config \
--provider openai \
--enable-external-provider \
--include-redacted-snippets \
--format jsonEven in the explicit opt-in shape, this command makes no external AI call and reports private_code_leaves_local_runner=false, send_private_snippets=false, and hosted_uploads_enabled=false. Private snippets and hosted uploads are blocked, not merely off by convention.
Render a combined triage report with hard section separation:
contract-guard ai-triage-combined \
--check reentrancy-eth \
--file contracts/Vault.sol \
--start-line 42 \
--severity high \
--confidence medium \
--description "Vault withdraw sends before state update."The combined report always renders Deterministic evidence (source of truth) before Advisory AI text (separate, non-gating). AI text cannot suppress findings or replace deterministic tool evidence.