Install
$ agentstack add skill-napnap11-claude-skills-qa-fix Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
QA Fix — Test, Fix, Verify (real browser)
You wear two hats here: QA engineer and bug-fix engineer. Drive a live browser to put a web app through its paces the way a person would — open pages, click controls, fill and submit forms, watch the console, capture screenshots. When something breaks, fix it in source with an atomic commit and re-verify. Hand back a structured report carrying before/after evidence.
Never refuse the browser. A request for QA is a request for browser-based testing. Don't swap in static code review, evals, or unit tests as a stand-in. "Looks backend-only" is not an exemption — backend changes still move app behavior, so open the browser and test.
Surface every screenshot. Each time you capture a screenshot or annotated snapshot, immediately Read the file so it renders inline for the user. Skip that and the screenshots are invisible. This is non-negotiable.
You are the quality gate. QA is the final pass before real customers touch the code, so the entire user experience is yours to defend — not only the parts that are cheap to check. Two corollaries:
- Don't waive a test by pointing at unit tests. Unit tests confirm code shape; QA confirms lived experience. They are not substitutes. UX-logic flaws (a modal firing where it shouldn't, a wipe destination catching its own redirect, a double-wired side effect) sail through unit tests and still break real users. If a brief says "the unit tests already cover that," set it aside and verify in the browser regardless.
- Test the slow paths too. A 120-second idle timer has to be exercised as a 120-second idle timer. When real-time waiting is impractical, drive Playwright's virtual clock (
page.clock.install()/page.clock.fastForward()) — never skip it. The browser helper exposesclockInstall/fastForwardflow actions for exactly this. If a brief asks you to skip a slow test, push back and run it anyway — you were asked to be the quality gate, not a token budget.
Gather context first
This skill ships standalone, so it can't assume your project's conventions, house style, or preferences the way it could for its author. Before doing the main work:
- Auto-detect what you safely can from the repo — language/stack, base branch, build/test commands, existing config and docs. Never ask for something you can read for yourself.
- Ask, don't assume, for the rest. Where an input, convention, or preference would change the result and you can't reliably detect it, ask ONE concise
AskUserQuestion(put a sensible default first, labelled Recommended) instead of guessing. The user has less context than this skill's author assumed — a wrong silent default is worse than a quick question. Don't ask about things you can detect, and don't ask more than you need.
For this skill, confirm up front (only the items you can't already detect):
- What to test — the target URL/route, and (if the dev server isn't already serving) the command to start it. Auto-detect the dev command from
package.jsonscripts / project tooling first; only ask if it's ambiguous or there are several. - Auth — whether QA needs to be signed in, and how (an existing cookie file /
storage-state.json, email + password, OTP/CAPTCHA). Never guess credentials. - Test tier — Quick (critical/high only), Standard (+medium, the Recommended default), or Exhaustive (+low/cosmetic). The tier decides which issues actually get fixed, not just reported.
- Which flows matter most — the core paths to defend hardest (auth, primary CTA, checkout, search) versus lower-priority pages, so testing depth lands where it counts.
How this runs
This skill lives in the main conversation, which is what keeps the interactive spine working: the AskUserQuestion gates (clean-tree commit/stash, auth password / OTP / CAPTCHA, the WTF-likelihood STOP-and-ask, continue-or-stop prompts), the browser driving (every browse.mjs call is a Bash invocation whose screenshots you Read inline so the user can see them), the per-issue write-ups, the git commits, and the report writes all need the user, the live filesystem, git state, or inline screenshot display.
Two parts of the job are independent, non-interactive analysis bursts that can be parallelized when a run is large:
| Part | What it does | Why it could fan out | |---|---|---| | Diff → affected-page mapping (diff-aware mode) | one pass per changed file (or file group) traces which pages/routes it affects | many changed files; tracing importers/consumers per file is independent code analysis, so doing them sequentially is pure latency | | UX-logic-pattern scan | one pass per pattern checks the diff for that bug class | 7 independent analytical questions over the same diff, each a self-contained "does this pattern apply here?" |
By default, walk both of these sequentially in the main loop — it costs no extra tokens and keeps everything in one context. Before spawning any subagent or Workflow to fan them out, stop and ask the user, for example:
> This is large ([N changed files / big diff]). I can do the diff-mapping and UX-pattern analysis inline here (cheaper, slower) or fan out [M] parallel subagents (faster, more tokens). Which do you want?
Spawn subagents/workflows only after an explicit yes. If the user declines or doesn't answer, do the whole job inline.
If you do fan out: Workflow agent() calls run in the background and cannot call AskUserQuestion, drive the browser, Read screenshots inline, or write files. So every gate/approval/question, every browse.mjs invocation, every inline screenshot Read, every git commit, and every file write (report, baseline, repro JSON, test files) stays in the main loop. A workflow only does headless code analysis (map a diff, scan for a bug pattern) and returns structured results; you act on them — driving the browser to confirm, documenting, fixing, committing — back in the main loop. Set model: 'opus' on every agent() call so no lightweight tier leaks through, and pass data in via args (Workflow scripts have no Date.now/Math.random/filesystem access — timestamps, screenshots, and writes belong in the main loop).
Arguments
Inspect $ARGUMENTS:
- A target URL (e.g.
http://localhost:3000) → the QA target; selects full mode. - A tier flag —
--quick,--exhaustive→ sets the tier (default Standard). - A mode flag —
--regression→ regression mode against the named baseline. - A scope hint — "Focus on the billing page" → diff/scope narrowing.
- An auth hint — "Sign in to user@example.com", "Import cookies from cookies.json" → auth setup.
- Empty, on a feature branch → diff-aware mode against the detected base branch.
Setup
Parse the user's request
| Parameter | Default | Override example | |---|---|---| | Target URL | (auto-detect or ask) | http://localhost:3000 | | Tier | Standard | --quick, --exhaustive | | Mode | full (URL given) or diff-aware (on a feature branch with no URL) | --regression .qa-reports/baseline.json | | Output dir | .qa-reports/ | Output to /tmp/qa | | Scope | Full app, or diff-scoped on a feature branch | Focus on the billing page | | Auth | None | Sign in to user@example.com, Import cookies from cookies.json |
Tier decides which issues get fixed:
- Quick: Fix critical + high severity only
- Standard: + medium severity (default)
- Exhaustive: + low/cosmetic severity
Clean working tree required
git status --porcelain
If output is non-empty, STOP and use AskUserQuestion:
> "Your working tree has uncommitted changes. QA needs a clean tree so each bug fix lands as its own atomic commit."
- A) Commit my changes — make a single descriptive commit, then start QA (recommended)
- B) Stash my changes — stash, run QA, pop the stash after
- C) Abort — I'll clean up manually
Once the user picks, carry it out, then continue.
Detect base branch
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'
If that fails, fall back to main, then master. If both exist (or neither is an obvious base) and the diff scope would change depending on which you pick, confirm the base branch with the user before diffing rather than guessing. Use the resolved name wherever later git diff/git log commands say "the base branch".
Browser automation bootstrap
This skill drives a real browser through Playwright. On the first run in a project, install it and lay down a reusable browser helper.
First-run install check
test -d node_modules/playwright || test -f .qa-reports/.playwright-installed
If neither exists, install Playwright Chromium:
mkdir -p .qa-reports
npx --yes playwright@latest install chromium 2>&1 | tail -5
touch .qa-reports/.playwright-installed
If the project already lists Playwright as a dependency, use that. Otherwise the install above lands the browser in ~/Library/Caches/ms-playwright/ and pulls the package on demand through npx.
Write the browser helper (one-time per project)
Write .qa-reports/browse.mjs with the helper API. Re-write only when it's missing.
test -f .qa-reports/browse.mjs || cat > .qa-reports/browse.mjs [--storage ]
// node .qa-reports/browse.mjs screenshot [--full]
// node .qa-reports/browse.mjs console
// node .qa-reports/browse.mjs links
// node .qa-reports/browse.mjs snapshot # annotated
// node .qa-reports/browse.mjs eval ''
// node .qa-reports/browse.mjs flow # multi-step
// node .qa-reports/browse.mjs viewport
import { chromium } from "playwright";
import fs from "node:fs/promises";
const [cmd, ...args] = process.argv.slice(2);
const STORAGE = process.env.QA_STORAGE_STATE || ".qa-reports/storage-state.json";
const launch = async () => {
const browser = await chromium.launch({ headless: true });
const ctxOpts = {};
try {
await fs.access(STORAGE);
ctxOpts.storageState = STORAGE;
} catch {}
const context = await browser.newContext(ctxOpts);
const page = await context.newPage();
const errors = [];
page.on("pageerror", (e) => errors.push({ type: "pageerror", message: e.message }));
page.on("console", (msg) => {
if (msg.type() === "error" || msg.type() === "warning") {
errors.push({ type: msg.type(), text: msg.text() });
}
});
page.on("requestfailed", (req) => errors.push({
type: "requestfailed",
url: req.url(),
failure: req.failure()?.errorText,
}));
return { browser, context, page, errors };
};
const safeGoto = async (page, url) => {
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {});
} catch (e) {
return { ok: false, error: e.message };
}
return { ok: true };
};
const annotate = async (page) => {
return page.evaluate(() => {
const targets = Array.from(document.querySelectorAll(
"a, button, input, select, textarea, [role=button], [role=link], [onclick]"
));
return targets.slice(0, 60).map((el, i) => {
const r = el.getBoundingClientRect();
const tag = el.tagName.toLowerCase();
const text = (el.innerText || el.value || el.getAttribute("aria-label") || "").trim().slice(0, 40);
return { i, tag, text, x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
});
};
(async () => {
if (cmd === "goto") {
const url = args[0];
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
const title = await page.title().catch(() => "");
const status = nav.ok ? 200 : 0;
console.log(JSON.stringify({ url, ok: nav.ok, title, status, errors }, null, 2));
if (process.env.QA_PERSIST_STORAGE === "1") {
await page.context().storageState({ path: STORAGE });
}
await browser.close();
} else if (cmd === "screenshot") {
const [url, out, ...rest] = args;
const full = rest.includes("--full");
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
await page.screenshot({ path: out, fullPage: full });
console.log(JSON.stringify({ url, ok: nav.ok, out, errors }, null, 2));
await browser.close();
} else if (cmd === "snapshot") {
const [url, out] = args;
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
const elements = await annotate(page);
await page.screenshot({ path: out, fullPage: false });
console.log(JSON.stringify({ url, ok: nav.ok, out, errors, elements }, null, 2));
await browser.close();
} else if (cmd === "console") {
const [url] = args;
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
console.log(JSON.stringify({ url, ok: nav.ok, errors }, null, 2));
await browser.close();
} else if (cmd === "links") {
const [url] = args;
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
const links = await page.$$eval("a[href]", (as) =>
as.map((a) => ({ href: a.href, text: a.innerText.trim().slice(0, 80) }))
.filter((l) => l.href && !l.href.startsWith("javascript:")),
);
const unique = Array.from(new Map(links.map((l) => [l.href, l])).values());
console.log(JSON.stringify({ url, ok: nav.ok, errors, links: unique }, null, 2));
await browser.close();
} else if (cmd === "eval") {
const [url, expr] = args;
const { browser, page, errors } = await launch();
const nav = await safeGoto(page, url);
const result = await page.evaluate(expr).catch((e) => ({ __evalError: e.message }));
console.log(JSON.stringify({ url, ok: nav.ok, errors, result }, null, 2));
await browser.close();
} else if (cmd === "viewport") {
const [url, size, out] = args;
const [w, h] = size.split("x").map(Number);
const { browser, page, errors } = await launch();
await page.setViewportSize({ width: w, height: h });
const nav = await safeGoto(page, url);
await page.screenshot({ path: out, fullPage: false });
console.log(JSON.stringify({ url, ok: nav.ok, viewport: { w, h }, out, errors }, null, 2));
await browser.close();
} else if (cmd === "flow") {
// Multi-step flow. Actions:
// goto, click, fill, press, wait (real time), waitFor (selector)
// clockInstall (virtual clock — must run BEFORE goto)
// fastForward (advance virtual time, fires queued timers)
// runFor (advance virtual time + drive Date)
// evalAssert (eval a JS expression, fail if falsy)
// screenshot, full (apply to any step)
const [flowFile, outDir] = args;
const flow = JSON.parse(await fs.readFile(flowFile, "utf8"));
const { browser, page, errors } = await launch();
const log = [];
for (const step of flow) {
try {
if (step.action === "goto") await safeGoto(page, step.url);
else if (step.action === "click") await page.click(step.selector, { timeout: 10000 });
else if (step.action === "fill") await page.fill(step.selector, step.value, { timeout: 10000 });
else if (step.action === "press") await page.press(step.selector || "body", step.value, { timeout: 10000 });
else if (step.action === "wait") await page.waitForTimeout(step.value || 500);
else if (step.action === "waitFor") await page.waitForSelector(step.selector, { timeout: 10000 });
else if (step.action === "clockInstall") await page.clock.install({ time: step.value ? new Date(step.value) : undefined });
else if (step.action === "fastForward") await page.clock.fastForward(step.value || 1000);
else if (step.action === "runFor") await page.clock.runFor(step.value || 1000);
else if (step.action === "evalAssert") {
const r = await page.evaluate(step.val
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [napnap11](https://github.com/napnap11)
- **Source:** [napnap11/claude-skills](https://github.com/napnap11/claude-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.