
Browser
- 396 installs
- 303 repo stars
- Updated April 20, 2026
- iamzhihuix/happy-claude-skills
browser is an agent skill that drives real browser sessions for UI verification, authenticated form flows, live web research, and interactive testing inside autonomous agent loops.
About
browser is a skill from iamzhihuix/happy-claude-skills that instructs coding agents to control real browser sessions for UI verification, multi-step form flows, authenticated testing, and live web research. Instead of guessing DOM behavior from static code, the agent opens pages, clicks elements, fills inputs, and observes rendered results. Developers reach for browser during pre-release QA when Stripe checkout, OAuth login, or dashboard widgets need end-to-end confirmation. The skill also supports research tasks that require navigating authenticated or dynamic SPAs. It integrates with agent tool loops where shell-only inspection cannot validate client-side JavaScript behavior or session cookies.
- Browser session control patterns
- Navigation, click, and form automation
- Authenticated flow handling
- Screenshot and DOM inspection hooks
- Safer pacing for flaky SPAs
Browser by the numbers
- 396 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,997 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/iamzhihuix/happy-claude-skills --skill browserAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 396 |
|---|---|
| repo stars | ★ 303 |
| Last updated | April 20, 2026 |
| Repository | iamzhihuix/happy-claude-skills ↗ |
How do agents test authenticated web UI flows?
Drive real browser sessions for UI verification, form flows, authenticated testing, and live web research inside agent loops.
Who is it for?
Developers running agent-assisted QA on SPAs, OAuth flows, or multi-step web forms that require a real browser rather than static code review.
Skip if: Skip browser when you only need unit tests with Jest/Vitest mocks or headless CI pipelines already covered by Playwright config files.
When should I use this skill?
User asks the agent to open a URL, test a login flow, verify a deployed UI, or research a live authenticated web application.
What you get
Verified page snapshots, completed form submissions, session-authenticated navigation logs, and UI regression notes.
Files
Browser Tools
Minimal CDP tools for collaborative site exploration and scraping.
Credits: Based on Mario Zechner's article What if you don't need MCP?, adapted from Factory.ai.
Setup
Before first use, install dependencies:
npm install --prefix skills/browserStart Chrome
./skills/browser/scripts/start.js # Fresh profile
./skills/browser/scripts/start.js --profile # Copy your profile (cookies, logins)Start Chrome on :9222 with remote debugging.
Navigate
./skills/browser/scripts/nav.js https://example.com
./skills/browser/scripts/nav.js https://example.com --newNavigate current tab or open new tab.
Evaluate JavaScript
./skills/browser/scripts/eval.js 'document.title'
./skills/browser/scripts/eval.js 'document.querySelectorAll("a").length'Execute JavaScript in active tab (async context).
IMPORTANT: The code must be a single expression or use IIFE for multiple statements:
- Single expression:
'document.title' - Multiple statements:
'(() => { const x = 1; return x + 1; })()' - Avoid newlines in the code string - keep it on one line
Screenshot
./skills/browser/scripts/screenshot.jsScreenshot current viewport, returns temp file path.
Pick Elements
./skills/browser/scripts/pick.js "Click the submit button"Interactive element picker. Click to select, Cmd/Ctrl+Click for multi-select, Enter to finish.
Workflow
1. Start Chrome with start.js --profile to mirror your authenticated state. 2. Drive navigation via nav.js https://target.app or open secondary tabs with --new. 3. Inspect the DOM using eval.js for quick counts, attribute checks, or extracting JSON payloads. 4. Capture artifacts with screenshot.js for visual proof or pick.js when you need precise selectors or text snapshots.
Usage Notes
- Start Chrome first before using other tools
- The
--profileflag syncs your actual Chrome profile so you're logged in everywhere - JavaScript evaluation runs in an async context in the page
- Pick tool allows you to visually select DOM elements by clicking on them
{
"name": "browser-tools",
"version": "1.0.0",
"description": "Minimal Chrome DevTools Protocol tools for browser automation",
"type": "module",
"scripts": {
"postinstall": "chmod +x scripts/*.js"
},
"dependencies": {
"puppeteer-core": "^24.0.0"
}
}
#!/usr/bin/env node
import puppeteer from "puppeteer-core";
const code = process.argv.slice(2).join(" ");
if (!code) {
console.log("Usage: eval.js 'code'");
console.log("\nExamples:");
console.log(' eval.js "document.title"');
console.log(' eval.js "document.querySelectorAll(\'a\').length"');
console.log("\nNote: The code must be a single expression or use IIFE for multiple statements:");
console.log(" Single expression: 'document.title'");
console.log(" Multiple statements: '(() => { const x = 1; return x + 1; })()'");
process.exit(1);
}
const b = await puppeteer.connect({
browserURL: "http://localhost:9222",
defaultViewport: null,
});
const p = (await b.pages()).at(-1);
if (!p) {
console.error("✗ No active tab found");
process.exit(1);
}
const result = await p.evaluate((c) => {
const AsyncFunction = (async () => {}).constructor;
return new AsyncFunction(`return (${c})`)();
}, code);
if (Array.isArray(result)) {
for (let i = 0; i < result.length; i++) {
if (i > 0) console.log("");
for (const [key, value] of Object.entries(result[i])) {
console.log(`${key}: ${value}`);
}
}
} else if (typeof result === "object" && result !== null) {
for (const [key, value] of Object.entries(result)) {
console.log(`${key}: ${value}`);
}
} else {
console.log(result);
}
await b.disconnect();
#!/usr/bin/env node
import puppeteer from "puppeteer-core";
const url = process.argv[2];
const newTab = process.argv[3] === "--new";
if (!url) {
console.log("Usage: nav.js <url> [--new]");
console.log("\nExamples:");
console.log(" nav.js https://example.com # Navigate current tab");
console.log(" nav.js https://example.com --new # Open in new tab");
process.exit(1);
}
const b = await puppeteer.connect({
browserURL: "http://localhost:9222",
defaultViewport: null,
});
if (newTab) {
const p = await b.newPage();
await p.goto(url, { waitUntil: "domcontentloaded" });
console.log("✓ Opened:", url);
} else {
const p = (await b.pages()).at(-1);
await p.goto(url, { waitUntil: "domcontentloaded" });
console.log("✓ Navigated to:", url);
}
await b.disconnect();
#!/usr/bin/env node
import puppeteer from "puppeteer-core";
const message = process.argv.slice(2).join(" ");
if (!message) {
console.log("Usage: pick.js 'message'");
console.log("\nExample:");
console.log(' pick.js "Click the submit button"');
console.log("\nInteractive element picker:");
console.log(" - Click to select element");
console.log(" - Cmd/Ctrl+Click for multi-select");
console.log(" - Enter to finish");
console.log(" - ESC to cancel");
process.exit(1);
}
const b = await puppeteer.connect({
browserURL: "http://localhost:9222",
defaultViewport: null,
});
const p = (await b.pages()).at(-1);
if (!p) {
console.error("✗ No active tab found");
process.exit(1);
}
// Inject pick() helper into current page
await p.evaluate(() => {
if (!window.pick) {
window.pick = async (message) => {
if (!message) {
throw new Error("pick() requires a message parameter");
}
return new Promise((resolve) => {
const selections = [];
const selectedElements = new Set();
const overlay = document.createElement("div");
overlay.style.cssText =
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
const highlight = document.createElement("div");
highlight.style.cssText =
"position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
overlay.appendChild(highlight);
const banner = document.createElement("div");
banner.style.cssText =
"position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";
const updateBanner = () => {
banner.textContent = `${message} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
};
updateBanner();
document.body.append(banner, overlay);
const cleanup = () => {
document.removeEventListener("mousemove", onMove, true);
document.removeEventListener("click", onClick, true);
document.removeEventListener("keydown", onKey, true);
overlay.remove();
banner.remove();
selectedElements.forEach((el) => {
el.style.outline = "";
});
};
const onMove = (e) => {
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || overlay.contains(el) || banner.contains(el)) return;
const r = el.getBoundingClientRect();
highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${r.top}px;left:${r.left}px;width:${r.width}px;height:${r.height}px`;
};
const buildElementInfo = (el) => {
const parents = [];
let current = el.parentElement;
while (current && current !== document.body) {
const parentInfo = current.tagName.toLowerCase();
const id = current.id ? `#${current.id}` : "";
const cls = current.className
? `.${current.className.trim().split(/\s+/).join(".")}`
: "";
parents.push(parentInfo + id + cls);
current = current.parentElement;
}
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
class: el.className || null,
text: el.textContent?.trim().slice(0, 200) || null,
html: el.outerHTML.slice(0, 500),
parents: parents.join(" > "),
};
};
const onClick = (e) => {
if (banner.contains(e.target)) return;
e.preventDefault();
e.stopPropagation();
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || overlay.contains(el) || banner.contains(el)) return;
if (e.metaKey || e.ctrlKey) {
if (!selectedElements.has(el)) {
selectedElements.add(el);
el.style.outline = "3px solid #10b981";
selections.push(buildElementInfo(el));
updateBanner();
}
} else {
cleanup();
const info = buildElementInfo(el);
resolve(selections.length > 0 ? selections : info);
}
};
const onKey = (e) => {
if (e.key === "Escape") {
e.preventDefault();
cleanup();
resolve(null);
} else if (e.key === "Enter" && selections.length > 0) {
e.preventDefault();
cleanup();
resolve(selections);
}
};
document.addEventListener("mousemove", onMove, true);
document.addEventListener("click", onClick, true);
document.addEventListener("keydown", onKey, true);
});
};
}
});
const result = await p.evaluate((msg) => window.pick(msg), message);
if (Array.isArray(result)) {
for (let i = 0; i < result.length; i++) {
if (i > 0) console.log("");
for (const [key, value] of Object.entries(result[i])) {
console.log(`${key}: ${value}`);
}
}
} else if (typeof result === "object" && result !== null) {
for (const [key, value] of Object.entries(result)) {
console.log(`${key}: ${value}`);
}
} else {
console.log(result);
}
await b.disconnect();
#!/usr/bin/env node
import { tmpdir } from "node:os";
import { join } from "node:path";
import puppeteer from "puppeteer-core";
const b = await puppeteer.connect({
browserURL: "http://localhost:9222",
defaultViewport: null,
});
const p = (await b.pages()).at(-1);
if (!p) {
console.error("✗ No active tab found");
process.exit(1);
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const filepath = join(tmpdir(), filename);
await p.screenshot({ path: filepath });
console.log(filepath);
await b.disconnect();
#!/usr/bin/env node
import { spawn, execSync } from "node:child_process";
import puppeteer from "puppeteer-core";
const useProfile = process.argv[2] === "--profile";
if (process.argv[2] && process.argv[2] !== "--profile") {
console.log("Usage: start.js [--profile]");
console.log("\nOptions:");
console.log(" --profile Copy your default Chrome profile (cookies, logins)");
console.log("\nExamples:");
console.log(" start.js # Start with fresh profile");
console.log(" start.js --profile # Start with your Chrome profile");
process.exit(1);
}
// Kill existing Chrome
try {
execSync("killall 'Google Chrome'", { stdio: "ignore" });
} catch {}
// Wait a bit for processes to fully die
await new Promise((r) => setTimeout(r, 1000));
// Setup profile directory
execSync("mkdir -p ~/.cache/browser-tools", { stdio: "ignore" });
if (useProfile) {
// Sync profile with rsync (much faster on subsequent runs)
// Adjust the path based on your system
const profilePath = process.env["HOME"] + "/Library/Application Support/Google/Chrome/";
execSync(
`rsync -a --delete "${profilePath}" ~/.cache/browser-tools/`,
{ stdio: "pipe" },
);
}
// Start Chrome in background (detached so Node can exit)
spawn(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
["--remote-debugging-port=9222", `--user-data-dir=${process.env["HOME"]}/.cache/browser-tools`],
{ detached: true, stdio: "ignore" },
).unref();
// Wait for Chrome to be ready by attempting to connect
let connected = false;
for (let i = 0; i < 30; i++) {
try {
const browser = await puppeteer.connect({
browserURL: "http://localhost:9222",
defaultViewport: null,
});
await browser.disconnect();
connected = true;
break;
} catch {
await new Promise((r) => setTimeout(r, 500));
}
}
if (!connected) {
console.error("✗ Failed to connect to Chrome");
process.exit(1);
}
console.log(`✓ Chrome started on :9222${useProfile ? " with your profile" : ""}`);
Related skills
FAQ
What can the browser skill verify?
The browser skill drives real browser sessions so agents can verify rendered UI, complete authenticated form flows, test multi-step checkout or login paths, and research live web pages that static code reading cannot validate.
How is browser different from unit tests?
The browser skill exercises live rendered pages with real clicks, cookies, and JavaScript execution inside agent loops, while unit tests mock components—browser catches session, routing, and client-side bugs invisible to isolated test runners.