web-pentest
Skill1 starsChanged 2 months agoMITView on GitHub
Conduct a black-box web application penetration test against a URL. Orchestrates two subagent skills — attack-surface-analysis and exploit-and-validate — in a loop, so each phase gets a fresh context instead of drifting over one long session. Use when the user asks to pentest, security-test, or hunt for vulnerabilities on a web app, or references an authorized engagement or bug bounty program. Does not require or use backend source code access. No separate LLM API key — subagents run as Claude Code sessions, same account, no external API integration.
- Installs packages
11 other projects use a version of this.
Adopted by
Most recently taken up by
- mrhlaingbwardev/agentic-hacking-skillstheir own version, first seen Nov 8, 2026
- briiirussell/cybersecurity-skillstheir own version, first seen Nov 1, 2026
- hassan-algo/open-sesametheir own version, first seen Oct 25, 2026
- nobodyohm-web/Thottheir own version, first seen Oct 13, 2026
- ferr079/claude-code-cybersec-skillstheir own version, first seen Oct 12, 2026
- hypnguyen1209/offensive-claudetheir own version, first seen Sep 21, 2026
Used with
Projects with it also use privesc-linux, threat-hunting and 2 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
jessefmooreSkillprivesc-linux
Linux privilege escalation and persistence — SUID/SGID abuse, kernel exploits, capabilities, sudo misconfig, cron jobs, writable paths, library hijacking, credential hunting, container escape, LXD/LXC, systemd timers, PAM skeleton key, polkit, MOTD, udev, git hooks, shell profile, NetworkManager, package manager hooks, at jobs, XDG autostart, LKM rootkit
Also in 3 projects that use this
allanninalSkillthreat-hunting
Hunt threats using Sigma detection rules and security analysis techniques. Use when investigating security incidents, creating detection rules, or analyzing suspicious activity.
Also in 4 projects that use this
TheWatcher01Skillosint-recon
Reconnaissance passive sur un domaine : certificats, DNS, headers HTTP, stack technique. Utiliser pour l'analyse OSINT d'un domaine cible.
Also in 3 projects that use this
jord0-cmdSkillRECON
Deep technical research that persists. Use when: exploring new technologies, investigating frameworks, comparative analysis of tools, understanding complex systems, or any research task that should be saved for future reference. Invokes Claude's built-in research agent and auto-saves results to a knowledge base for retrieval across sessions.
Also in 3 projects that use this
Other versions11 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 11
- briiirussell/cybersecurity-skills★ 489same name
- hypnguyen1209/offensive-claude★ 401same name
- mrhlaingbwardev/agentic-hacking-skills★ 347same name
- hassan-algo/open-sesame★ 89same name
- samklee1/web-pentest★ 4same name
- hackingyseguridad/IA★ 3same name
- jessefmoore/offensive-claude-code★ 2same name
- thesawdawg/agent-skills★ 2same name
- ferr079/claude-code-cybersec-skills★ 0same name
- imMamdouhaboammar/cybersecurity-skills★ 0same name
1 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository2 other files it gives its agents.
The file
Show its rules and text
We split this skill into the rules it gives an agent
19 rules. 0 are common, 8 are distinctive, and 11 are very specific to this project.
0
Common
Most of the 8 edited versions we read keep these.
8
Distinctive
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 29
Every fact this engagement relies on must trace to a live request made against the target this engagement, or to independent reasoning from …
- Very specific: line 67
Every command that sends a crafted request to the target, or opens/parses/renders content the target returned (a downloaded file, an HTTP …
- Distinctive: line 102
This is where you talk to the user: When a pass reports one or more flagged candidates:
- Very specific: line 112
On decline: leave the row Skipped: user declined, see pentest-findings.md and move on — this is a legitimate, permanent resolution for that …
- Very specific: line 115
Update pentest-findings.md/coverage to reflect the outcome either way, so Phase 6 reporting sees a resolved state, not a dangling flag.
- Distinctive: line 118
Never approve a flagged action yourself without asking — that defeats the entire point of the gate.
- Very specific: line 171
Try self-service first, before asking anyone for anything: Check whether the app has a public self-registration/signup route (search …
- Very specific: line 193
On approval with a token/cookie: use it directly as the session credential for subsequent requests (the exact header/cookie name depends on …
- Distinctive: line 209
On decline: leave the blocked rows/chains exactly as they are — a legitimate, permanent gap for the report, same as any other declined …
- Very specific: line 242
If this is not a bug bounty engagement (owned app, signed engagement without a public policy page), record scope.rate_limit: "not …
- Very specific: line 245
Environment question — required, don't guess it: Ask explicitly: disposable/sandbox the user fully owns, or shared/production/third-party?
- Distinctive: line 248
Credential-testing question — required, don't guess it: Ask explicitly: is password/hash cracking or brute-forcing authorized for this …
- Distinctive: line 402
After any write to this file, read it back and parse it: If it doesn't parse, fix it before moving on.
- Very specific: line 418
Write findings into recon_notes in the state file.
- Very specific: line 430
Write the raw route list into recon_notes.raw_routes_found.
- Very specific: line 476
Update control.iteration_count and control.last_attack_surface_pass each time through.
- Very specific: line 562
Instead, write .claude/pentest-needs-review.md — a separate file from the report, produced alongside it, containing every …
- Distinctive: line 579
Every technique tested in this engagement is found through one two-tier lookup, not two separate systems.
- Distinctive: line 639
If you (or a subagent) already know a technique with no matching row above, use it — don't wait for a row to exist first.
“Common” is measured: kept by at least 6 in 10 of the 8 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Web Application Penetration Testing (Black-Box, Autonomous)
You are the orchestrator for an authorized black-box web application security assessment. You do authorization, recon, and reporting yourself. For the two phases most prone to losing track of things over a long session — mapping the attack surface, and turning a finding into real impact — you delegate to two subagent skills instead of doing them inline. A subagent starts cold: it has nothing except what you give it, so whatever matters has to be written down. That's the point.
The two subagent skills:
attack-surface-analysis— builds and maintains the app model (roles, trust boundaries, workflows, hypothesized attack chains, assets implied but not yet found).exploit-and-validate— tests known assets and immediately chases anything it finds to real impact, in the same pass. No separate "come back to this later."
You run these in a loop (see "The Loop" below) until both are done. You never invoke them
yourself by doing their job inline — always spawn them via the Agent tool, subagent_type:
general-purpose (needs Bash, Read, Write, and the Skill tool itself), in the foreground
(run_in_background: false), and wait for the result before continuing. In the Agent prompt, name
the skill to invoke, the target project path, and which mode (initial/review) if applicable — a
subagent has nothing except what's in that prompt plus the state file.
Black-box constraint — binding on you and every subagent, every call
Every fact this engagement relies on must trace to a live request made against the target this engagement, or to independent reasoning from such a request. Never use training/prior knowledge of the target's specific identity to guess routes, credentials, known CVEs, standard challenge lists, or assumed architecture — this holds even when the target's name looks familiar or is self-reported in a banner/header; treat that name as an unverified claim, not a lookup key. This applies regardless of which tool the knowledge would come through — recalling it from training data and looking it up live (e.g. a WebSearch keyed on the target's own name/brand) are the same violation.
Restate this constraint explicitly in every attack-surface-analysis/exploit-and-validate
spawn prompt — don't rely on the subagent having read this file or on the state file carrying it
implicitly. A subagent starts cold each time; if this isn't in the prompt, it isn't guaranteed to
apply.
Scope enforcement — hard gate, binding on you and every subagent
"Check against scope" as a prose reminder is not enough — a subagent reading a JS bundle or following a redirect can touch a new host without ever consciously deciding to. Instead of relying on that judgment call every time, run the check mechanically:
Before the first request to any host not already verified this engagement, run:
python3 <path-to-skill>/web-pentest/scripts/check_scope.py <url-or-host> (it locates
.claude/pentest-state.json on its own by walking up from cwd, or accept --state PATH). It
prints ALLOW <host> — matches scope target '...' or DENY <host> — ... and exits 0/1
accordingly. Only proceed on ALLOW. On DENY, stop — don't route around it, don't ask "is this
really out of scope" and decide yourself; treat it as out of scope and note it in your summary.
Log every check in pentest-tools.md (one line: host, ALLOW/DENY, which target matched) — this is
what lets Phase 6 audit that the gate was actually used on every host touched, not just claimed.
This gate is host-level only. It does not replace path/asset-level scope.exclusions — those still
have to be honored when deciding which rows to add to coverage or which requests to send within
an in-scope host.
Restate this gate explicitly in every subagent spawn prompt, same as the black-box constraint above — a subagent starts cold and won't know the script exists unless told.
Sandboxed execution — binding on you and every subagent
Every command that sends a crafted request to the target, or opens/parses/renders content the
target returned (a downloaded file, an HTTP response body), runs through
scripts/sandbox_exec.sh '<command>' instead of bare Bash — full rule and fallback behavior in
tool-resolution.md's "Sandboxed execution" entry. This bounds the blast radius of a payload
behaving unexpectedly or a malicious response reaching your host filesystem/process space; it is
not optional for commands that fit that description, and it is not limited to content you already
suspect is dangerous.
You build the sandbox image once, yourself, before Phase 1 (subagents shouldn't each attempt
this redundantly): check docker image inspect pentest-sandbox:latest; if missing, run
scripts/sandbox/build.sh. If Docker itself isn't usable (not installed, or a permission error
even after sg docker -c '...', which sandbox_exec.sh already tries automatically), that's a
step-4 ask-the-user case from tool-resolution.md — tell the user exactly what's blocking it and
what you need from them (e.g. sudo usermod -aG docker $USER, or Docker installed at all). Don't
silently proceed unsandboxed without recording that gap in pentest-tools.md.
Restate this in every subagent spawn prompt, same as the other hard gates — a subagent starts cold and won't know the sandbox exists, or that the image is already built, unless told.
External research — the live half of technique retrieval, available to every subagent and phase
Technique retrieval is two-tiered — see "Capability-card lookup" below for the full explanation
and the Tier 0 index itself. Restate that the live tier (external-research.md) exists in every
subagent spawn prompt, same as the other hard gates above — don't rely on a subagent discovering
it only by reading a specific capability card that happens to mention it.
High-risk action gate — asking the user before anything destructive/irreversible
exploit-and-validate does not talk to the user (see its own SKILL.md) — so when a technique it's
about to run is destructive or hard to reverse (completing a reboot/shutdown-type action, mass or
non-synthetic deletion, a real resource-exhaustion run, anything sharing that shape — full list in
exploit-and-validate/SKILL.md's "High-risk action gate"), it stops short and reports the
candidate action back to you in its summary instead of running it, marking that coverage row
"Skipped: flagged for user approval".
This is where you talk to the user. When a pass reports one or more flagged candidates:
- Ask the user explicitly, one question per candidate (or grouped if genuinely related) via
AskUserQuestion — describe exactly what request would be sent and what it would prove, not a
vague "can I do something risky." Use the detail the subagent wrote to
pentest-findings.mdfor this. - On approval: either send the single minimal request yourself (if it's simple enough not to
warrant a full subagent round-trip), or spawn
exploit-and-validateagain scoped tightly to just that one action — restate all the usual binding constraints, plus explicitly note the user has approved this specific action so the subagent doesn't re-flag it. - On decline: leave the row
Skipped: user declined, see pentest-findings.mdand move on — this is a legitimate, permanent resolution for that row, not something to keep re-asking about in later iterations. - Update
pentest-findings.md/coverageto reflect the outcome either way, so Phase 6 reporting sees a resolved state, not a dangling flag.
Never approve a flagged action yourself without asking — that defeats the entire point of the
gate. Never let "the user already said full-exploitation in Phase 0" substitute for this — PoC
depth and destructiveness are different axes (see exploit-and-validate/SKILL.md).
Credential-gap gate — asking the user for a test session when that's the only blocker
credential_testing_policy: not-authorized means don't guess, crack, or brute-force your way into
an account — it does not mean "never operate with a session." Those are different things: one
is an attack technique on unknown credentials, the other is simply acting as a user the human
handed you access as. When exploit-and-validate reports one or more coverage rows/chains that
are N/A/blocked specifically because no authenticated session exists — not because a specific
technique was declined, but because the entire authenticated surface is unreachable without one —
that's worth surfacing to the user directly.
This gate also fires when a session exists but not of the required role. A real engagement self-registered a customer account, found 9 real findings on the customer-facing surface, then found that the target's entire ground-truth vulnerability set lived exclusively behind an admin login — and the gap sat there, silently accepted as "policy-blocked," without ever explicitly asking the human whether admin access could be provided or a bounded credential attempt authorized for that specific, now-concrete scenario. Having some session (customer) is not the same as having the session a specific chain needs (admin) — don't let the existence of a lower-privileged session substitute for asking about the higher-privileged one a blocked chain actually requires.
This ask is mandatory, not discretionary, and applies by default to every engagement — it is
not something that needs separate opt-in, and a Phase 0 answer of not-authorized for credential
attacking does not pre-empt this specific ask when a concrete scenario makes it relevant. The
rule in one sentence: whenever reaching further requires an admin login or a credential-cracking
attempt, do not silently mark it as a permanent gap — ask the human first, every time it becomes
concretely actionable (once per distinct blocked scenario, not once per blocked coverage row
touching the same scenario, and not preemptively before you actually know you'll need it).
A decline closes only the specific scenario under the factual conditions and preconditions that existed when the decision was made. If a later finding changes a precondition that the original request explicitly depended on, the resulting action is a new actionable scenario and must be reconsidered — it is not covered by the earlier decline.
Example: the user is asked whether to attempt credential guessing against an admin account, but the account and admin path cannot be confirmed. The user declines, with the condition that guessing should not proceed unless the path and account can first be confirmed. Several passes later, an unrelated scan discovers the admin path and confirms a real admin account email. The earlier decline should not automatically suppress further action, because the factual precondition that caused the original request to be declined has changed. The orchestrator must recognize this as a new actionable scenario and ask the user again whether to proceed with credential guessing against the now-confirmed account — don't let "we already covered admin access" substitute for checking whether the specific precondition that blocked the earlier ask has since flipped.
Before any hand-crafted path/account guessing for this gate, follow enum_hidden.md's own tool
preference — check for ffuf/gobuster plus a real wordlist (SecLists or similar) first; only
fall back to a short hand-written guess list if no tool is installable. Skipping straight to
hand-crafted guessing (as happened once already: 77 improvised admin-path guesses, none matching,
while a real wordlist tool would likely have found it in one run) wastes effort and can produce a
false "not discoverable" conclusion that a proper tool run would have disproven.
Try self-service first, before asking anyone for anything. Check whether the app has a public
self-registration/signup route (search recon_notes.raw_routes_found and the mapped route list for
register/signup/sign-up-shaped paths — this is a live-request question, so the usual scope
gate and sandboxed execution rules apply). If one exists, create your own low-privilege account
through it — choosing your own credentials for an account you create is not credential testing, it's
using the app exactly as a normal new user would. Log in with the account you just created and
resume the loop with that session; note in recon_notes that the session came from a
self-registered test account (still never persist the password itself in the state file). Only move
to step 1 below if self-registration doesn't exist, requires something you can't complete black-box
(email/SMS verification, admin approval, a CAPTCHA), or the app is invite-only by design (as with
most B2B/admin-console software) — in which case there's no anonymous bootstrap path and asking the
human is the only option left, not a shortcut being skipped.
- Ask the user explicitly, via AskUserQuestion, with (at least) three concrete options — don't just ask an open-ended "can you help": (a) a session token or cookie value from their own already-logged-in session for the required role, (b) a username/password for an account with that role, or (c) explicit authorization for a specific, bounded credential-guessing attempt against a confirmed account (e.g. "try up to N generic defaults like admin/admin against the admin@example.com account confirmed to exist") — state the actual account, the actual bound, and what it would prove, the same level of concreteness the High-risk action gate requires. Never default to (c) yourself or decide its bound on your own — that's the human's call to size, every time, not a policy you infer once and reapply.
- On approval with a token/cookie: use it directly as the session credential for subsequent
requests (the exact header/cookie name depends on what the app's own login flow uses — check
recon_notes/the auth workflow already recorded). Do not attempt to decode, crack, or otherwise analyze it beyond using it as given. On approval with a username/password: perform exactly one legitimate login with the exact credential given — never a variation, never a guess, never a second attempt with a different password. On approval of a bounded credential-guessing attempt: try exactly what was approved — the exact account, the exact list/count the human sized — and stop the instant it's exhausted, successful or not. Approval of one bounded attempt is not standing authorization for a bigger one later; a materially larger or different attempt needs its own fresh ask. In all three cases: never write the raw secret (password, token, or cookie value) intopentest-state.json,pentest-findings.md, orpentest-tools.md. Record only that an authenticated session was established via user-provided test access, and the role/username it corresponds to if known — the secret itself has no reason to persist in a file a future subagent or the final report will read. - On decline: leave the blocked rows/chains exactly as they are — a legitimate, permanent gap for the report, same as any other declined authorization. Don't ask again for this same, already-declined scenario later in the same engagement — but see the callout above: if a later finding supplies a fact (a confirmed path, a confirmed account) that the decline was conditioned on not having, that's a new scenario, not a repeat of the old one, and does need a fresh ask. What must never happen is skipping the ask itself — accepting a credential-gated gap as final without having concretely asked is the failure this gate exists to prevent, regardless of what a Phase 0 answer implied or what an earlier, differently-conditioned decline covered.
- Re-enter the loop with the new session available (if one was obtained) so
exploit-and-validatecan pick up whatever it previously could only markN/A: requires auth, no session available.
This is a separate decision from the Phase 0 credential-testing question — that one is about whether attacking authentication is in scope; this one is about whether the human can simply hand you a way in so the rest of the app can be tested at all.
Phase 0 — Authorization and scope (do not skip, do not proceed without this)
You do this yourself. It requires talking to the user directly — a subagent can't do it.
- Explicit authorization. Confirm testing is authorized — a named bug bounty program, a signed engagement, or a target the user owns/controls. If unclear, ask before touching anything.
- Scope. Precise in-scope assets. Explicit out-of-scope exclusions if any were stated.
- Program policy, if this is a bug bounty engagement — treat it as a hard override on this
skill's default methodology. Get the actual policy page (ask the user for the link or the
text), don't infer it. Pull out, specifically:
- Rate limit / concurrency cap, if the program states one. Record it verbatim in
scope.rate_limit. If the program states none, record"not specified — default conservative"rather than leaving the field blank, so a subagent can't mistake an unset field for "no policy was checked." - Forbidden techniques or rules of engagement — e.g. no automated scanners, no DoS-adjacent
testing, no social engineering, specific out-of-scope vuln classes. Record each as a short
phrase in
scope.program_rules, even if the list is empty ([]means "checked, nothing beyond the defaults" — not "didn't check"). If this is not a bug bounty engagement (owned app, signed engagement without a public policy page), recordscope.rate_limit: "not applicable — non-bounty engagement"andscope.program_rules: []rather than skipping the fields.
- Rate limit / concurrency cap, if the program states one. Record it verbatim in
- Environment question — required, don't guess it. Ask explicitly: disposable/sandbox the user fully owns, or shared/production/third-party? This sets the PoC-ceiling mode. If unanswered, default to PoC-only.
- Credential-testing question — required, don't guess it. Ask explicitly: is password/hash
cracking or brute-forcing authorized for this engagement (e.g. trying a short list of
common/weak passwords against an extracted hash, or against a password-protected file like a
KeePass DB)? If unanswered, default to not authorized. A weak/exposed hash, or a downloaded
protected credential file, is a complete finding on its own — see
capabilities/crypto_failures.md— recovering the actual plaintext is a separate, higher-risk action that needs its own sign-off, not something to attempt by default just because a hash or protected file was found. - Create
.claude/pentest-state.jsonwith the full schema below — not justscopefilled in. Every top-level key (recon_notes,attack_surface,coverage,findings_index,control) needs to exist from the start, even empty, so a subagent reading the file later never hits a missing key. Setcontrol.current_phase: "recon". Check every newly discovered host against scope before touching it. Never expand scope on your own judgment.
Working state
One structured file the orchestrator and both subagents share, plus two prose files for content that doesn't fit a rigid schema.
.claude/pentest-state.json — the source of truth
{
"scope": {
"targets": ["..."],
"authorized_by": "who confirmed, when, how",
"environment": "owned-sandbox | shared-production",
"poc_ceiling_mode": "poc-only | full-exploitation",
"credential_testing_policy": "not-authorized | authorized",
"exclusions": ["..."],
"rate_limit": "verbatim program limit, or 'not specified — default conservative', or 'not applicable — non-bounty engagement'",
"program_rules": ["short phrase per forbidden technique/rule, [] if checked and none apply"]
},
"recon_notes": {
"fingerprint": "server/framework facts from Phase 1",
"raw_routes_found": ["..."]
},
"attack_surface": {
"roles": [{"name": "...", "notes": "..."}],
"trust_boundaries": [{"description": "..."}],
"workflows": [{"name": "...", "steps": ["..."]}],
"hypothesized_chains": [
{"id": "C1", "trigger": "...", "expected_target": "...", "tested": false},
{"id": "C2", "trigger": "...", "expected_target": "...", "tested": false,
"source": "vuln_components.md tier1-osv.dev | tier2-payloadsallthethings | tier3-websearch",
"reference": "CVE-ID or advisory/technique-page URL"}
],
"implied_assets": [
{"name": "...", "source": "...", "located": false}
]
},
"coverage": [
{"asset": "...", "method": "GET", "priority": "high | standard | low",
"owasp": {
"A01": [{"technique": "authz_idor", "status": "untested"},
{"technique": "authz_rbac", "status": "untested"}],
"A02": "untested",
"A03": [{"technique": "vuln_sqli", "status": "untested"},
{"technique": "vuln_xss", "status": "untested"},
{"technique": "vuln_ssti", "status": "untested"},
{"technique": "vuln_cmdi", "status": "untested"},
{"technique": "vuln_xxe", "status": "untested"}],
"A04": "untested", "A05": "untested", "A06": "untested",
"A07": [{"technique": "auth_session", "status": "untested"},
{"technique": "auth_jwt", "status": "untested"},
{"technique": "auth_mfa", "status": "untested"},
{"technique": "auth_password_reset", "status": "untested"},
{"technique": "auth_provision", "status": "untested"},
{"technique": "auth_derive_identity", "status": "untested"},
{"technique": "auth_weak_credentials", "status": "untested"}],
"A08": "untested", "A10": "untested"
},
"notes": "..."}
],
"findings_index": [
{"id": "F1", "title": "...", "severity": "Critical", "status": "Confirmed",
"impact_demonstrated": true, "chain": "C1", "owasp": "A07", "technique": "auth_jwt"},
{"id": "F2", "title": "...", "severity": "Unrated - unconfirmed",
"status": "Unconfirmed observation - not counted as a vulnerability finding",
"impact_demonstrated": false, "chain": null, "owasp": "A01", "technique": "authz_rbac"}
],
"control": {
"current_phase": "recon | mapping | attack_surface | exploit_validate | reporting",
"iteration_count": 0,
"last_attack_surface_pass": "added N new items | nothing new",
"terminated": false
}
}coverage[].priority sizes how much testing effort a row is worth before exploit-and-validate
checkpoints and moves on — see that skill's own SKILL.md for exactly how the budget scales. Set by
attack-surface-analysis when the row is created, from concrete, checkable properties of the
asset — not a fresh subjective judgment call each time, and not proportional to how interesting the
asset merely sounds:
high: any state-changing (POST/PUT/DELETE/PATCH) endpoint on an authenticated route; any endpoint tied to ahypothesized_chainsentry whoseexpected_targetnames a financial transaction, credential/session compromise, cross-account data exposure, or admin/back-office access; any endpoint identified as part of a checkout/payment/account-provisioning workflow.standard: authenticated GET routes returning user-specific data (account pages, order history, addresses) — real IDOR-relevant surface, but lower blast radius than a money- or credential-moving action.low: static/informational GET routes with no state-changing action and no user-specific data (catalog listings, CMS pages, sitemap-shaped pages).
A row's priority isn't permanent — if a review pass or exploit-and-validate surfaces a fact
that changes the picture (a "low" static page turns out to render user-controlled input into an
admin view, say), update it the same way any other stale model fact gets corrected.
Two owasp cell shapes:
- Single-card categories (A02, A04, A05, A06, A08, A10 — each maps to exactly one capability card, see the lookup table below): a plain string status.
- Multi-card categories (A01, A03, A07 — map to 2, 5, and 6 capability cards respectively): a
list of
{"technique": "<capability card name>", "status": "..."}entries, one per card. This exists because a single confirmed vulnerability under one technique (e.g. SQL injection) does not mean the other techniques in that category (XSS, SSTI, command injection, XXE) were also tried — each needs its own status. If a technique with no matching card is used, append a new{"technique": "...", "status": "..."}entry rather than leaving it unrecorded — the card list is a floor, not a ceiling, and the coverage list should reflect that.
Status values, for either shape: "untested", "Vulnerable:F<n>", "Tested/Not Vulnerable",
"N/A: <reason>", or "Skipped: <named blocker>". A09 is not a column — see Phase 6.
A real anomaly with no demonstrated impact is not the same thing as a confirmed vulnerability —
don't let a coverage cell's "Vulnerable:F<n>" label imply otherwise when F<n>'s
impact_demonstrated is false. An observed deviation from expected behavior (a missing auth
check that returns no real data, a hash exposed with no way to prove it's exploitable) is real and
worth recording, but it reads to a report's audience as a confirmed breach the moment it sits in
findings_index with a normal severity and "Confirmed" status — regardless of a boolean flag
buried in the same object. So the flag isn't the only signal: when a finding's impact_demonstrated
is false, give it "severity": "Unrated - unconfirmed" and
"status": "Unconfirmed observation - not counted as a vulnerability finding" (see the F2 example
above) instead of a normal severity/"Confirmed" pair. This is what lets Phase 6 partition the
report correctly (see Phase 6, Findings) without re-deriving intent from prose.
findings_index's "technique" field mirrors coverage's granularity: set it to the capability
card name (e.g. "vuln_sqli", "auth_jwt") whenever "owasp" is a multi-card category (A01, A03,
A07), so a finding can be traced back to the exact technique entry it resolved in coverage.
Omit it (or leave it null) for single-card categories, where the category alone already
identifies the technique.
Keep every free-text value terse across this entire file — a short phrase, not a paragraph.
This applies to owasp statuses and coverage[].notes, but equally to attack_surface.roles[].notes
and attack_surface.trust_boundaries[].description — in a real multi-iteration engagement, those
two fields grow the same way coverage notes do, from repeated attack-surface-analysis review
passes each appending a new observation instead of replacing the old one. One sentence, citing a
finding ID for detail ("N/A: no XML input accepted", "Vulnerable:F14, see F14 for full
evidence"), is the target length everywhere in this file. Full justification belongs in
pentest-findings.md, referenced by ID — never inlined here. This file gets read in full by every
fresh subagent before it does anything else; if any field carries paragraphs, the file eventually
grows past what a subagent can load in one read, and subagents start re-testing rows or re-deriving
model facts they can't fully see are already resolved. Terseness here isn't a style preference,
it's what keeps this file usable as the engagement gets longer.
After any write to this file, read it back and parse it. If it doesn't parse, fix it before moving on. A broken state file breaks every subagent downstream of it.
.claude/pentest-findings.md
Full evidence prose — the actual requests/responses — one entry per finding, referenced by ID from
findings_index. Every entry needs an explicit Impact demonstrated line.
.claude/pentest-tools.md
Audit trail: what was run, tool vs. hand-crafted, per tool-resolution.md.
Phase 1 — Recon
You do this yourself, inline — no subagent. Fingerprint the live surface: server/framework
signatures, headers, marker files. Establish an authenticated session if the app has auth. Card:
capabilities/recon.md, capabilities/auth_provision.md, capabilities/auth_session.md.
Write findings into recon_notes in the state file. Set control.current_phase: "mapping" when
you move to Phase 2.
Phase 2 — Application Understanding / Mapping
You do this yourself, inline. Read the JS bundles — don't just regex for paths, actually read them
for HTTP methods, params, multi-step workflows, and any place client-side code trusts an
authorization decision. Content-discovery enumeration for routes not in the JS. Cards:
capabilities/recon_js.md, capabilities/app_mapping.md, capabilities/enum_hidden.md,
capabilities/enum_rest.md, capabilities/enum_graphql.md, capabilities/enum_websocket.md,
capabilities/enum_params.md.
Write the raw route list into recon_notes.raw_routes_found. This is raw material — you are not
building the app model here. That's the next phase's job, done by a subagent with a clear head.
Set control.current_phase: "attack_surface" when you move to Phase 3.
Phase 3 — Attack Surface Analysis (subagent)
Spawn the attack-surface-analysis skill via the Agent tool, foreground, mode: initial. Give it
the target project path and tell it explicitly to read .claude/pentest-state.json. Wait for it
to return.
It fills in attack_surface and builds the coverage skeleton. Read its summary. This is the end
of the one-time setup — everything after this point is the loop. Set
control.current_phase: "exploit_validate" before entering it.
The Loop (Phases 4–5, merged)
Repeat until termination:
- Spawn
exploit-and-validate, foreground. It tests untested rows incoverage, chases any access-yielding finding to real impact in the same pass, updatesfindings_indexandpentest-findings.md. Wait for its summary — including anything it flags as implying new attack surface. If its summary (or a scan ofcoverage) shows rows blocked purely on "no session available" rather than a declined technique, and you haven't already asked this engagement, run the Credential-gap gate above before continuing the loop. - Spawn
attack-surface-analysis, foreground, mode: review. Give it whatever the previous step flagged. It checks the model against that, updates it if needed, and reports "added N new items" or "nothing new." Check termination:
- All rows in
coverageresolved — no"untested"status anywhere, checked at the leaf level: for multi-card categories (A01, A03, A07), every technique entry in that category's list must be non-"untested", not just the category as a whole — and - The latest
attack-surface-analysispass reported nothing new.
Both must hold. If either is false, go back to step 1.
Turn count or session length is never a reason to stop early. If it's taking a long time, say so to the user directly and ask whether to continue — don't quietly relabel untested rows as done to make the loop end.
Safety valve, separate from the above. This is not about time — it's a check against a possible bug (e.g.
attack-surface-analysisreporting "new items" that keep turning out not to be real, so the loop never sees "nothing new"). Ifcontrol.iteration_countpasses 15, pause and show the user what's been found and what's still open, and ask whether to keep going. This is a transparent check-in, not a silent stop — don't relabel anything to make the count look lower than it is.- All rows in
Update control.iteration_count and control.last_attack_surface_pass each time through. When
the loop's termination condition is reached, set control.current_phase: "reporting" and move to
Phase 6 — control.terminated stays false until Phase 6 actually finishes (see below).
Phase 6 — Reporting
You do this yourself, inline. By the time the loop terminates, the audit gate's usual "re-derive the asset list and diff it" step is already satisfied — that's what termination means here. Still do these checks:
- For every
Vulnerable/Tested/Not Vulnerabletechnique entry (or plain cell, for single-card categories), confirm a citable request exists inpentest-findings.mdor was described in a subagent's summary. If not, that entry isn't actually resolved — send it back through the loop. - For every
findings_indexentry, confirmimpact_demonstratedis set —truewith evidence, orfalsewith a real reason. An entry missing this field entirely meansexploit-and-validatedidn't finish its job on that finding — loop again. For every entry where it'sfalse, confirmseverityreads"Unrated - unconfirmed"andstatusreads"Unconfirmed observation - not counted as a vulnerability finding"(not a normal severity with"Confirmed"status) — fix in place if a pass left a normal severity on an impact-unconfirmed entry, don't just note it and move on. - Check for the pattern where one exciting finding drowned out the rest of its batch. A Critical on one endpoint is not evidence about any other endpoint — and for multi-card categories specifically, a confirmed finding on one technique (e.g. SQL injection) is not evidence about the other techniques in that same category (XSS, SSTI, command injection, XXE) on the same asset. Check each technique entry was resolved on its own evidence, not inherited from a sibling technique's finding.
- Confirm whole-target actions (a scanner run, a dependency audit) have a citable record in
pentest-tools.md. - If the target exposes self-reporting ground truth (a challenge list, a scoreboard), compare
against
findings_indexas a sanity check — but only if in scope; respect any exclusions recorded inscope.exclusions. Treat a match cautiously: a target's own "solved" signal can be satisfied by a different bug than the one it's named after (a client-trust bypass tripping the same flag a real DOM XSS would) — don't let it substitute for the technique-specific evidence required by check 1 above. - Scan the whole state file for any status or free-text value longer than roughly one
sentence —
coveragestatuses/notes,attack_surface.roles[].notes,attack_surface.trust_boundaries[].descriptionincluded. Long inline prose belongs inpentest-findings.md, referenced by finding ID — trim it in place. This keeps the state file within what a fresh subagent can read in one pass on future engagements. - For every
findings_indexentry whoseowaspis a multi-card category, confirmtechniqueis set and matches a real entry in that asset'scoveragelist. A multi-card finding with notechniquefield can't be traced back to which coverage entry it resolved. - Confirm no finding required password/hash/credential cracking unless
scope.credential_testing_policyis"authorized". If it's unset or"not-authorized", every finding involving a hash or protected credential file must stand on exposure/weak-scheme evidence alone (percapabilities/crypto_failures.md), not on recovered plaintext. - Confirm the scope gate was actually used. List every distinct host that appears in
pentest-findings.mdandrecon_notes. For each, confirm a matchingcheck_scope.pylog line exists inpentest-tools.mdshowingALLOW. A host with findings/notes but no logged check is a gap — go verify it againstscope.targetsnow (don't retroactively assume it was fine) and record the check before finalizing the report. - Confirm every asset-listing response was actually followed, not just noted. Scan
recon_notes/pentest-tools.mdfor any route whose own response is a list of further script/plugin/asset paths (an extensions manifest, a plugin registry, a source-map index) — confirm each listed path has its own fetch-and-grep log entry perrecon_js.md's asset-fetch loop. A manifest route marked "tested" with none of its listed paths ever fetched is an incomplete mapping pass, not a resolved one — send it back through Phase 2 rather than reporting the target as fully mapped. This is the specific gap that let two documented vulnerabilities (routes defined only in unfetched plugin JS) go completely undiscovered in a real engagement; it is cheap to check and easy to miss without a name. - For every coverage cell marked
Skipped: <reason>citing a missing/uninstallable tool, confirm the relevant card's own hand-crafted fallback was actually attempted and separately evidenced as having failed — not just that a preferred named tool wasn't installable. ASkippedreason that stops at "package X not found" or "tool Y not installed," with no attempted-and-failed fallback logged inpentest-tools.md, is not a resolved row — send it back through the loop. Restating the card's fallback instructions more forcefully in the card itself is not a reliable fix on its own: a real engagement had a card that already explicitly said to fall back to a hand-crafted listener, and abandoned the check at the firstpip installfailure anyway, without ever attempting that documented fallback. Catching the gap here, at review time, is what actually closes it — checking whether the refusal was earned, not just asserted.
Set control.terminated: true. Produce the report:
- Executive Summary — headline finding counts/severities here are computed only from
findings_indexentries withimpact_demonstrated: true. An unconfirmed observation never contributes to "N vulnerabilities found" or a severity total, no matter how suggestive it looks. - Asset Inventory — from
attack_surface+coverage - Coverage Summary — reference
pentest-state.json Findings, split infrastructure-layer vs. application-layer, each with severity, evidence, and its Impact demonstrated line. This section is built only from
findings_indexentries withimpact_demonstrated: true. An entry withimpact_demonstrated: false(see the"Unrated - unconfirmed"convention above) does not appear in the report at all — not as a finding, not in a subsection, not in Gaps, nowhere in this document.Instead, write
.claude/pentest-needs-review.md— a separate file from the report, produced alongside it, containing everyimpact_demonstrated: falseentry: its title, the evidence excerpt frompentest-findings.md, and exactly what's missing to confirm it (a valid ID, a session, a specific payload) framed as a concrete next step. This is the file for a human to review and decide whether to chase further — distinct from the report (which only ever presents proven vulnerabilities) and distinct frompentest-findings.md/pentest-state.json(working files a human isn't expected to read directly). Tell the user this file exists and what's in it when you deliver the report; don't bury its existence in the report's own text.- Attack Paths — from
attack_surface.hypothesized_chains, markedtested: true— these are chains that were actually run, not reasoned about after the fact - Exploitation Evidence — state which PoC-ceiling mode was active
- Gaps — anything
Skipped, cross-referenced with its named blocker, plus the fixed note: "A09 (Logging & Monitoring Failures): not testable black-box; no dedicated capability." - Recommendations
Technique retrieval: capability cards (Tier 0) + external research (used by every subagent)
Every technique tested in this engagement is found through one two-tier lookup, not two separate
systems. Tier 0 is the table below — a small, maintained, exhaustively-enumerable index of
common techniques, checked first, every time. Tier 1+ is external-research.md — a live
lookup, reached for whenever Tier 0 comes up short. It handles two different gaps, defined in full
there: Gap A, the technique itself has no matching row below (HTTP request smuggling, cache
poisoning, prototype pollution, GraphQL-specific abuse, a framework-specific deserialization
chain — whatever the app's actual shape calls for); and Gap B, the technique is known (it's in
the table below, or was itself found via a Gap-A lookup) but a target-specific fact is missing
(does a live-fingerprinted library+version have a known CVE, what are a fingerprinted product's
default credentials). Neither tier is owned by any one card or phase — the orchestrator's own
inline Phase 1/2 work, attack-surface-analysis, and exploit-and-validate all consult both the
same way, whenever they hit either gap, and both stay inside the black-box constraint the same way
(see external-research.md for exactly how).
Tier 0 — the capability-card index
| OWASP Category | Capability cards to check | Coverage shape |
| A01 Broken Access Control | authz_idor.md (object-reference IDOR and mass assignment/overposting on create-update endpoints), authz_rbac.md (function-level gating and path-normalization filter bypasses) | multi-card — one technique entry per card |
| A02 Cryptographic Failures | crypto_failures.md | single cell |
| A03 Injection | vuln_sqli.md (SQL and NoSQL), vuln_xss.md, vuln_ssti.md, vuln_cmdi.md, vuln_xxe.md, vuln_log_injection.md (logging-framework injection, e.g. JNDI/Log4Shell-class) — six cards, check all six | multi-card — one technique entry per card |
| A04 Insecure Design | business_logic.md | single cell |
| A05 Security Misconfiguration | vuln_nuclei.md, enum_hidden.md | single cell |
| A06 Vulnerable and Outdated Components | vuln_components.md (the Gap-B entry point for dependency-version CVEs specifically) | single cell |
| A07 Identification and Authentication Failures | auth_session.md, auth_jwt.md, auth_mfa.md, auth_password_reset.md, auth_provision.md, auth_derive_identity.md, auth_weak_credentials.md (run unprompted whenever credential testing is authorized and a real username is known — see the card, this is not a targeted guessing campaign and doesn't need the Credential-gap gate, and doesn't get deprioritized just because admin access was already achieved some other way) — seven cards | multi-card — one technique entry per card |
| A08 Software and Data Integrity Failures | data_integrity.md | single cell |
| A10 SSRF | ssrf.md | single cell |
Cross-cutting: file_upload.md, file_download.md — by asset type, not OWASP category. Any
coverage row for an asset that serves a file by name/path/ID (/file/, /download/, /cover/,
/avatar/, /export/, /attachment/, or similar) must explicitly note that file_download.md's
full traversal battery was applied, not just left as an unexplained "untested" A05 cell.
Traversal testing needs no valid, known-good filename to run — a payload that escapes the serving
directory works whether or not you've ever found a legitimate filename this app uses. Don't let
"no valid file ID located yet" block or delay this test, and don't let it sit as an open
implied_asset across multiple passes when the traversal check itself was never actually run.
Report-time: chain_analysis.md, poc_validate.md (the evidence bar in poc_validate.md applies
identically to a Tier-0 card-sourced finding and a Tier-1+ external-research-sourced one — there is
no separate, looser standard for the latter).
A live fingerprint is a mandatory research trigger, not an optional one
The black-box constraint bans target-identity lookups (don't search the target's own self-reported
name to import assumed CVEs/routes/architecture) — it does not ban researching a generic
technology or library once a live request independently reveals one. These are different things,
and conflating them causes real misses: a distinctively-named cookie (rememberMe → Apache Shiro,
PHPSESSID → PHP, laravel_session → Laravel), a response header, an error-page banner, or a
fingerprinted framework (e.g. Spring Boot confirmed via JSESSIONID + Actuator) is a live,
independently-observed fact about a dependency the target happens to run — squarely inside Gap B
in external-research.md, the same as a version-to-CVE lookup. Treat it the same way: the moment
such a fingerprint appears, treat identifying that library and pulling its known bypass/attack
technique classes (not just its CVEs) as required, the same way an untested authz_rbac cell is
required — not something to do only if a specific check already "needs" it. A generic technique
that applies to a fingerprinted library (e.g. filter-chain path-normalization bypasses for
Java/Shiro-style access control) is exactly the kind of thing this research is for, and skipping it
because "we don't look things up about this target" throws out real, in-scope research along with
the banned target-identity lookup it isn't.
Tier 0 is a floor, not a ceiling — Tier 1+ is how you go past it
If you (or a subagent) already know a technique with no matching row above, use it — don't wait for
a row to exist first. If you don't already know one and nothing above fits either, that's a Gap-A
lookup in external-research.md, not a reason to skip the check. Either way, for a multi-card
category this means appending a new technique entry to that category's coverage list (see Working
State schema above), not just using the technique and leaving it unrecorded — same rule whether the
technique came from the table or from a live lookup.
A03/A01/A07 finding one vulnerability does not close the category. Confirming SQL injection
resolves only the vuln_sqli technique entry — vuln_xss, vuln_ssti, vuln_cmdi, vuln_xxe, and
vuln_log_injection on that same asset stay "untested" until each is independently tried or ruled
out. Same logic for A01's two techniques and A07's seven, and for any technique entry added via a
Gap-A/Gap-B lookup.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

