Exploratory QA of a running feature through its UI (Playwright) and/or API (curl) to find issues the plan and committed tests missed; returns findings with evidence, known issues, and blockers. Use when the user asks to QA, probe, or try to break a feature, after code review passes.
Installs into .claude/skills of the current project.
Are you the author of Adversarial Qa?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cunhaax-adversarial-qa)
---
name: adversarial-qa
description: >
Exploratory QA of a running feature through its UI (Playwright) and/or API
(curl) to find issues the plan and committed tests missed; returns findings
with evidence, known issues, and blockers. Use when the user asks to QA,
probe, or try to break a feature, after code review passes.
license: MIT
compatibility: >
Needs a runnable dev server (AGENTS.md Commands) and, for UI surfaces, the
plugin's bundled Playwright MCP server; API probing uses curl.
metadata:
author: André Cunha
---
# /adversarial-qa
Exercise a feature in the running app and surface anything wrong, confusing,
or likely to bite a real user. Go beyond the committed tests; don't
re-verify the spec or review code.
## Input
- Plan text (optional): read Requirements only to understand what the feature
does, not as a checklist
- Open deferred QA findings list (optional; empty list = "none open", which
is a value, not an omission)
## Steps
### 1. Get open deferred QA findings
- List passed in → use it as-is
- Otherwise:
- `AGENTS.md` → *Task Tracking* → *List open deferred QA findings* holds a
real command/tool call → run it
- section/bullet absent, `[TODO:]`, or `none` →
`gh issue list --label known-issue --state open`
- holds a real command you cannot run (e.g. an MCP tool you don't have) →
never fall back to `gh` (wrong tracker). Report under *Blockers* "known
issues not checked: <reason>", skip step 5, continue the rest
### 2. Determine the surface(s)
- From the plan's Requirements (or the diff, if no plan): **UI** (templates,
views, a controller path rendering a view/fragment/client-driven response),
**API** (REST or other network-callable endpoint, no view layer), or both
- Probe every surface the feature exposes; findings on one don't substitute
for another
### 3. Set up and drive, per surface
Start the server with the project's dev-server command; app URL from
`AGENTS.md` → *Commands*. Stop it with the documented stop command when done
(never `kill` by PID, never hunt processes with `lsof`).
- **UI**: drive the feature in a browser via Playwright MCP
- Server won't start or Playwright unavailable → STOP, report the blocker
- Never substitute `curl`, SQL, or other workarounds for browser
exploration on a UI surface
- Mechanical setup with a fixed sequence (login, navigating to the
feature) → batch it in one `browser_run_code_unsafe` call, not a
click/type/snapshot round trip per step
- Use granular tools (`browser_click`, `browser_snapshot`, …) only for the
exploration in step 4
- To locate an element by text or regex, use `browser_find` instead of
capturing a full `browser_snapshot`
- **API**: `curl` via `Bash` against the same app URL
- Server won't start, or a request needs credentials you don't have →
STOP, report the blocker
### 4. Probe beyond the happy path
Try what the planner likely didn't enumerate.
- **UI**: narrow viewports · keyboard-only navigation · browser back ·
multiple tabs on the same form · paste of weird/long/XSS content · reload
mid-edit · error-toast timing · interaction with unrelated UI on the page
· stale state after a failed submit · light/dark colour scheme and reduced
motion (`browser_emulate_media`)
- **API**: malformed, missing, or extra fields · wrong `Content-Type` ·
auth/authz boundaries (missing/expired token, wrong role or tenant) ·
idempotency and duplicate submission · pagination and limit edge cases ·
concurrent/racing requests · oversized payloads · unicode/injection strings
· status-code and error-envelope correctness · rate limiting
Report anything that looks off, even outside this feature's plan. Never work
around issues, infer intent, or decide a bug is "probably expected"; the
developer decides.
### 5. Compare with known issues
(Skip if *Blockers* already reports them as unchecked.)
- Finding matches an open `known-issue` → *Known issues* section, cite its
task id; not in *Findings*
- Observed behaviour worse than or different from the task's description →
that difference IS a finding
## Evidence
Capture only once something is a confirmed finding, never while exploring.
- **UI**: `browser_take_screenshot` (costs more than `browser_snapshot`; use
it only for a confirmed finding)
- Pass `filename: .qa-evidence/<name>.png` (relative to the project
directory)
- A request the page made: `browser_network_request` (headers and bodies
of one request listed by `browser_network_requests`), saved with its
`filename` parameter under `.qa-evidence/`
- **API**: the request and response showing the problem: method, URL,
relevant headers, status code, body
- Run `mkdir -p .qa-evidence` first (API evidence is written with `Bash`)
- Save under `.qa-evidence/` at the repo root (gitignored)
- Every finding cites ≥1 evidence file there + a one-sentence description of
what it shows
## Output
```
### Findings
- [Short description] — [evidence path] — [severity: bug / concern / nit]
### Known issues (already deferred — no action needed)
- [task id] [title] — [still present / not observed on this pass]
### Blockers (if any)
[Anything that prevented exploring — server won't start, Playwright
unavailable, credentials needed, etc.]
```
- Empty *Findings* is valid only if you genuinely probed and found nothing
worth flagging; "ran out of ideas" is not