🛡️ claude-oswe
White-Box Security Audit plugin for Claude Code
A Laucked Security project
Deep, OSWE-style white-box web-app security audit, run from your editor. Type /oswe:audit in a
repo you're allowed to test; the plugin traces attacker input from source → sink, chains the
findings toward unauthenticated RCE, verifies each chain under a proof contract, and writes an
evidence-backed report (Markdown + a self-contained visual HTML report you can save as PDF).
The problem it solves
Asking an LLM "are there vulnerabilities in this code?" gives you a confident wall of maybes: false positives on safe code, no exploit chain, no proof, no reproducibility. That's noise, not an audit.
oswe is built to not do that:
- Determinism where it matters. Severity, dedup, and the "is this a Critical unauth-RCE?" decision are not left to the model — they run in tested, dependency-free Node helpers with 375 unit tests (320 pipeline + 55 benchmark, including two end-to-end replay tests that drive the full helper chain via real CLIs with pre-baked responses — one for the no-cache path, one for the kill-then-resume cache path). The model finds; the helpers decide.
- A verifier that pushes back — by refutation. Every candidate chain and high-severity finding is re-checked by an independent verifier that can downgrade or reject. Since SP6, an accepted finding must survive a counterexample checklist (auth blocks? real sanitizer? source attacker-controlled? sink reachable?) — it is confirmed only when every checked counterexample is refuted.
- Schema-gated I/O. Each agent response is validated against a JSON Schema before it's trusted; a malformed response is rejected and re-run, never silently used to invent data.
- Low noise, proven. On the clean, real-world Flask tutorial it reports 0 exploitable findings. On real vulnerable code (OWASP NodeGoat) it finds and verifies a real RCE chain. See Proven on real code.
This is an OSWE-style audit assistant for finding and fixing bugs — not a penetration test and not a safety guarantee. See Honest limits.
Install
Requires Node.js ≥ 20 (the validators and tests target it; the audit aborts early without it).
# Run Claude Code with the plugin loaded from a local checkout:
git clone https://github.com/Laucked-Security/claude-oswe.git
claude --plugin-dir ./claude-oswe
That's it — no npm install needed to use it. The runtime validators are committed as a
self-contained, zero-dependency file (AJV is a dev-only tool used to regenerate them).
Usage
/oswe:audit # audit the whole project
/oswe:audit src/api # restrict to a sub-path (kept inside the project root)
/oswe:audit --sarif results.sarif # also adjudicate a SAST's findings (Semgrep/CodeQL SARIF)
/oswe:audit --sarif results.sarif src/api # ...restricted to a sub-path
/oswe:audit --concurrency 8 # raise analyzer/verifier throughput (1..16, default 4)
The audit never auto-runs (disable-model-invocation: true) — it triggers only on the explicit
command. A timestamped report is written to .oswe/reports/oswe-report-YYYY-MM-DD-HHMM.{md,html,json,sarif,xml} —
the .json is a schema-validated, canonical machine artifact (SP6) that downstream tooling (benchmark
ledger, baseline/diff, exports) keys on; the .sarif and .xml are CI-ready exports derived from it.
CI integration
oswe runs in CI and uploads the emitted SARIF to GitHub or GitLab code scanning. The validation-layer differentiator: SAST leads that oswe refuted (false positives Semgrep/CodeQL flagged but the validation layer ruled out) appear as SARIF suppressions — so your code-scanning dashboard learns which alerts oswe assessed as false positives, without leaving them as open noise.
# After your audit job writes .oswe/reports/oswe-report-*.{sarif,xml}:
- name: Upload SARIF to code scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: .oswe/reports/oswe-report-*.sarif
- name: Publish JUnit test results
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: .oswe/reports/oswe-report-*.xml
The exports are also a standalone transform — run anytime on a saved report.json:
node skills/audit/scripts/export-sarif.mjs --file <report.json> --out out.sarif
node skills/audit/scripts/export-junit.mjs --file <report.json> --out junit.xml --fail-on high
Proof-graph findings + counterexamples (SP6). Every accepted High finding now carries a complete
source→sink proof chain, and every accept/downgrade verdict carries a structured counterexamples[]
checklist the verifier had to refute. These are enforced deterministically (schema + apply-verdicts)
and measured on the benchmark (finding_proof_complete_rate, ce_resolved_rate). On the OWASP
BenchmarkJava stratified sample (24/category, 11 categories) oswe scores precision 1.000 / recall
1.000 over Semgrep, refuting Semgrep's false positives while keeping a complete proof for each promotion.
Trust-boundary hygiene lane (SP7). On top of the RCE-chain core, obvious CWE-501 trust-boundary
violations (attacker data written into trusted/session state) are reported as Low/Info hygiene
findings — never a chain member, never an RCE path, accepted by the verifier only for a direct
attacker→trusted-store crossing. This recovered the 8 previously-missed trustbound cases (recall
0.929 → 1.000) without adding a single false positive. OSWE-core for exploit chains, plus explicit
low-severity hygiene findings when the evidence is clean. See benchmark/BENCHMARK.md.
Kill-then-resume (SP5 v1). Long audits on big repos are now interruption-safe: every helper +
every validated agent response is checkpointed under .oswe/checkpoints/<run-id>/ keyed by input
- helper-version + plugin-contract digest. Kill the run with
Ctrl-C, relaunch the same command, and the already-validated work is reused — the helpers short-circuit on cache hit, the analyzer + verifier dispatches are skipped on cache hit, and the final report is produced again without re-paying for the LLM passes that already succeeded. The checkpoint dir is purged at clean exit (secrets are bounded to the kill→resume window, same trust model as.oswe/tmp/).
Example run (abridged)
/oswe:audit test-fixtures/python/vulnerable
Verdict: unauthenticated RCE — Critical (strong static proof)
CHAIN-1 POST /login { "is_admin": true } → session["admin"] mass-assignment (OSWE-1)
└─► /render?tpl={{ … }} → Jinja2 SSTI via render_template_string (OSWE-2) → RCE
entry: unauthenticated · 2 transitions, both accepted
Findings: 2 High (accepted) · Coverage: 2/2 partitions · no gaps
Report: .oswe/reports/oswe-report-2026-06-16-1600.md (+ .html)
📄 Full example reports (real output, public fixtures, no secrets): docs/examples/ —
Python · Java ·
.NET · a clean "safe" run.
Supported stacks
| Stack | Frameworks | Example sink classes detected |
|---|---|---|
| PHP | Laravel, Symfony, vanilla | type-juggling/magic-hash auth bypass, unrestricted upload, SQLi, command injection, PHP object injection |
| Node.js | Express, Nest | NoSQL operator injection, SSJI (eval), prototype pollution, command injection, SSRF |
| Python | Flask, Django | SSTI (render_template_string), pickle/yaml.load deserialization, mass assignment, SQLi |
| Java | Spring | SpEL/OGNL injection, Java deserialization gadget chains, command injection, XXE |
| .NET | ASP.NET (Core & classic) | forgeable/unsigned auth cookies, BinaryFormatter deserialization, command injection, XXE |
Each stack has a curated source→sink reference under skills/audit/references/.
A polyglot repo loads every relevant reference and partitions the audit by stack.
Hybrid mode — make your SAST precise (optional)
Pass a SARIF file and oswe treats each result as a lead: it reads the cited code and either
promotes it into a proven finding (chained + verified like any other) or refutes it with a
reason. You get your SAST's scale and rule coverage, plus oswe's discovery of the logic/auth bugs
SAST misses — and a report where every item is proven or explicitly refuted.
Measured against raw Semgrep on OWASP BenchmarkJava (an 88-case stratified subset spanning all 11 CWE categories, CWE-matched per the OWASP methodology):
| precision | false-positive rate | |
|---|---|---|
| Semgrep (official Java rules) | 0.732 | 0.341 |
| oswe-over-Semgrep | 1.000 | 0.000 |
oswe refuted all 15 of Semgrep's false positives — across every category — while keeping 40 of
41 true positives (one defensible recall cost, scored against us anyway). On the categories where
Semgrep was already precise (xss/weakrand/securecookie/crypto), oswe added zero false
refutations. It also recovered a true positive Semgrep missed (a trust-boundary bug found by LLM
analysis) and chained a SQLi to unauthenticated RCE via HSQLDB CALL. Full result, per-category
breakdown, and honest caveats: benchmark/BENCHMARK.md; reproduce via
benchmark/README.md.
How it works
parse-args → recon → partition (by module / framework / auth boundary)
→ analyze (parallel read-only oswe-analyzer subagents, max <concurrency>, default 4)
→ aggregate/dedupe (deterministic Node helper, stable OSWE-N ids)
→ build chains → verify (independent oswe-verifier, bound batches)
→ apply verdicts (deterministic Critical gating) → report (.md + .html)
→ finalize checkpoint (clean exit) — or leave it on kill, for the next run to resume
- Read-only agents. The analyzer/verifier subagents cannot modify your code.
- Confined scope. The path argument is normalized by a tested confinement helper that rejects
anything escaping the project root (
../, symlinks, sibling-prefix tricks). - Critical gating. A chain is
Criticalonly if the verifier accepted it end-to-end, every member finding is accepted, the entry isunauthenticated, and the impact isunauth-rce. - Configurable throughput.
--concurrency N(1..16, default 4) caps parallel analyzer + verifier dispatches. Independent of the 12-partition coverage budget — both still apply. - Implicit resume. Same command after a kill picks up where the previous run left off:
helpers detect their own cached output by content+version digest, agent responses are reused
only if they cleared every downstream gate (schema + partition binding + lead coverage +
validate-batch + global preflight). Editing user code, an analyzer reference, or
SKILL.mdinvalidates the affected cache entries automatically. - Secrets never leave. Discovered secret values are
[REDACTED]; onlyfile:lineis cited. Intermediate.oswe/tmp/files are purged at start, end, and on any abort..oswe/checkpoints/is gitignored and purged at clean exit (same trust model).
The ten JSON Schemas live in skills/audit/schemas/; the deterministic
helpers and their tests in skills/audit/scripts/.
Proven on real code
Run against OWASP NodeGoat — a real multi-module Express + MongoDB
app, not a toy fixture — /oswe:audit found and verified an end-to-end RCE chain:
Verdict: effectively-unauthenticated RCE — Critical (strong static proof)
CHAIN-1 POST /signup (open self-registration → instant authenticated session)
└─► eval(req.body.preTax) on POST /contributions → server-side RCE
24 findings across 6 partitions · 7 High accepted
What makes the result trustworthy rather than noisy:
- the verifier downgraded 4 over-eager
Highfindings (e.g. a "stored XSS" neutralised bymarked sanitize:true) instead of accepting them; - the schema gate rejected one malformed analyzer response and re-ran that partition once — no data is invented to fill a gap;
- the headline chain is reported as "effectively-unauthenticated" with the open-registration caveat stated, not inflated.
And the control: against the clean Flask tutorial flaskr the same
pipeline reports 0 exploitable findings (one Info-level hardening note, not inflated). Finds real
chains on real vulnerable code; stays quiet on clean code.
Honest limits
- OSWE-style assistant, not a pentest. This is static white-box source analysis. "Verified" = strong static proof (source→sink under a proof contract), not a live exploit fired at a running target. No dynamic execution, fuzzing, or runtime confirmation.
- "No path to RCE" ≠ safe. It means none found within the analyzed coverage. Budgeted partitions, unsupported stacks, and skipped paths are listed in each report's Coverage section — read it.
- The verifier shares the model's blind spots. It's an independent pass with a strict contract, not
ground truth. Confirm findings before acting; triage
not-requesteditems yourself. - Scope/scale bounded. Up to 12 partitions per audit; very large monorepos will hit coverage gaps (reported, never hidden). Dependency/gadget analysis is read-on-demand, not exhaustive.
- Five stacks, one curated reference page each — broad sink coverage, not an exhaustive SAST ruleset.
Treat the output as high-quality leads with evidence, to be confirmed and fixed by an engineer.
Development
The runtime validator skills/audit/scripts/validators.mjs is a
self-contained, zero-runtime-dependency ESM file (the AJV-generated validators with the one runtime
helper inlined). Run the suites (Node ≥ 20 only, no install):
( cd skills/audit/scripts && node --test ) # 320 pipeline tests
( cd benchmark && node --test ) # 55 benchmark-engine tests
node .github/scripts/check-structure.mjs # stacks / references / fixtures-markers gate
Regenerate validators.mjs after changing any schema (AJV is dev-only, used only to generate):
( cd skills/audit/scripts && npm install && npm run build && node --test )
CI (.github/workflows/ci.yml) runs the unit tests on Node 20 & 22,
runs the structure gate, and verifies the committed validators.mjs is in sync with the schemas.
claude plugin validate . --strict is run as a local gate before release (it needs the Claude
Code CLI, which isn't available on the CI runner).
Authorization & ethics
For authorized white-box review of code you own or are permitted to test, for defensive purposes only. Do not audit untrusted, hostile, or unauthorized repositories. A hostile repo's comments and strings are treated as untrusted data, never instructions. See SECURITY.md for the full responsible-use policy and how to report a vulnerability in the plugin itself.
No comments yet
Be the first to share your take.