GET /invoices/8843 as the owner 200 as anyone else 200 one of these should have been a 403
Broken access control sits at the top of the OWASP Top 10 and is still found by hand, one identifier at a time. J0 is a lightweight workbench that replays every request you made as every other account, and proves what came back — locally, with no account and no cloud.
Beta. macOS verified, Linux and Windows unproven. There is no signed bundle yet, so what you download is the source and three commands.
GET /invoices/8843 as the owner · 200 · 41,208 B as a viewer · 200 · 41,208 B got through
Three moves, and none of them guess
You browse. J0 watches whose session carried what, sends it again as everyone else, and then tries to talk itself out of the result before it reports one.
-
01 · watch
Whose session was that
Every request is attributed to the credential set that carried it — including when two accounts arrive with differently named cookies. Names are recorded; values never are.
-
02 · replay
As everybody else
The whole credential set is exchanged, not just a cookie: auth headers, body tokens and all three anti-forgery carriers. Swap half of it and the rejection looks exactly like a control holding.
-
03 · prove
A 200 is not a finding
An identifier belonging to nobody, the request repeated, every credential stripped — then the question that settles it: could this account have obtained that identifier at all?
Numbers that carry their own bar
“Similar in shape and length” is the sentence that gets a report sent back. J0 carries the measurement that settled a finding into the write-up: the size gap, the threshold it was weighed against, and where that threshold came from.
The bar is learned per endpoint from its own repeated reads, so an endpoint that swings 19 % between two identical requests is not judged by the same bar as one that is byte-stable. Resemblance is never decisive on its own.
The shape of an Issues entry. The identifiers are illustrative; the format is the one the application prints.
GET /org/billing/invoices as a viewer · 403 denial fingerprint matches blocked · the control held
What it refuses to do
Most of what makes a security tool trustworthy is what it will not do. None of these is a setting, and none is on a roadmap to be relaxed.
-
01
A skipped check never reads as clean
Every refusal, every unsupported case, every control that was not reachable is recorded with its reason and appears in the report. A results list that does not say what it never reached reads as an all-clear, and an all-clear nobody earned is the most expensive thing a security tool can produce.
-
02
No model composes a request
A language model chooses what to look at and writes up what came back. It never builds a request, never picks a payload, never sees a credential. The sender accepts only a value minted from real captured traffic, so a string a model invented cannot reach it — the program would not compile.
-
03
Nothing runs on its own
No daemon, no schedule, no background scan that was already going when you opened the window. A run starts because somebody started it. The certificate authority is generated locally and offered as a download; it never lands in a system trust store on J0’s initiative.
-
04
The bytes are preserved
Duplicate headers, duplicate parameters, binary bodies and unusual encodings survive intact, and anything normalised is kept apart from the original. A tool that quietly canonicalises a request is reporting on a request it did not send.
-
05
No severity score
Severity is demonstrated impact, and only somebody who knows what the data is can judge it. Findings are bucketed by what to do next — dig, candidate, not applicable, merged — and never reduced to a number.
One window, eighteen tabs
Burp’s shape without its drawer of single-purpose extensions, grouped Capture, Manual tools and Analysis — and the proxy is already running when you get there.
| Tool | What it does | State |
|---|---|---|
| Proxy | Forward, CONNECT tunnel, HTTPS interception with a local CA, bidirectional WebSocket capture, Match & Replace rules applied in flight. | built |
| History | Byte-faithful storage in a SQLite project file, bodies de-duplicated, credential-set attribution per exchange, per-request notes. | built |
| Repeater | Tabbed and persisted, HTTP/1.1 and HTTP/2, per-tab send history, Send as another account, and a compare bench that runs the engine’s own comparison. | built |
| Intruder | Sniper, battering ram, pitchfork, cluster bomb. List and number-range payloads, grep-match columns, outlier highlighting, hard caps. | built |
| Issues | Findings worst-first with triage buckets, each carrying the measurement that settled it, and a coverage ledger beside them. | built |
| Agentic pentest | Logs each account in, crawls their features in a browser it drives click by click, then runs the cross-account scan and the passive passes. | built |
| Upstream proxy | Chaining J0 through another proxy. | deferred |
| HTTP/2 at the proxy | The Repeater speaks HTTP/2; the interception path does not yet. | deferred |
| WebSocket injection | Frames are captured and viewable. Editing one in flight is not built. | deferred |
agent · list_sessions owner cookies: JSESSIONID, csrf_token names only no value returned
A model can drive it. It cannot send anything.
The AI layer is optional and off until you turn it on. It is bounded by the Rust engine rather than by a prompt, which is the only kind of boundary worth having.
Pick a brain, or none
The deterministic engine runs the whole cross-account scan with no model at all. Point it at Claude Code, at any OpenAI-compatible endpoint including a local one, or at a hosted provider — the run is the same run, and every one of them is recorded the same way.
It drives a real browser
On an agentic run the model logs each account in through the proxy and crawls their features click by click, choosing controls by role and accessible name. It never reads a field’s value. The click policy refuses destructive controls, and a feature it declines is written down as declined.
The prompt is data, not a constant
The system prompt is an editable tree of files, one source for every driver. The boundary clauses are not in that tree and cannot be edited out — they are compiled in, and a test fails the build if any driver loses one.
-
may
Read the capture through 17 tools over MCP, ask the engine questions, choose which feature to look at, and draft the write-up.
-
may not
Compose request bytes. The sender accepts only a value minted from real captured traffic, so a string a model invented cannot reach it — the program would not compile.
-
sees
Cookie and header names. Page reads are redacted: tokens and JWTs scrubbed, object identifiers and UUIDs deliberately kept, because those are the thing under test.
-
never sees
A credential value, in any carrier. The tool API has no shape that could return one.
-
fires
Only behind an explicit toggle. Every agentic request is marked as one in HTTP history, with the account it ran as.
-
never decides
A verdict. The engine settles findings; the model explains them. A run is recorded with its brain, scope and ending — the deterministic control included, because a control that is not recorded is not a control.
Coverage is an output, not a footnote
Each class of check reports tested, not measured, or refused — with the reason attached. Not measured is never rendered as a pass, anywhere in the application.
The same rule governs an agentic run: every feature it walks is recorded as tested, skipped or never reached, and a refusal to touch a destructive control is written down rather than quietly passed over.
| Class | Reach | State |
|---|---|---|
| Access control | 389 of 412 endpoints | tested |
| Reflected XSS | passive, on captured replies | tested |
| Business logic | leads only, never confirmed | candidates |
| Injection | out of scope by design | not measured |
| File upload | refused: destructive control | not reached |
Twenty-three endpoints were refused in this example, each with its reason, and all twenty-three appear in the report.
Running in three commands
There is no installer yet — signing, checksums and an SBOM are the work left before a tagged release. Until then it builds from source, and the first launch needs no configuration: pick a project file, and the proxy is already listening.
# Rust on PATH, once per shell
$ source "$HOME/.cargo/env"
$ pnpm install
$ pnpm tauri dev
Then point your browser at 127.0.0.1:9090 and download the CA from the
landing page it serves. Verified on macOS aarch64; the Linux and Windows jobs
in the CI matrix have not been run.