How to build a self-verifying browser agent with Claude Code and MegaIndex Browser API
30 july 2026
Author: admin

How to build a self-verifying browser agent with Claude Code and MegaIndex Browser API

practical architecture in which Claude Code writes browser automation, MegaIndex BrowserAPI runs it, and deterministic checks decide whether the task is actually complete.

Claude Code can write a Playwright workflow, fix selectors, add retries, and explain why the implementation should work.

None of that proves that the browser task succeeds.

The page may render differently in a remote session. A login redirect may appear only with a fresh profile. A selector may find a hidden template instead of the visible element. A proxy may return a different regional version of the site. The script may even produce valid JSON while extracting the wrong records.

For browser automation, code review is only half of the verification process.

The stronger approach is to build a closed feedback loop:

Claude Code changes the automation ↓ MegaIndex BrowserAPI runs the real browser scenario ↓ The verification layer collects evidence ↓ Claude receives the failure and repairs the workflow ↓ The task ends only after the evidence passes

This turns Claude from a code generator into an agent working against a measurable browser contract.



The missing layer in AI browser development


Most Claude Code workflows stop too early.

The agent edits the project, runs the test suite, and returns a summary:

Updated the login flow. Improved selector handling. Added retries. All tests pass.

That summary may be accurate. It may also hide several problems:

  • only mocked HTML was tested;
  • the real website returned a login page;
  • the browser opened with the wrong profile;
  • the proxy configuration was ignored;
  • the expected element existed but was not visible;
  • the extractor returned incomplete records;
  • the browser session remained open after failure;
  • a test was weakened to accept the new behavior.

A reliable browser agent needs an independent definition of success.

MegaIndex BrowserAPI provides the execution environment. Claude Code provides the implementation and repair loop. A deterministic verification layer sits between them.

Component Responsibility
Claude Code Reads the repository, changes the automation, and fixes failures
MegaIndex BrowserAPI Runs the workflow in a remote browser session
Playwright or Puppeteer Controls navigation, interaction, and extraction
Verification script Checks page state, output, cleanup, and generated artifacts
Stop hook Prevents Claude from finishing when verification fails


The important distinction is simple:

Claude may decide how to fix the workflow, but it should not decide whether the workflow passed.

Define a browser contract before writing code


A browser task should have a precise acceptance contract.

“Open the product page and collect the data” is not precise enough. Claude can satisfy that request in several incompatible ways.

A useful contract describes:

  • the starting URL;
  • the browser profile to use;
  • whether a custom proxy is required;
  • the page state that confirms successful navigation;
  • the interaction sequence;
  • the expected output schema;
  • the minimum acceptable result;
  • the cleanup behavior;
  • the evidence that must be saved.

For example:

Browser scenario: authenticated product extraction Input: - target URL; - optional profile ID; - optional custom proxy; - expected account name. Success conditions: - BrowserAPI connection is established. - The target URL opens without returning to the login page. - The expected account name is visible. - At least five product cards are found. - Every product contains title, URL, price, and identifier. - A screenshot and JSON result are saved. - The browser session closes after success or failure. Failure conditions: - login page detected; - challenge page detected; - expected account missing; - product count below five; - required product field missing; - browser session not closed.

This contract gives Claude a target that can be tested.

It also prevents the agent from redefining success during implementation.

Separate implementation from acceptance


The browser code and the acceptance code should not be the same function.

If Claude writes both the workflow and the only test of that workflow in one place, it can accidentally reproduce the same assumption twice.

A better structure is:

src/ ├── browser/ │ ├── connect-browser.js │ ├── create-session.js │ └── close-session.js ├── workflows/ │ └── collect-products.js ├── validation/ │ └── validate-products.js └── artifacts/ └── save-run-artifacts.js scripts/ └── verify-product-workflow.mjs

The application workflow performs the task.

The verification script calls the workflow, observes the result, and checks it against the browser contract.

This gives the verifier permission to reject technically successful but useless outcomes.

For example, navigation may succeed while the application has actually opened:

  • a login page;
  • an access-denied page;
  • a regional redirect;
  • an empty search result;
  • a challenge page;
  • a partially rendered application shell.

A verification layer must distinguish these states from the intended page.

Connect the verification script to MegaIndex BrowserAPI


The connection details should come from environment variables.

Claude needs to know the variable names, but it does not need to read or print their values.

Install Playwright Core:

npm install --save-dev playwright-core

Create scripts/verify-product-workflow.mjs:

import fs from "node:fs/promises"; import process from "node:process"; import { chromium } from "playwright-core"; const browserEndpoint = process.env.MEGAINDEX_BROWSER_WS_ENDPOINT; const targetUrl = process.env.BROWSER_TEST_URL; const expectedAccount = process.env.BROWSER_EXPECTED_ACCOUNT; const artifactDirectory = process.env.BROWSER_ARTIFACT_DIRECTORY ?? "artifacts"; const minimumProducts = Number.parseInt( process.env.BROWSER_MINIMUM_PRODUCTS ?? "5", 10, ); if (!browserEndpoint) { throw new Error( "MEGAINDEX_BROWSER_WS_ENDPOINT is required", ); } if (!targetUrl) { throw new Error( "BROWSER_TEST_URL is required", ); } if (!expectedAccount) { throw new Error( "BROWSER_EXPECTED_ACCOUNT is required", ); } if ( !Number.isInteger(minimumProducts) || minimumProducts < 1 ) { throw new Error( "BROWSER_MINIMUM_PRODUCTS must be a positive integer", ); } await fs.mkdir(artifactDirectory, { recursive: true, }); const resultPath = `${artifactDirectory}/browser-result.json`; const screenshotPath = `${artifactDirectory}/browser-page.png`; const pageHtmlPath = `${artifactDirectory}/browser-page.html`; let browser; let page; const startedAt = new Date().toISOString(); try { browser = await chromium.connectOverCDP( browserEndpoint, ); const context = browser.contexts()[0] ?? await browser.newContext(); page = context.pages()[0] ?? await context.newPage(); const response = await page.goto( targetUrl, { waitUntil: "domcontentloaded", timeout: 60000, }, ); if (!response) { throw new Error( "Navigation returned no HTTP response", ); } if (!response.ok()) { throw new Error( `Navigation failed with HTTP status ${response.status()}`, ); } await page .locator("[data-account-name]") .waitFor({ state: "visible", timeout: 30000, }); const accountName = ( await page .locator("[data-account-name]") .first() .textContent() )?.trim() ?? ""; if (accountName !== expectedAccount) { throw new Error( `Unexpected account. Expected "${expectedAccount}", received "${accountName}"`, ); } const loginFormVisible = await page .locator( "form[action*='login'], input[type='password']", ) .first() .isVisible() .catch(() => false); if (loginFormVisible) { throw new Error( "The browser was redirected to a login page", ); } const challengeVisible = await page .locator( "[data-captcha], iframe[src*='captcha'], iframe[src*='challenge']", ) .first() .isVisible() .catch(() => false); if (challengeVisible) { throw new Error( "A challenge page was detected instead of the target content", ); } const products = await page .locator("[data-product]") .evaluateAll((elements) => elements.map((element) => ({ title: element .querySelector("[data-title]") ?.textContent ?.trim() ?? "", price: element .querySelector("[data-price]") ?.textContent ?.trim() ?? "", url: element .querySelector("a") ?.href ?? "", id: element.getAttribute( "data-product-id", ) ?? "", })), ); if (products.length < minimumProducts) { throw new Error( `Expected at least ${minimumProducts} products, received ${products.length}`, ); } const invalidProduct = products.find( (product) => !product.title || !product.price || !product.url || !product.id, ); if (invalidProduct) { throw new Error( `Invalid product record: ${JSON.stringify(invalidProduct)}`, ); } await page.screenshot({ path: screenshotPath, fullPage: true, }); await fs.writeFile( pageHtmlPath, await page.content(), "utf8", ); const result = { passed: true, startedAt, finishedAt: new Date().toISOString(), requestedUrl: targetUrl, finalUrl: page.url(), title: await page.title(), httpStatus: response.status(), accountName, productCount: products.length, products, artifacts: { screenshotPath, pageHtmlPath, }, }; await fs.writeFile( resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8", ); console.log( JSON.stringify(result), ); } catch (error) { if (page) { await page .screenshot({ path: screenshotPath, fullPage: true, }) .catch(() => undefined); await fs .writeFile( pageHtmlPath, await page.content(), "utf8", ) .catch(() => undefined); } const result = { passed: false, startedAt, finishedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error), finalUrl: page?.url() ?? null, artifacts: { screenshotPath, pageHtmlPath, }, }; await fs.writeFile( resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8", ); console.error( JSON.stringify(result), ); process.exitCode = 1; } finally { await browser ?.close() .catch(() => undefined); }

This example verifies more than connectivity.

It checks:

  • the HTTP response;
  • the authenticated account;
  • the absence of a login form;
  • the absence of an obvious challenge page;
  • the minimum number of results;
  • the required fields in each record;
  • the screenshot;
  • the saved HTML;
  • browser cleanup.

The selectors should be adapted to the target workflow, but the verification pattern remains the same.

Save evidence, not just logs


A failed browser run is difficult to diagnose from one exception message.

The verification layer should save an artifact bundle.

Artifact Why it matters
JSON result Provides structured status, extracted records, URLs, and error details
Screenshot Shows what the remote browser actually rendered
HTML snapshot Allows selector and page-state inspection after the session closes
Trace Shows navigation, requests, actions, and timing
Console log Reveals client-side JavaScript failures
Network summary Confirms whether required requests completed


This evidence is useful to both the developer and Claude.

Instead of receiving:

Selector timeout after 30000 ms

Claude can inspect:

  • the final URL;
  • the screenshot;
  • the saved HTML;
  • the failed request;
  • the page title;
  • the account state;
  • the extracted result.

That produces better repairs and fewer speculative changes.

Classify failures before asking Claude to fix them


Not every failure should trigger the same response.

A useful browser agent separates failures into categories.

Failure type Example Expected response
Connection BrowserAPI WebSocket connection failed Retry with a strict limit
Navigation Timeout before the target page loaded Inspect response, redirect, and ready condition
Authentication Session returned to the login page Check profile selection and session persistence
Page state Expected account or page marker missing Inspect screenshot and HTML before changing selectors
Extraction Required product field is empty Review DOM structure and validation rules
Challenge Challenge page detected Stop treating the page as valid content
Cleanup Remote session remains open Repair ownership and finally-block cleanup
Business validation Too few records returned Check pagination, filtering, and page state


This prevents Claude from applying generic retries to every failure.

Retries are appropriate for some connection and navigation errors. They are not appropriate when:

  • the wrong account is open;
  • the selector targets the wrong element;
  • a challenge page is present;
  • the output schema is invalid;
  • an action may create duplicate records.

Create a verification skill for browser changes


Claude should not receive the entire verification procedure in every prompt.

Store it as a skill that becomes relevant when browser code changes.

Create:

.claude/skills/verify-browser-workflow/SKILL.md

Use:

--- name: verify-browser-workflow description: Use after changes to MegaIndex BrowserAPI connections, Playwright or Puppeteer navigation, browser profiles, proxies, sessions, selectors, extraction, retries, or browser tests. --- # Verify the browser workflow 1. Read the current git diff. 2. Identify the affected browser scenarios. 3. Check whether browser ownership, profile selection, proxy precedence, or cleanup changed. 4. Run unit tests, linting, and type checking. 5. Run the affected scenario through MegaIndex BrowserAPI. 6. Inspect the JSON result, screenshot, and saved HTML. 7. Confirm: - the expected page was reached; - authentication state is correct; - no challenge or login page was accepted as valid content; - extracted records satisfy the output contract; - the browser session closed; - no test was skipped, deleted, or weakened. 8. Report the commands, exit codes, artifacts, changed tests, and final result.

The skill gives Claude a repeatable review procedure.

It still does not guarantee that Claude will perform every step. That is the hook’s job.

Block completion with a Stop hook


A Stop hook runs when Claude tries to finish its turn.

It can reject completion when the browser verification fails.

Create:

.claude/hooks/verify-browser-before-stop.sh

#!/usr/bin/env bash set -uo pipefail FAILED=0 run_check() { local name="$1" shift echo "Running ${name}..." if "$@"; then echo "${name}: passed" else echo "${name}: failed" >&2 FAILED=1 fi } run_check \ "unit tests" \ npm test run_check \ "lint" \ npm run lint run_check \ "type checking" \ npm run typecheck run_check \ "MegaIndex BrowserAPI verification" \ npm run verify:browser if [[ "$FAILED" -ne 0 ]]; then echo "Browser acceptance checks failed. Inspect the artifacts and continue working." >&2 exit 2 fi echo "All browser acceptance checks passed." exit 0

Register it in:

.claude/settings.json

{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-browser-before-stop.sh", "timeout": 600 } ] } ] } }

The blocking exit code is 2.

Code 1 may report an error without producing the required blocking behavior. A hook intended to prevent completion must be tested specifically for this condition.

Keep the BrowserAPI rules narrow


Permanent repository rules should describe constraints that apply to most browser tasks.

A focused CLAUDE.md could contain:

# MegaIndex BrowserAPI project rules - Reuse the existing BrowserAPI connection module. - Read connection details only from environment variables. - Never print or commit WebSocket credentials, cookies, tokens, or proxy passwords. - Keep browser session ownership explicit. - Close every remote browser session after success or failure. - A custom proxy overrides the proxy assigned to the selected profile. - Do not retry actions that may create duplicate external effects. - Do not replace event-based waits with fixed delays. - Do not accept login, challenge, or error pages as valid target content. - Do not skip, delete, or weaken tests to make a workflow pass. - Browser changes are complete only after remote verification succeeds.

Avoid filling this file with website-specific selectors, full extraction schemas, or troubleshooting histories.

Those details belong in task-specific skills and references.

Add a cold review after the implementation passes


The Claude session that wrote the code has already committed to an approach.

After several repair cycles, it may overlook a weakness because the reasoning that produced the implementation remains in context.

A fresh Claude session can review the diff without that history.

Use a read-only prompt:

Review the current diff as an independent browser automation reviewer. Do not modify files. Check: 1. MegaIndex BrowserAPI connection ownership. 2. Browser cleanup after success and failure. 3. Profile and custom proxy precedence. 4. Authentication state validation. 5. Retry limits and non-idempotent actions. 6. Login, challenge, and error page detection. 7. Extraction validation. 8. Test changes that weaken existing guarantees. 9. Credential exposure. 10. Whether the saved evidence proves the browser contract. Return only concrete findings with file paths and reasons.

This is useful for changes involving:

  • authentication;
  • payments;
  • account modifications;
  • large extraction workflows;
  • session reuse;
  • proxy routing;
  • multiple target websites.

Use structured output in CI


The same workflow can run without the interactive Claude Code interface.

First, run deterministic checks:

set -euo pipefail npm test npm run lint npm run typecheck npm run verify:browser

Then run a read-only Claude review:

claude -p \ --permission-mode plan \ --output-format json \ --max-turns 5 \ "Review the current diff for BrowserAPI regressions. Check session ownership, cleanup, profile and proxy precedence, authentication validation, retry behavior, extraction assertions, test weakening, secret exposure, and the generated verification artifacts." \ > artifacts/claude-browser-review.json

The CI status should depend on actual command exit codes.

Claude’s structured review is an additional inspection layer, not a replacement for the tests.

Recommended repository layout


project/ ├── CLAUDE.md ├── .claude/ │ ├── settings.json │ ├── hooks/ │ │ └── verify-browser-before-stop.sh │ └── skills/ │ └── verify-browser-workflow/ │ ├── SKILL.md │ └── reference.md ├── scripts/ │ └── verify-product-workflow.mjs ├── src/ │ ├── browser/ │ ├── workflows/ │ ├── validation/ │ └── artifacts/ ├── tests/ ├── artifacts/ └── package.json

Each part has one role:

  • CLAUDE.md contains permanent constraints.
  • Skills describe repeatable browser procedures.
  • Hooks enforce acceptance checks.
  • Browser scripts run real scenarios through MegaIndex BrowserAPI.
  • Artifacts preserve evidence for Claude and human review.
  • Tests validate local behavior and browser contracts.

Prompt for creating the self-verifying setup


Inspect this repository and create a self-verifying workflow for its MegaIndex BrowserAPI automation. Do not expose credentials. 1. Identify the existing BrowserAPI connection, session ownership, profile, proxy, extraction, and cleanup code. 2. Define the browser acceptance contract for the primary workflow: - required input; - expected authenticated state; - expected page state; - minimum output; - required output fields; - failure conditions; - cleanup requirements; - artifacts to save. 3. Create a verification script that: - reads the BrowserAPI endpoint from the environment; - connects over CDP; - runs the primary workflow; - rejects login, challenge, and error pages; - validates the extracted output; - saves JSON, screenshot, and HTML artifacts; - closes the browser after success or failure; - exits non-zero when the contract is not satisfied. 4. Create a Claude Code verification skill for changes affecting: - BrowserAPI connections; - sessions; - profiles; - proxies; - navigation; - selectors; - extraction; - retries; - browser tests. 5. Create a Stop hook that runs: - unit tests; - lint; - type checking; - BrowserAPI verification. The hook must exit with code 2 when any required check fails. 6. Audit the current diff and confirm: - no test was deleted, skipped, or weakened; - no credential can appear in logs; - browser cleanup occurs on every path; - retries cannot repeat non-idempotent actions. Return: - browser contract; - files changed; - commands executed; - artifacts produced; - verification result; - unresolved risks.

What changes after this setup


Without an acceptance layer, Claude finishes when the code looks complete.

With a self-verifying BrowserAPI workflow, Claude finishes only when the browser evidence satisfies the contract.

The development loop becomes:

Define the browser contract ↓ Let Claude implement the change ↓ Run the scenario through MegaIndex BrowserAPI ↓ Collect JSON, screenshot, HTML, and test results ↓ Feed failures back to Claude ↓ Block completion until every required check passes ↓ Review the final diff from a fresh context

This setup does not eliminate human review.

It removes the need to supervise every command and gives the reviewer something more useful than a success summary: a reproducible browser run with evidence.

Discussion

To add a comment, please log in