Enter to open · arrow keys to move · results come from a build-time index, no network calls

Answers reflect what the code in src/ actually does. Where the honest answer is "no" or "not yet", it says so.

Why not just paste a screenshot into the chat myself?#

You can, and for a one-off it is fine. Three things change when the assistant takes the capture itself. It gets the console and network errors alongside the pixels, which a screenshot cannot show — a hydration mismatch or a 404 hero image is invisible in an image. It gets the real DOM, so it edits a class that exists instead of the one it remembers writing. And it can recapture after its own edit to check the fix, without you doing another round trip. The last one is the biggest change in practice: verification stops depending on your attention.

Does it work with my framework?#

It renders a URL in Chromium, so the framework is irrelevant: Next.js, Vite, React, Vue, Svelte, Angular, Astro, Rails, Django, a static python -m http.server. If your dev server answers HTTP on a local port, it works. The only framework-shaped concern is timing — apps that render after hydration or a data fetch may need delay raised to 2000–3000 ms.

Can it inspect my staging or production site?#

No, and that is deliberate rather than unimplemented. Only localhost, *.localhost and private-network addresses are allowed. The URL is chosen by a model that may have read untrusted content, and VibeLens opens it in a real browser on your machine with your local network access — that is an SSRF primitive if unconstrained. The allowlist is the boundary. There is no flag to relax it, because an opt-out would be the first thing an injected prompt tried to talk the model into setting. See ADR 0003.

Why are hostnames rejected even when they resolve locally?#

Because resolving them is the vulnerability. If VibeLens looked up a hostname and checked the resulting IP, Chromium would then perform its own separate lookup milliseconds later — and an attacker-controlled DNS zone with a short TTL can return a private address to the check and a public one to the browser. Refusing hostnames outright removes the race. localhost and *.localhost are exempt because RFC 6761 reserves them as always-local, so they need no lookup.

Why only one tool? A screenshot tool would be simpler.#

The three signals are only useful together: the image shows the button overflows, the DOM shows which class is responsible, the console explains why the render does not match either. Splitting them means the model has to know which one it needs before it has looked, and three calls would be three browser launches of three possibly-different page states. One tool also removes the routing decision, which is where models most often make a wrong first call. See ADR 0004.

Why JPEG instead of PNG? The screenshot looks slightly soft.#

Because the payload is spent from your assistant's context window, and the tool is only useful if it can be called repeatedly. JPEG at quality 75 preserves layout, spacing, alignment, overflow and approximate colour, which is what the model reasons about. It does cost you fidelity: VibeLens is not suitable for visual regression diffing, and judging a 1px border or a subtle gradient is at the edge of what the image supports. See ADR 0006.

How much of my context window does one call cost?#

The image plus a JSON text block. The DOM is the field that would otherwise dominate, and the sanitizer collapses it hard: the test fixture's ~10 KB page comes back as ~680 characters with every Tailwind class intact. Every text field is also hard-capped — 20,000 characters of DOM, 160 per text node, 300 per attribute, 40 console entries, 20 failed requests. In normal use nothing reaches those caps; they exist for the pathological page.

Can it click a button, fill a form, or scroll before capturing?#

Not today. The tool is annotated readOnlyHint: true and it only observes: navigate, wait, read, capture. Pre-capture interaction is on the roadmap, and it is a genuine design change rather than a small feature, because it breaks the read-only guarantee that the annotation and the documentation currently promise.

Can it inspect a page that requires login?#

Not usefully. Every call creates a fresh browser context with no cookies, no localStorage and no service workers, so an authenticated route renders as logged-out or redirects to a login page. That isolation is intentional — it is what makes two captures of the same URL comparable — but it does mean authenticated pages are currently out of reach. If your app has a dev-mode bypass or a seeded session cookie set by the server, that works.

Does it leave browser processes running, and why not reuse one?#

No, it does not leak: each call launches its own Chromium and closes it in a finally block, which itself swallows close errors so a teardown failure cannot mask the real one. That strictness matters because the server lives for your whole IDE session, and a leaked Chromium costs hundreds of MB of RSS you would notice hours later as a slow laptop rather than as a failed call.

A pool would save the launch cost, but it has to answer eviction, idle timeouts, crash recovery and shutdown-on-disconnect, and every one of those answers is a new way to leak a browser — the exact failure the current design makes impossible. The usage pattern also does not reward it: calls arrive a handful of times per session, seconds to minutes apart, so a pooled browser would sit idle holding memory on a laptop already running your dev server and IDE. Reuse would additionally bleed state between captures. summary.captureMs is in every response, so if launch overhead ever dominates real usage there will be data to justify changing this.

Why do I still need npx playwright install chromium?#

Playwright manages its own pinned browser builds rather than using your system Chrome, which is what makes a capture reproducible from a lockfile. Installing the npm package does not download a browser; that is a separate one-time step of roughly 95 MB, shared across all projects on the machine. If you skip it, the tool returns BROWSER_NOT_INSTALLED with that exact command as the hint rather than failing opaquely.

The screenshot is blank. Is it broken?#

Usually it is timing. The capture waits for load, then up to 5 s of best-effort networkidle, then your delay (default 1000 ms). An app that renders after hydration or a fetch can be captured before content appears — raise delay to 2000–3000. Before assuming timing, read the text payload: a blank page with an entry in uncaughtPageErrors is a crashed render that no delay will fix, and a blank page with a 404 on its bundle in failedRequests is a build problem.

Is it safe to point this at a page with sensitive data?#

The capture stays on your machine except for one thing that matters: the screenshot and DOM are returned to your AI assistant, which for most hosts means they are sent to a model provider. Anything visible on the page — a customer record in a dev database, a token rendered into the DOM — travels with it. Treat a capture like pasting a screenshot into the chat, because that is effectively what it is.

Can a malicious page attack my assistant through the DOM snapshot?#

It can try, and this is a documented residual risk rather than something the code prevents. Text scraped from a rendered page could contain instructions aimed at your assistant. The allowlist bounds where the browser goes, not what the page says. The mitigation is on the consuming side: the DOM snapshot is data, never direction. Since VibeLens only reaches local addresses, the realistic exposure is content your own dev server renders from an untrusted source.

Why does my console.log never appear in the output?#

Only error and warning levels are recorded; console.log is dropped as the noisiest thing on a dev server. Use console.error while debugging with this tool. Uncaught exceptions are captured separately as uncaughtPageErrors, so a React render crash surfaces even if nothing was logged.

Is 192.168.x.x really allowed? That is my whole home network.#

Yes, and it is the loosest part of the allowlist. It is there because dev servers legitimately bind to a LAN address — you test on a phone against your laptop's IP, or run the server in a VM. The narrower rule would break a real workflow. What is excluded is the part that actually hurts: all of link-local 169.254.0.0/16, plus the specific metadata endpoints (169.254.169.254, 169.254.170.2, fd00:ec2::254, 100.100.100.200), which are checked before the allowlist so no future edit can accidentally permit them.

How do I know the whole pipeline works on my machine?#

From a clone: npm run typecheck && npm test. The suite is 63 vitest cases and it launches a real Chromium against a local fixture page that deliberately contains a console error, an uncaught exception, a 404 image, a base64 data URI and a huge SVG path — so a green run means the actual capture pipeline works, not a mocked one. Then node scripts/smoke.mjs, which spawns dist/index.js and completes an initialize -> tools/list -> tools/call exchange over real stdio. That second step is the only one that catches a stdout-corruption regression.

Do I need the Claude Code plugin, or is the MCP server enough?#

The server is enough; the plugin adds the workflow layer on top of it — skills (check-ui, responsive-audit, console-triage, a11y-audit, before-after), two subagents, and a PostToolUse hook that flags a frontend edit as unverified until it is re-captured — so you do not have to describe the workflow every time. Note that the plugin does not contain the server: its .mcp.json launches the published npm package, exactly like every other IDE config. That is why a cleanly installed plugin can still fail to connect if npm is unreachable, and why Chromium is still a separate install.