Holds in code review.
Fails as user B with one curl.

One install gives your agent an executed, evidence-backed red-team pass over an app you own: nine written safety gates first, then three parallel record-only subagents — each with its own isolated User A, User B and Admin accounts — running 14 authorization and abuse checks, a request and a response captured per row, one central fix pass, and a rerun with a negative control that proves the test can see the bug.

3 record-only subagents 14 checks, one row each 9 written safety gates 13/14 failed on the fixture

for coding agents · local or isolated environments only, never production

install

Copy one line. Test before your users do.

Detects Claude Code, Cursor, Codex, Qoder and more, then writes the skill into the right folder. Add -l to see what the repo ships before anything is installed.

npx skills add arm-pz/pre-ship-security-red-team

ask it things like

Point it at a localhost port you own.

It takes a URL and, optionally, a project path — <local app URL> [project path] — and asks until every one of the nine gates is answered in writing. Nothing is attacked before the gate is filled in.

Before you ship gate first

The launch question, asked honestly.

  • /pre-ship-security-red-team http://127.0.0.1:3000 — play the bad guy before I ship
  • Is my vibe-coded site safe to launch
  • Red team my staging preview, then fix and prove the fixes

The specific worry one row at a time

Names the failure class; it runs the check and shows the capture.

  • Test whether user A can read or delete user B's data, then fix it and prove the fix
  • Check that my free-tier limits and rate limits hold when someone bypasses the UI
  • Can I grant myself admin from the profile endpoint

Also when you have just added triggers

The workflow fires on the surfaces that carry the risk, not only on an explicit "red team" request.

  • login and sessions
  • roles or an admin area
  • credits, quotas, paid tiers
  • file uploads and imports
  • anything that fetches a user-supplied URL
  • AI ingestion of text you don't control

how it works

Three groups, three account sets, one fixer.

The subagents attack and record — scenario id, preconditions, safe reproduction, redacted request and response, expected versus actual, evidence location. They never fix, never edit application files, never touch accounts. The chair consolidates duplicates, fixes centrally in blast-radius order, then reruns.

Identity & authorization 1 · high effort

The failures that expose or destroy someone else's data.

  • S01 anonymous database or API access
  • S02 cross-user object access (IDOR/BOLA)
  • S03 self-service privilege escalation
  • S04 password-reset token reuse

UI bypass & input 2

Where the client was trusted and the server agreed.

  • S05 direct request bypass of UI limits
  • S06 free-tier or quota bypass
  • S07 concurrent quota race
  • S08 input validation and boundaries
  • S09 redirect validation

Browser, network, cache, AI 3

Everything that leaves the request/response pair.

  • S10 stored XSS
  • S11 SSRF, local mock server only
  • S12 private response and cache isolation
  • S13 preview deployment access
  • S14 prompt injection through ingested content

One isolated account set each no collisions

User A, User B and Admin per group, prefixed g1a@test.invalid so two subagents never write the same rows and cleanup stays mechanical. The chair creates and deletes them; subagents never promote or remove an account.

  • one browser context per group
  • synthetic rows only, never a prod copy
  • each group fills only its own file
  • sequential fallback if subagents are unavailable

The evidence contract all

Scaffolded into .pre-ship-red-team/<run-id>/: gate.md, an ignored .env written before the first test credential exists, one brief and one results table per group, capture directories, then report.md. Verdicts are pass, fail, not-proven, not-applicable — unrun is the scaffolded default and always fails the check. After the fixes, check_results.mjs --require-fixes exits 1 on a missing row, a pass without a captured request and response, a fix without its negative control, or an unmasked credential anywhere in the run directory.

  • 14 rows, no adding or removing
  • negative control: remove the guard, watch it fail, restore it
  • a durable regression test per finding
  • ship posture HOLD or ready, scoped to what ran

before and after

14 scenarios against a fresh fixture: 13 failed.

One end-to-end run, 2026-10-18, against a purpose-built disposable app on 127.0.0.1 — a dependency-free Node notes/SaaS fixture, not published. The environment gate resolved to LOCAL, so every request stayed on loopback against synthetic rows. Three record-only subagents, one per isolated account set, returned this.

real end-to-end run · 2026-10-18 · local fixture · gate verdict LOCAL · every row executed

scenariobeforeafterfalsified
S01 anonymous database or API accessfail critical private collection readable with no credentials, using a key the app published itselfpassyes
S02 cross-user object accessfail critical read, write and delete on another user's row, on both the API and the HTML routepassyes
S03 self-service privilege escalationfail critical PATCH /api/me {"role":"admin"} persisted and unlocked the admin endpointpassyes
S04 password-reset token reusefail high same token accepted three times, never consumed, no expiry, minting unthrottledpassyes
S05 direct request bypass of UI limitsfail high unreviewed draft published by POSTing past the disabled buttonpassyes
S06 free-tier or quota bypassfail medium one AI surface gated on a client-supplied x-credits headerpassyes
S07 concurrent quota racefail high 5 concurrent calls accepted against an allowance of 3; only 2 deductedpassyes
S08 input validation and boundariesfail medium 50k-char, empty, NUL, bidi, wrong-type and non-JSON bodies all stored and read backpassyes
S09 redirect validationfail medium //evil and /\evil both cleared a leading-slash test and left the originpassyes
S10 stored XSSfail critical body rendered raw, alert() dialog fired in a browser as a second userpassyes
S11 SSRFfail critical internal loopback service fetched and its body reflected back to the callerpassyes
S12 private response and cache isolationfail critical per-user profile marked public, max-age=600 with no Varypassyes
S13 preview deployment accesspass route required its token at baselinepassn/a
S14 prompt injection through ingested contentfail critical a SYSTEM: line in an uploaded document made the tool return another user's recordspassyes

how the fixes were proved

Twice, not asserted: the committed rerun suite returns 14 REFUSED / 0 DETECTED, and a mutation harness deletes one protection at a time on a throwaway copy of the server and requires the mapped check to flip back to DETECTED — 17 of 17 protections falsified. That second run is what makes the first one meaningful; a suite that only ever passes cannot tell you whether it can see dirt.

every row carries its method, request, response and severity in results/group-<n>.md, and the checker refuses the run until each row is filled, evidenced and masked--require-fixes additionally blocks any surviving fail

Read the table with its limits, which the run records in report.md: it shows what the rig planted, not what an average app contains; six controls in the rig held at baseline, so 13-of-14 is a fixture property; the S12 proof is header-level because no shared cache sits in front of a loopback server; the browser dialog proof exists for the before state only; and the hosting-platform preview class (Vercel, Netlify, Cloudflare) was never reachable from a local run.

Also verified, independently of the app: the scaffold and the checker are tested against 11 fixture outcomes — unfilled rows, code-only passes, legitimate not-applicable rows, open fails allowed pre-fix and blocked with --require-fixes, fixes without a negative control, an unmasked JWT planted in a capture — all passing. This is a pass rate over 14 checks, not a claim that an app is free of vulnerabilities, and it never calls anything secure.

what it will not do

Nine gates, and a hard no.

Anything you do not own

Explicit ownership or explicit authorization, and nothing outside the approved environment. No third-party systems, no real users, no real payment flows — payments in test mode only, and the mode is confirmed from the key itself, not from the filename.

Production, ever

Local or isolated environments only, never production. A preview that shares the production database is production: a non-local --app-url is flagged in the written gate, and the checker refuses to certify a report whose gate was never filled in.

Hammer an endpoint

The concurrent-quota test defaults to five requests; twenty only on your explicit approval for a local-only test. It backs off on 429 or 503, stays inside the per-group request budget recorded in the gate, and stops the run on instability or any sign of production connectivity.

Real metadata or live attacker sites

SSRF uses a local mock server and reserved .invalid destinations; redirects go to .test hosts. It never loads a third-party origin, never port-scans past the app's own origin, and never calls a real cloud metadata endpoint.

Commit credentials or raw responses

Passwords stay in a git-ignored .env written before the first credential exists; tokens and personal data are masked in every artifact. And it changes nothing you did not approve: no plugin installs, no dashboard or configuration edits, no external deletions, no commits or pushes.

Call your app "secure"

It reports what ran, what was refused and what was never proven, with a ship posture of HOLD or ready scoped to what was tested, and a named list of every row that never ran. Fourteen passing rows is evidence about fourteen checks — not proof of no vulnerabilities.

Run the attacks before your users do — and keep the captures.

Free, open, and it only runs where you are allowed to test: local or isolated environments, never production.

Install pre-ship-security-red-team Read the source