🛡️ claude-oswe

White-Box Security Audit plugin for Claude Code

A Laucked Security project

ci License: MIT Tests: 375 passing Node ≥ 20 Stacks: PHP · Node · Python · Java · .NET Claude Code plugin

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 Critical only if the verifier accepted it end-to-end, every member finding is accepted, the entry is unauthenticated, and the impact is unauth-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.md invalidates the affected cache entries automatically.
  • Secrets never leave. Discovered secret values are [REDACTED]; only file:line is 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 High findings (e.g. a "stored XSS" neutralised by marked 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-requested items 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.

License

MIT © 2026 Laucked