Building a Bug Hunter: Teaching Claude to QA Your Apps with Playwright
I built a Claude Code skill that combines static code analysis with Playwright browser testing to systematically find and fix bugs. Here's the five-phase workflow and what I learned about the gap between reading code and driving a browser.
I got tired of finding bugs by accident. A modal that scrolled past its buttons, a collapsible section that snapped instead of animating, raw JSON leaking into a dashboard card. The kind of bugs that aren't catastrophic but make you wince every time you see them. They accumulate because nobody's systematically looking.
So I built a skill that does the looking for me. It's a Claude Code skill called bug-hunter, a structured workflow that combines reading source code with driving a real browser via Playwright. Point it at a codebase and a URL, and it systematically explores, tests, triages, and fixes what it finds.
Here's how I designed it and what I learned about the gap between static analysis and dynamic testing.
The Problem with Finding Bugs
There are two traditional approaches to finding bugs, and each one misses what the other catches.
Reading code catches logic errors, missing null checks, unescaped user input, race conditions: things that are visible in the source if you're methodical enough to trace every path. But code reading can't tell you that a modal is 918 pixels tall in a 788-pixel viewport, or that an Alpine.js plugin is referenced but never loaded. Those are runtime problems.
Manual testing catches the runtime problems: broken layouts, missing animations, forms that don't validate, buttons that don't respond. But manual testing is slow, inconsistent, and terrible at catching the things code reading finds: the catch(e) {} that swallows errors silently, the API endpoint that returns 200 on failure.
The insight behind the bug hunter is simple: do both, systematically, in sequence. Read the code to understand the architecture, then drive a browser to test what the code actually produces. The code reading informs what to test; the testing reveals what the code reading missed.
The Five-Phase Workflow
The skill operates in five phases. Each one feeds into the next.
Phase 1: Recon
Before testing anything, understand the project. What framework? What's the frontend? Where are the routes defined? What's the data layer? Map the URL structure so you know what to test.
This sounds basic, but it's the phase that prevents wasted effort. If you don't know that the app is an Express.js SPA with client-side routing, you'll waste time testing server-rendered pages that don't exist. If you don't know the API endpoints, you can't test error handling on them.
What framework? (Express, FastAPI, Next.js, Rails, etc.)
What's the frontend? (React, HTMX, Jinja2, vanilla JS, etc.)
Where are the routes defined? (app.py, server.js, routes/, etc.)
What's the CSS approach? (Tailwind, vanilla, SCSS, CSS-in-JS)Phase 2: Static Analysis
Read the code systematically. Don't grep for patterns. Trace execution paths. Start at the route handler, follow the data through validation, business logic, and rendering. At each step, ask: what happens if this input is null? Empty? Huge? Malformed?
The key categories:
- Logic errors: off-by-one, wrong comparisons, missing awaits
- Error handling gaps: swallowed catches, missing validation, no error pages
- Security vectors: XSS, injection, missing CSRF, hardcoded credentials
- Frontend issues: broken links, missing loading states, accessibility gaps
Phase 3: Dynamic Testing (Playwright)
This is where the Playwright MCP tools earn their keep. Navigate to every route. Take snapshots. Check console errors. Then test interactively: fill forms with valid data, then empty data, then XSS payloads. Open modals, close them, reopen them. Click every button. Resize to mobile viewports.
The Playwright MCP gives Claude Code access to browser_navigate, browser_snapshot, browser_take_screenshot, browser_click, browser_fill_form, browser_resize, and browser_console_messages. That's enough to replicate what a thorough QA engineer does manually, but without the fatigue and inconsistency.
The dynamic testing is structured by test type:
- Smoke test: every page loads, no console errors, content renders
- Interactive: forms, buttons, modals, navigation
- Responsive: mobile (390×844), tablet (768×1024), desktop (1440×900)
- Edge cases: empty states, rapid clicks, long inputs, special characters
Phase 4: Triage
After testing, classify findings by severity. Critical means data loss or security holes. High means a feature is broken on a common path. Medium means it works but wrong. Low means it's a nitpick.
Present the full report to the user before fixing. Let them confirm priorities and opt out of any fixes they don't want. Some "bugs" are intentional behavior.
Phase 5: Fix
Work through confirmed bugs in severity order. One bug, one fix. Minimal diff: no drive-by refactoring. Verify every fix with Playwright. Check for regressions.
What Goes in the Skill vs. What the Model Figures Out
A Claude Code skill is a structured prompt that loads when triggered. The key design decision is what to specify explicitly and what to leave to the model's judgment.
Explicit in the skill:
- The five-phase sequence (don't skip recon, don't fix before triaging)
- The severity classification rubric (consistent definitions across runs)
- The bug report format (reproducible, with steps and evidence)
- The fix protocol (one-at-a-time, minimal, verified)
- What to check in each test type (forms, modals, responsive breakpoints)
Left to the model:
- Which routes to test first (it reads the code and prioritizes)
- What XSS payloads to try (it knows the common ones)
- How to trace execution paths (it understands the framework)
- How to fix each specific bug (it reads the context and writes minimal code)
- Whether a finding is a bug or intentional (it asks when unsure)
The skill is about 200 lines of structured instructions. It's a workflow, not a script. The model brings the judgment; the skill brings the discipline.
What I Learned
The combination is more than the sum. Static analysis found a rendering function that fell through to JSON.stringify() when the input was an object instead of an array. Playwright found that this rendered as raw JSON in a dashboard card. Neither approach alone would have caught both the root cause and the user-facing symptom.
Console warnings are signal. Alpine.js was logging "You can't use [x-collapse]" six times per page load across 20+ pages. That's 100+ warnings per session, invisible to the user but a clear indicator that a plugin was missing. Playwright's browser_console_messages caught this instantly.
Screenshots change the conversation. When the model takes a screenshot of a modal with its buttons clipped below the viewport, there's no ambiguity about whether it's a real bug. The visual evidence makes triage instant.
The five-phase structure prevents thrashing. Without the explicit workflow, the model would jump between reading code and testing, potentially fixing things before understanding the full scope. The phases force a complete survey before any repairs.
The bug hunter skill is available in my homelab repos. It works on any web application with a running URL and accessible source code. Point it at your project and see what it finds. I was surprised by what it caught in apps I'd been using daily for weeks.