Generated banner for Autobrowse
Generated logo for Autobrowse

Autobrowse

BrowserbaseOfficialCreated by browserbase

  • Skill
  • Workflow
  • Automation and integrations
  • MIT
  • Low risk
  • Has scripts

From the author

Self-improving browser automation via the auto-research loop. Iteratively runs a browsing task, reads the trace, and improves the navigation skill (strategy.md) until it reliably passes. Supports parallel runs across multiple tasks using sub-agents. Use when you want to build or improve browser automation skills for specific website tasks.
— the description by browserbase

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

AutoBrowse is a development framework designed to create reliable browser automation skills. It operates by running an inner agent through a browsing task, capturing the execution trace, and allowing an outer agent to analyze the results and refine the navigation strategy. This iterative process continues until the task passes consistently. It supports both single-task development and parallel multi-task training, with optional integration for deep browser-level observability via browser-trace.

What it does

  • Automates the evaluate-improve cycle for browser navigation tasks.
  • Supports parallel training of multiple browser tasks using sub-agents.
  • Provides deep observability into browser events when integrated with browser-trace.
  • Generates structured, self-contained skill files for use in Claude Code.
  • Supports code generation for frameworks like Playwright and Stagehand.
  • Maintains a workspace for training artifacts, keeping the skill source read-only.

When to use it

  • When you need to build a new browser automation skill from scratch.
  • When an existing automation task is failing and needs iterative debugging.
  • When you need to develop multiple browser skills simultaneously.
  • When you want to generate deterministic scripts for frameworks like Playwright.

Advantages

  • Evidence-based improvements grounded in trace data.
  • Reduces manual trial-and-error by automating the feedback loop.
  • Supports complex debugging through unified event streams.
  • Produces clean, portable skill documentation upon graduation.

Usage examples

“/autobrowse --task google-flights”
Initiates the self-improving loop for the google-flights task, running iterations to refine the strategy.md file.
“/autobrowse book a flight on delta.com”
Parses the natural language request, creates a new task definition, and begins the training loop.
“/autobrowse --tasks google-flights,amazon-add-to-cart”
Spawns parallel sub-agents to train both tasks simultaneously, reporting progress for each.
“/autobrowse --task google-flights --browser-trace”
Runs the training loop with deep CDP-level observability enabled to diagnose complex site interactions.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/browserbase/autobrowse/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip autobrowse.zip -d .agents/skills

Documentation by browserbase

AutoBrowse — Self-Improving Browser Skill

Build reliable browser automation skills through iterative experimentation. An inner agent browses the site (evaluate.ts). You — the outer agent — read what happened and improve the instructions (strategy.md). Repeat until it passes consistently.

Entry Points

Invocation is flexible — both explicit flags and free-form natural language work:

/autobrowse --task google-flights
/autobrowse --task google-flights --iterations 10 --env remote
/autobrowse --task google-flights --browser-trace
/autobrowse --tasks google-flights,amazon-add-to-cart
/autobrowse --all

# Also fine — parse freely:
/autobrowse https://flights.google.com/
/autobrowse book a flight on delta.com
/autobrowse fix the existing google-flights skill

--browser-trace (default off, remote-only): pairs each iteration with the sibling browser-trace skill — wraps the inner agent in a CDP capture for per-page network/console/page-lifecycle evidence. Implies --env remote; errors if combined with --env local. Requires the sibling browser-trace skill present at ${CLAUDE_SKILL_DIR}/../browser-trace/, and the BROWSERBASE_API_KEY env var.

When the user drops a URL or free-form instruction instead of --task <name>:

  • If an existing task in ${WORKSPACE}/tasks/ clearly matches the site/intent, use it.

  • Otherwise, pick a short kebab-case name, create ${WORKSPACE}/tasks/<name>/task.md from ${CLAUDE_SKILL_DIR}/references/example-task.md, fill in the URL/goal based on what the user said, and proceed. Tell the user the chosen name in one line.


How to run

Step 1 — Parse arguments and orient

Check what was passed:

  • --task <name> → single task mode

  • --tasks a,b,c or --all → multi-task mode (spawn sub-agents)

  • --iterations N → how many evaluate → improve cycles (default: 5)

  • --env local|remote → browser environment (default: local; use remote for bot-protected sites)

  • --browser-trace → opt in to the browser-trace integration (default off). Implies --env remote. If --env local --browser-trace are both passed explicitly, error with: browser-trace requires Browserbase; drop --env local or drop --browser-trace.

If the user passed free-form text instead, map it to one of the above before continuing.

Step 2 — Set up the workspace

All training artifacts (task definitions, strategy iterations, traces, reports) live in a workspace directory in the current working directory — NOT inside ~/.claude/skills/. This keeps the inner agent's file writes out of Claude's home dir and away from permission friction.

Default workspace: ${CWD}/autobrowse/

mkdir -p ./autobrowse/tasks ./autobrowse/traces ./autobrowse/reports

If the task directory (./autobrowse/tasks/<task>/task.md) doesn't exist yet, scaffold it:

mkdir -p ./autobrowse/tasks/<task>
cp ${CLAUDE_SKILL_DIR}/references/example-task.md ./autobrowse/tasks/<task>/task.md
# Then edit task.md to describe the URL, inputs, steps, and expected JSON output

The skill source at ${CLAUDE_SKILL_DIR} stays read-only — only ./autobrowse/ in CWD gets written to during training. Graduation (final step) writes a single file to ~/.claude/skills/<task>/SKILL.md.

List available tasks:

ls ./autobrowse/tasks/
Step 3 — Multi-task: spawn parallel sub-agents

If running multiple tasks, use the Agent tool to spawn one sub-agent per task simultaneously. Each sub-agent receives a self-contained prompt to run the full autobrowse loop for its task:

"You are running the autobrowse skill for task <name>. Workspace: <absolute-path-to-workspace> (e.g. /path/to/project/autobrowse). Run <N> iterations of: evaluate → read trace → improve strategy.md → repeat. Use --env <env>. Pass --workspace <workspace> to every evaluate.mjs invocation. If the parent invocation used --browser-trace, you MUST use the traced-path block of the SKILL.md loop for every iteration (pre-create session, attach bb-capture, pass --connect-url to evaluate.mjs, stop+bisect, release) — do not fall back to the default single-command path. Follow the autobrowse loop instructions exactly.

When graduating, install the skill to ~/.claude/skills/<task-name>/SKILL.md with proper agentskills frontmatter (name + description). Do not just copy strategy.md — write a self-contained skill.

At the end, output a structured summary with: task name, pass/fail on final run, total cumulative cost, iterations completed, per-iteration table (iter number, turns, cost, status, hypothesis tested), and 2-3 bullet key learnings."

Spawn all sub-agents in parallel, wait for all to complete, then collect their summaries and write the session report.

For single task, skip this step and run the loop directly below.


The Loop (run this for each task)

Iteration start

Check that ./autobrowse/tasks/<task>/task.md exists (scaffold it from the template if not — see Step 2). strategy.md is auto-created empty by the harness on first run.

Requirements
  • ANTHROPIC_API_KEY must be in the environment (or in a .env file in CWD — evaluate.mjs auto-loads it). If missing, the harness prints a clear error and exits; don't hunt for keys in other paths.

Run the inner agent

Default path (no --browser-trace) — single command, no orchestration:

node ${CLAUDE_SKILL_DIR}/scripts/evaluate.mjs --task <task-name> --workspace ./autobrowse
# or for bot-protected sites:
node ${CLAUDE_SKILL_DIR}/scripts/evaluate.mjs --task <task-name> --workspace ./autobrowse --env remote

This runs the browser session and writes a full trace to ./autobrowse/traces/<task>/latest/.

Traced path (--browser-trace, remote only) — the outer harness pre-creates a Browserbase session, attaches bb-capture as a passive observer, and passes the session's connectUrl to evaluate.mjs so every inner browse call uses --cdp $connectUrl --session autobrowse-main (the canonical browser-trace pattern that gives observers full Network/Console events). Run this block once per iteration with $N set to the 1-indexed iteration number:

# Preflight — fail fast if browser-trace isn't installed alongside autobrowse.
BT_DIR="${CLAUDE_SKILL_DIR}/../browser-trace"
if [ ! -f "$BT_DIR/scripts/bb-capture.mjs" ]; then
  echo "ERROR: --browser-trace requires the browser-trace skill at $BT_DIR." >&2
  echo "Install it by cloning github.com/browserbase/skills and copying skills/browser-trace/" >&2
  echo "into the same parent directory as autobrowse (e.g. ~/.claude/skills/browser-trace/)." >&2
  exit 1
fi

# a. SESSION SETUP — pre-create the keep-alive session and derive its connectUrl
sid=$(browse cloud sessions create --keep-alive --verified --proxies \
  | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))")
connect_url=$(browse cloud sessions get "$sid" \
  | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).connectUrl))")

RUN_ID="run-$(printf '%03d' "$N")"
TRACE_ROOT="./autobrowse/traces/<task-name>/$RUN_ID"
mkdir -p "$TRACE_ROOT"
export O11Y_ROOT="$TRACE_ROOT/.o11y"   # park browser-trace output inside the autobrowse run dir
export O11Y_RUN_ID="$RUN_ID"           # tells the browse CLI which run dir to write descriptors.ndjson into

# b. ATTACH BROWSER-TRACE — passive observer; runs in background
node ${CLAUDE_SKILL_DIR}/../browser-trace/scripts/bb-capture.mjs "$sid" "$RUN_ID" &
sleep 2

# c. RUN AUTOBROWSE — connectUrl flag tells evaluate.mjs to inject --cdp/--session
#    into every inner browse call. The inner agent never sees --remote.
node ${CLAUDE_SKILL_DIR}/scripts/evaluate.mjs \
  --task <task-name> --workspace ./autobrowse --env remote \
  --connect-url "$connect_url" --run-number "$N"

# d. STOP + BISECT + UNIFY — order matters; bisect needs the session to still
#    exist, and unify-trace joins the bisect output with autobrowse's trace.json
#    into a single time-ordered NDJSON the outer agent reads first each iter.
node ${CLAUDE_SKILL_DIR}/../browser-trace/scripts/stop-capture.mjs "$RUN_ID"
node ${CLAUDE_SKILL_DIR}/../browser-trace/scripts/bisect-cdp.mjs "$RUN_ID"
node ${CLAUDE_SKILL_DIR}/scripts/unify-trace.mjs \
  --trace-dir "$TRACE_ROOT" \
  --o11y-dir "$O11Y_ROOT/$RUN_ID"

# e. RELEASE
browse cloud sessions update "$sid" --status REQUEST_RELEASE

This writes the inner-agent trace to ./autobrowse/traces/<task-name>/latest/ and the CDP bisect to ./autobrowse/traces/<task-name>/latest/.o11y/<run-id>/. The traced browse CLI also emits per-command rich node descriptors to .o11y/<run-id>/cdp/descriptors.ndjson (one JSON object per page-driving call: target tag/id/role/accessibleName/attributes/xpath/bounding-rect). The descriptors file feeds downstream codegen; it is not required for hypothesis formation — skip it when reading the trace.

Read the trace
cat ./autobrowse/traces/<task-name>/latest/summary.md

The summary has duration, cost, turns, the decision log, and the final JSON output.

If the agent failed or got stuck, look deeper:

  • Read ./autobrowse/traces/<task-name>/latest/trace.json — search for the failure turn

  • Read screenshots around the failure point with the Read tool

When --browser-trace was used — start with unified-events.jsonl. The harness joins the agent's turn log and the browser's CDP firehose into one time-ordered NDJSON stream at the run root. One file, source-tagged (source: "agent" | "browser"), interleaved by wall-clock timestamp. Skim it top-to-bottom; the failure cause is usually one or two adjacent lines (the agent issued command X, the browser responded with Y).

cat ./autobrowse/traces/<task-name>/latest/unified-events.jsonl

The structured files (trace.json, .o11y/<run-id>/cdp/*) are also agent-consumable as drill-downs when the unified stream points at something you need more of:

NeedDrill-down file or command
Per-page totals + timing (events, network counts, errors by page).o11y/<run-id>/cdp/summary.json
All failed network requests in one place.o11y/<run-id>/cdp/network/failed.jsonl
Full console exception payloads (stacktraces, etc.).o11y/<run-id>/cdp/console/exceptions.jsonl
Per-page slice (only events on page N).o11y/<run-id>/cdp/pages/<pid>/
Full reasoning text / untruncated tool outputs for a specific turntrace.json (filter by turn === N)
Ad-hoc grouped query (e.g. top hosts, errors-by-page)O11Y_ROOT=./autobrowse/traces/<task-name>/latest/.o11y node ${CLAUDE_SKILL_DIR}/../browser-trace/scripts/query.mjs <run-id> <cmd>

The unified stream is the default; drill into structured files only when you need a grouped query, a full-text payload, or filtering the stream can't give you.

Form one hypothesis

Find the exact turn where things went wrong. What single heuristic would have prevented it?

Under --browser-trace, the hypothesis must cite a specific event from unified-events.jsonl (line number or timestamp) — or name the drill-down file if you had to descend into one. This keeps updates evidence-grounded rather than vibes-driven. A hypothesis based only on the agent's commands might say "the click didn't work"; grounded in the unified stream, it can say "line 47 of unified-events.jsonl: browse open was followed by Network.responseReceived status 403 on /api/checkout — switch to --verified --proxies."

Examples:

  • "After clicking the dropdown, wait 1s — options animate in before they're clickable"

  • "Navigate directly to /pay-invoice/ — skip the landing page entirely"

  • "Use browse fill #field_3 value not browse type — this field clears on focus"

  • "The page shows a spinner at turn 8 — add browse wait timeout 2000 before snapshot"

  • (with --browser-trace) "At line 47 of unified-events.jsonl, 3 consecutive Network.responseReceived events on /api/availability returned 403 right after browse open — the site is fingerprinting; the next iter needs --verified --proxies."

Update strategy.md

Edit ./autobrowse/tasks/<task-name>/strategy.md. Keep everything that worked. Fix the specific failure. Add a concrete heuristic.

Good strategies have:

  • Fast path: direct URL or shortcuts to skip exploration

  • Step-by-step workflow: exact sequence with timing notes

  • Site-specific knowledge: selector IDs, form field names, success indicators

  • Failure recovery: what to do when X goes wrong

Judge the result

Read the new summary. Did it pass? Make clear progress?

  • Pass or progress → keep, next iteration

  • No progress or regression → revert strategy.md to the previous version and try a different hypothesis

Generate a runnable script (optional)

Once the task has converged, you can produce a deterministic, runnable script in one or more frameworks via scripts/codegen.mjs. This is one shot of an LLM call per framework, cached by content hash, with optional verify-against- fresh-session and rewrite-on-failure.

node ${CLAUDE_SKILL_DIR}/scripts/codegen.mjs \
  --task <name> \
  --workspace ./autobrowse \
  --frameworks playwright,stagehand \
  --verify

Each framework gets its own subdirectory under tasks/<name>/<framework>/ with the emitted script and a self-contained scaffold (package.json, tsconfig.json). The directory is runnable standalone with cd tasks/<name>/playwright && npm install && npx tsx <name>.ts — the only runtime requirement is BROWSERBASE_API_KEY (plus ANTHROPIC_API_KEY for the Stagehand target).

Builtin frameworks: playwright, stagehand. Add a custom framework with --prompt-template <path> --frameworks custom (and provide your own runner or pass --no-verify).

Common flags:

FlagPurpose
--frameworks a,b,...Comma-separated; default playwright
--verify / --no-verifyRun the produced script against a fresh BB session; default --verify
--max-retries NRewrite-on-verify-failure cap; default 2
--cache-onlyError if cache miss (CI-friendly)
--forceBust the cache
--dry-runEstimate prompt size + cost; don't call the LLM
--run <id>Force a specific run-NNN (default: latest passing)

Output is one JSON line per framework on stdout. Non-zero exit if any selected framework's final state is passed: false.

See references/playwright-cdp-bridge.md for the canonical connectOverCDP patterns the emitted scripts follow.

After all iterations — publish if ready

If the task passed on 2+ of the last 3 iterations or has reached the max iteration limit, install it as a Claude Code skill. Do not just copy strategy.md — the skill must be self-contained and useful to someone who has never seen this codebase. If graduating at max iterations without a clean pass, note the known failure point but still document everything learned.

Install by writing to ~/.claude/skills/<task-name>/SKILL.md:

mkdir -p ~/.claude/skills/<task-name>

Use this structure for the SKILL.md:

---
name: <task-name>
description: <1-2 sentences describing what this skill does and when to use it. Include trigger keywords.>
---

# <Task Title> — Browser Skill

## Purpose
<1-2 sentences: what this automates and why it exists.>

## When to Use
<When should someone reach for this skill.>

## Browse CLI Reference
The inner agent uses the `browse` CLI. Key commands for this task:
- `browse stop` — kill existing session (always run before switching to remote)
- `browse open <url> --remote` — start a fresh Browserbase cloud session and navigate
- `browse open <url> --local` — start a clean local browser and navigate
- `browse tab new <url>` — open URL in a new tab
- `browse wait load` — wait for page to finish loading
- `browse wait timeout <ms>` — wait a fixed amount of time for spinners or animations
- `browse wait selector "<selector>"` — wait for an element to become visible
- `browse get title` — verify you're on the right page
- `browse get text body` — extract all visible text (preferred for content extraction)
- `browse snapshot` — get accessibility tree; each node has a ref in `[X-Y]` format (e.g. `[0-5]`, `[2-147]`)
- `browse click [X-Y]` — click element by ref from the latest snapshot (include the brackets)

**Never use `--session <name>` flags in SKILL.md.** Named sessions are a parallel-run workaround — they contaminate skills with infrastructure concerns. Skills must work in isolation with the default session.

## Workflow

### Step 1 — Start session
<exact browse commands in order>

### Step 2 — Navigate
<exact URL and verification steps>

### Step 3 — Extract
<exact extraction commands>

### Step 4 — Output
<what JSON to emit, referencing the schema below>

## Site-Specific Gotchas
<Bullet list of every hard-won heuristic from the iterations. This is the core value of the skill.>

## Failure Recovery
<What to do when navigation fails, session is contaminated, or extraction returns garbage>

## Expected Output
```json
<paste the exact expected output schema from task.md>

After writing the SKILL.md, confirm it's installed:
```bash
ls ~/.claude/skills/<task-name>/SKILL.md

The skill is now available as /<task-name> in Claude Code.


Final report (multi-task mode)

After all sub-agents complete, print a markdown table:

TaskIterationsFinal StatusGraduatedCost
google-flights5✅ passyes$0.42
amazon-add-to-cart5❌ failno$1.20

Then write a persistent session report to ./autobrowse/reports/ so there's a durable record of the run inside the workspace:

mkdir -p ./autobrowse/reports

Write the file ./autobrowse/reports/YYYY-MM-DD-HH-MM-<tasks>.md with:

# AutoBrowse Session Report
**Date:** <ISO date>
**Tasks:** <comma-separated list>
**Environment:** remote|local
**Total cost:** $X.XX

## Results

| Task | Iterations | Pass Rate | Final Status | Graduated | Cost |
|------|-----------|-----------|--------------|-----------|------|
| ... | ... | X/5 | ✅/❌ | yes/no | $X.XX |

## Per-Task Learnings

### <task-name>
- **Key insight 1:** <what the agent learned>
- **Key insight 2:** <another heuristic>
- **Failure mode fixed:** <what was failing and how it was resolved>

## Iteration Log

### <task-name>
| Iter | Turns | Cost | Status | Hypothesis tested |
|------|-------|------|--------|-------------------|
| 1 | 79 | $18.75 | ❌ fail | baseline |
| 2 | 9 | $0.26 | ✅ pass | session contamination fix |
| ... | ... | ... | ... | ... |

Rules

  • Only edit strategy.md — never touch task.md (unless creating it from the template) or evaluate.mjs

  • Stay in the workspace — all training writes go to ./autobrowse/, never to ~/.claude/skills/autobrowse/. The skill source is read-only.

  • One hypothesis per iteration — test one change at a time

  • Build on wins — keep what worked, add to it

  • Trust the trace — the inner agent shows exactly what it saw and did

  • Graduate to ~/.claude/skills/ — the only file you write there is the final graduated SKILL.md

  • Don't release before bisecting — under --browser-trace, the order at the end of each iteration is non-negotiable: stop-capture → bisect-cdp → browse cloud sessions update REQUEST_RELEASE. Bisect depends on the session still existing when the trace stops.

Files

scripts/evaluate.mjs

#!/usr/bin/env node

/**
 * evaluate.mjs — Inner agent harness.
 *
 * Runs a browsing agent using the raw Anthropic API with a single `execute`
 * tool. The agent calls browse CLI commands to navigate websites. Full trace
 * is captured incrementally and written to disk.
 *
 * Usage: node scripts/evaluate.mjs --task <task-name> [--workspace <dir>] [--env local|remote] [--model <model>] [--run-number N]
 */

import "dotenv/config";
import Anthropic from "@anthropic-ai/sdk";
import * as fs from "node:fs";
import * as path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import crypto from "node:crypto";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SKILL_DIR = path.resolve(__dirname, "..");

// ── Config ─────────────────────────────────────────────────────────

const DEFAULT_MODEL = "claude-sonnet-4-6";
const MAX_TURNS = Number(process.env.MAX_TURNS) || 30;
const MAX_TOKENS = 4096;
const EXEC_TIMEOUT_MS = 30_000;

// Trace artifacts can contain cookies, auth headers, bearer tokens, passwords,
// private URLs, and screenshots of authenticated pages. Restrict them to the
// owner so other local users or processes on a shared host (CI runners, shared
// dev boxes, multi-tenant containers) can't read them. 0700/0600 are
// unaffected by the process umask.
const TRACE_DIR_MODE = 0o700;
const TRACE_FILE_MODE = 0o600;

// Recursively tighten an already-written trace tree to owner-only. Catches
// files created by subprocesses (screenshots from the `browse` CLI, .o11y
// artifacts) whose creation mode we don't control. Symlinks are skipped.
function lockDownTrace(root) {
  let entries;
  try {
    entries = fs.readdirSync(root, { withFileTypes: true });
  } catch {
    return;
  }
  for (const entry of entries) {
    const p = path.join(root, entry.name);
    if (entry.isSymbolicLink()) continue;
    if (entry.isDirectory()) {
      try { fs.chmodSync(p, TRACE_DIR_MODE); } catch {}
      lockDownTrace(p);
    } else if (entry.isFile()) {
      try { fs.chmodSync(p, TRACE_FILE_MODE); } catch {}
    }
  }
  try { fs.chmodSync(root, TRACE_DIR_MODE); } catch {}
}

// ── Tool definition ────────────────────────────────────────────────

const TOOLS = [
  {
    name: "execute",
    description:
      "Execute a browse CLI command for browser automation.\n\n" +
      "Browse commands:\n" +
      "  browse open <url> --local|--remote — Navigate and choose browser mode\n" +
      "  browse snapshot            — Get accessibility tree; refs look like [0-5] (primary perception)\n" +
      "  browse screenshot --path <path> — Save screenshot to file\n" +
      "  browse click <ref>         — Click element by [X-Y] ref from snapshot\n" +
      "  browse type <text>         — Type into focused element\n" +
      "  browse fill <sel> <value>  — Fill input (clears first — preferred over type)\n" +
      "  browse press <key>         — Keyboard: Enter, Tab, Escape, ArrowRight, ArrowLeft...\n" +
      "  browse mouse scroll <x> <y> <dx> <dy> — Scroll at coords (positive dy scrolls down)\n" +
      "  browse select <sel> <val>  — Select dropdown option\n" +
      "  browse wait load|selector|timeout — Wait for page load, a selector, or a timeout\n" +
      "  browse get url/title/text  — Get page info\n" +
      "  browse mouse drag <x1> <y1> <x2> <y2> — Drag (for sliders)\n" +
      "  browse back/reload/stop    — Navigation/session control\n\n" +
      "Critical: Always `browse snapshot` after every action — refs invalidate on DOM changes.",
    input_schema: {
      type: "object",
      properties: {
        command: {
          type: "string",
          description: "The browse CLI command to execute",
        },
      },
      required: ["command"],
    },
  },
];

// ── CLI args ───────────────────────────────────────────────────────

function getArg(name, fallback) {
  const idx = process.argv.indexOf(`--${name}`);
  if (idx !== -1 && process.argv[idx + 1]) return process.argv[idx + 1];
  return fallback;
}

function showHelp() {
  console.log(`evaluate.mjs — Inner agent harness for autobrowse skill

Usage: node scripts/evaluate.mjs --task <name> [options]

Options:
  --task <name>        Task name — matches tasks/<name>/ directory (required)
  --workspace <dir>    Workspace root holding tasks/ and traces/ (default: ./autobrowse)
  --env local|remote   Browser environment (default: local)
  --model <model>      Claude model for the inner agent (default: ${DEFAULT_MODEL})
  --run-number N       Force a specific run number (default: auto-increment)
  --connect-url <url>  Browserbase wss connectUrl; when set, every inner browse
                       call is rewritten to attach via --cdp <url> --session
                       autobrowse-main, and browse stop is suppressed. Used by
                       the outer harness when --browser-trace is active so a
                       sibling bb-capture observer can see Network/Console
                       events (the --remote attach path routes those only to
                       the driving client).
  --help               Show this help message

Environment variables:
  ANTHROPIC_API_KEY          Required — Claude API key
  BROWSERBASE_API_KEY        Required for --env remote
  BROWSERBASE_PROJECT_ID     Optional Browserbase project override

Output:
  traces/<task>/run-NNN/summary.md     Decision log and final output
  traces/<task>/run-NNN/trace.json     Full tool call log
  traces/<task>/run-NNN/messages.json  Raw API message history
  traces/<task>/run-NNN/screenshots/   Visual captures

Examples:
  node scripts/evaluate.mjs --task google-flights
  node scripts/evaluate.mjs --task my-portal --env remote
  node scripts/evaluate.mjs --task checkout --model claude-opus-4-6`);
  process.exit(0);
}

function resolveWorkspace() {
  const workspace = path.resolve(getArg("workspace", "autobrowse"));
  return workspace;
}

function getTaskName(workspace) {
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
    showHelp();
  }

  const task = getArg("task");
  if (!task) {
    console.error("ERROR: --task <name> is required");
    console.error("Usage: node scripts/evaluate.mjs --task google-flights");
    console.error("\nRun with --help for full usage.");
    console.error(`\nAvailable tasks in ${workspace}:`);
    const tasksDir = path.join(workspace, "tasks");
    if (fs.existsSync(tasksDir)) {
      const dirs = fs.readdirSync(tasksDir, { withFileTypes: true })
        .filter(d => d.isDirectory())
        .map(d => `  - ${d.name}`);
      console.error(dirs.length > 0 ? dirs.join("\n") : "  (none — create tasks/<name>/task.md)");
    } else {
      console.error("  (no tasks/ directory found — create one via the SKILL.md workflow)");
    }
    process.exit(1);
  }
  return task;
}

function ensureApiKey() {
  if (!process.env.ANTHROPIC_API_KEY) {
    console.error("ERROR: ANTHROPIC_API_KEY is not set.");
    console.error("");
    console.error("Set it one of these ways:");
    console.error("  1. export ANTHROPIC_API_KEY=sk-ant-...");
    console.error("  2. Create a .env file in the current directory with:");
    console.error("       ANTHROPIC_API_KEY=sk-ant-...");
    console.error("");
    console.error("Get a key at https://console.anthropic.com/settings/keys");
    process.exit(1);
  }
}

// ── Helpers ─────────────────────────────────────────────────────────

function getNextRunNumber(tracesDir) {
  const n = getArg("run-number");
  if (n) { const num = parseInt(n, 10); if (!isNaN(num)) return num; }
  if (!fs.existsSync(tracesDir)) return 1;
  const dirs = fs.readdirSync(tracesDir).filter((d) => d.startsWith("run-"));
  if (dirs.length === 0) return 1;
  const nums = dirs.map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => !isNaN(n));
  if (nums.length === 0) return 1;
  return Math.max(...nums) + 1;
}

const ALLOWED_COMMAND = "browse";

function parseCommand(command) {
  const args = [];
  let current = "";
  let quote = null;
  let escaping = false;
  let tokenStarted = false;

  for (const char of command.trim()) {
    if (escaping) {
      current += char;
      escaping = false;
      tokenStarted = true;
      continue;
    }

    if (quote === "'") {
      if (char === "'") {
        quote = null;
      } else {
        current += char;
      }
      tokenStarted = true;
      continue;
    }

    if (quote === "\"") {
      if (char === "\"") {
        quote = null;
      } else if (char === "\\") {
        escaping = true;
      } else {
        current += char;
      }
      tokenStarted = true;
      continue;
    }

    if (char === "'" || char === "\"") {
      quote = char;
      tokenStarted = true;
      continue;
    }

    if (char === "\\") {
      escaping = true;
      tokenStarted = true;
      continue;
    }

    if (/\s/.test(char)) {
      if (tokenStarted) {
        args.push(current);
        current = "";
        tokenStarted = false;
      }
      continue;
    }

    current += char;
    tokenStarted = true;
  }

  if (escaping) {
    return { error: "BLOCKED: command ends with an unfinished escape sequence." };
  }

  if (quote) {
    return { error: "BLOCKED: command has an unclosed quote." };
  }

  if (tokenStarted) {
    args.push(current);
  }

  if (args.length === 0) {
    return { error: "BLOCKED: empty command." };
  }

  return { args };
}

// browse subcommands that drive a page through a local daemon — these need
// the --cdp/--session injection when --connect-url is set. Excludes commands
// that don't touch the daemon (cloud/cdp/status) or that would tear it down
// (stop).
const PAGE_DRIVING_VERBS = new Set([
  "open", "snapshot", "screenshot", "click", "type", "fill", "press", "select",
  "wait", "get", "reload", "back", "forward", "mouse", "tab",
]);

// Per-process local daemon name. Derived from a hash of the connectUrl so two
// evaluate.mjs invocations driving different Browserbase sessions (e.g. SKILL.md
// Step-3 parallel sub-agents) use distinct local daemons and don't collide on
// the "autobrowse-main" socket. crypto.createHash is sync, fast, no deps.
function deriveBrowseSessionName(connectUrl) {
  const hash = crypto.createHash("sha1").update(connectUrl).digest("hex").slice(0, 8);
  return `autobrowse-${hash}`;
}

function rewriteArgsForTrace(args, connectUrl) {
  // No-op when not in trace mode or when the first arg isn't a page-driving
  // verb (cloud, cdp, etc. pass through unchanged).
  if (!connectUrl || args.length === 0) return args;
  const verb = args[0];
  if (!PAGE_DRIVING_VERBS.has(verb)) return args;

  const out = args.slice();
  const hasFlag = (name) => out.some((a, i) => a === name && i > 0);
  // Remove --remote / --local — they'd conflict with --cdp.
  for (let i = out.length - 1; i > 0; i--) {
    if (out[i] === "--remote" || out[i] === "--local") out.splice(i, 1);
  }
  if (!hasFlag("--cdp")) out.push("--cdp", connectUrl);
  if (!hasFlag("--session") && !hasFlag("-s")) out.push("--session", deriveBrowseSessionName(connectUrl));
  return out;
}

function executeCommand(command, connectUrl) {
  // Security: only allow the browse CLI and execute it without a shell so
  // metacharacters are treated as literal arguments instead of extra commands.
  const parsed = parseCommand(command);
  if ("error" in parsed) {
    return { output: parsed.error, error: true, duration_ms: 0 };
  }

  const [executable, ...args] = parsed.args;
  if (executable !== ALLOWED_COMMAND) {
    return { output: `BLOCKED: only browse commands are allowed. Got: ${command.slice(0, 50)}`, error: true, duration_ms: 0 };
  }

  // Suppress `browse stop` under --browser-trace: the outer harness owns the
  // session and daemon; tearing the named daemon down would orphan the trace
  // observer and block the rest of the iteration.
  if (connectUrl && args[0] === "stop") {
    return { output: '{"stopped":true,"suppressed":"browse-trace mode owns the session"}', error: false, duration_ms: 0 };
  }

  const finalArgs = rewriteArgsForTrace(args, connectUrl);

  const start = Date.now();
  try {
    const output = execFileSync(executable, finalArgs, {
      encoding: "utf-8",
      timeout: EXEC_TIMEOUT_MS,
      stdio: ["pipe", "pipe", "pipe"],
      maxBuffer: 1024 * 1024,
    });
    return { output: output.trim(), error: false, duration_ms: Date.now() - start };
  } catch (err) {
    const stderr = typeof err.stderr === "string" ? err.stderr : err.stderr?.toString("utf-8");
    const stdout = typeof err.stdout === "string" ? err.stdout : err.stdout?.toString("utf-8");
    const output = stderr || stdout || err.message || String(err);
    return { output: output.trim(), error: true, duration_ms: Date.now() - start };
  }
}

function buildSystemPrompt(strategy, traceDir, browseEnv, connectUrl) {
  // Under --browser-trace (connectUrl set), the outer harness owns the
  // Browserbase session and a sibling bb-capture observer is attached. Every
  // browse call is rewritten by executeCommand to attach via
  // --cdp $connectUrl --session autobrowse-main, and `browse stop` is a no-op.
  // The inner agent must NOT pass --remote/--local or run browse stop.
  const openFlag = connectUrl ? "" : (browseEnv === "remote" ? "--remote" : "--local");
  const envDesc = connectUrl
    ? `**Traced session managed by the outer harness.** Do not run \`browse stop\`. Do not pass \`--remote\` or \`--local\` — the harness routes every browse call through the trace-attached connection automatically. Just write \`browse open <url>\`, \`browse snapshot\`, etc.`
    : (browseEnv === "remote"
    ? `Use **remote mode** (Browserbase) — Browserbase Identity, Verified browsers, CAPTCHA solving, residential proxies:
\`\`\`
browse stop
browse open <url> --remote
\`\`\`
Run \`browse stop\` first when a prior daemon may be active; active sessions do not switch between local and remote automatically.`
    : `Use **local mode** — runs on local Chrome:
\`\`\`
browse open <url> --local
\`\`\``);

  return `You are a browser automation agent. You navigate websites using the browse CLI via the execute tool.

# Browser Automation via Browse CLI

All browser interaction happens through the \`browse\` command, run via the execute tool.

## Environment

${envDesc}

## Commands

### Navigation
- \`browse open <url> ${openFlag}\` — Go to URL
- \`browse reload\` — Reload page
- \`browse back\` / \`browse forward\` — History navigation

### Page State (prefer snapshot over screenshot)
- \`browse snapshot\` — Get accessibility tree. Each element has a ref in \`[X-Y]\` format (e.g. \`[0-5]\`, \`[2-147]\`). This is your PRIMARY perception tool.
- \`browse screenshot --path ${traceDir}/screenshots/step-NN.png\` — Save visual screenshot (for debugging only)
- \`browse get url\` / \`browse get title\` — Page info
- \`browse get text <selector>\` — Get text content ("body" for all)
- \`browse get value <selector>\` — Get form field value

### Interaction
- \`browse click [X-Y]\` — Click element by ref from the latest snapshot. Pass the ref EXACTLY as it appears in the tree, including brackets (e.g. \`browse click [2-147]\`).
- \`browse type <text>\` — Type text into focused element
- \`browse fill <selector> <value>\` — Fill input without pressing Enter (clears existing text — PREFERRED over type)
- \`browse fill <selector> <value> --press-enter\` — Fill input and press Enter
- \`browse select <selector> <values...>\` — Select dropdown option(s)
- \`browse press <key>\` — Press key: Enter, Tab, Escape, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, Cmd+A
- \`browse mouse drag <fromX> <fromY> <toX> <toY>\` — Drag (useful for sliders)
- \`browse mouse scroll <x> <y> <deltaX> <deltaY>\` — Scroll at coords (positive dy scrolls down)
- \`browse wait load\` — Wait for page to finish loading
- \`browse wait timeout <ms>\` — Wait a fixed amount of time for spinners or animations
- \`browse wait selector "<selector>"\` — Wait for an element to become visible (or use \`--state\`)

### Session
- \`browse stop\` — Close browser
- \`browse status\` — Check daemon status
- \`browse tab list\` — List open tabs
- \`browse tab switch <index-or-target-id>\` — Switch tabs

## Workflow Pattern
1. \`browse stop\` — clean up any previous run
2. \`browse open <url> ${openFlag}\` — navigate to page in ${browseEnv} mode
3. \`browse snapshot\` — read accessibility tree; refs appear as \`[X-Y]\`
4. \`browse click [X-Y]\` / \`browse fill <sel> <val>\` / \`browse press <key>\` — interact using refs
5. \`browse snapshot\` — confirm action worked (refs invalidate after DOM changes!)
6. Repeat 4-5 until done
7. \`browse stop\` — clean up

## Critical Rules
1. **Start clean when needed** — if a daemon may already be active, run \`browse stop\` before \`browse open <url> ${openFlag}\`
2. **ALWAYS snapshot after every action** — refs like \`[0-5]\` invalidate when the DOM changes
3. **Use fill, not type, for input fields** — fill clears existing text first
4. **Use refs from the LATEST snapshot only** — old refs are stale
5. **Never invent refs.** If you haven't seen \`[X-Y]\` in the snapshot output, it doesn't exist. Snapshot first, then click.
6. **Save screenshots at key decision points** — \`browse screenshot --path ${traceDir}/screenshots/step-NN.png\`
7. **When an action fails**, run \`browse snapshot\` to see current state and try a different approach
8. **When done, output your final answer as a JSON code block**

## Troubleshooting
- **Action fails / element not found**: Run \`browse snapshot\` to see available elements
- **Page seems empty**: Try \`browse wait timeout 1000\` then \`browse snapshot\`; if you know the target element, use \`browse wait selector "<selector>"\`
- **Dropdown didn't open**: Wait briefly, then snapshot to check
- **Slider won't move with click**: Use \`browse press ArrowRight\` / \`browse press ArrowLeft\` after clicking the slider thumb

# Current Navigation Strategy

The following strategy has been learned from previous iterations. Follow these guidelines:

${strategy}

# Important
- Your goal is to complete the task and return the result as a JSON code block.
- Save screenshots to: ${traceDir}/screenshots/
- If you get stuck on an approach, try something different rather than repeating the same failing action.
`;
}

// ── Main agent loop ────────────────────────────────────────────────

async function main() {
  const workspace = resolveWorkspace();
  const taskName = getTaskName(workspace);
  ensureApiKey();

  const model = getArg("model", DEFAULT_MODEL);
  const taskDir = path.join(workspace, "tasks", taskName);
  const tracesDir = path.join(workspace, "traces", taskName);

  const taskFile = path.join(taskDir, "task.md");
  const strategyFile = path.join(taskDir, "strategy.md");

  if (!fs.existsSync(taskFile)) {
    console.error(`ERROR: ${path.relative(process.cwd(), taskFile)} not found.`);
    console.error(`Create it from the template: ${path.join(SKILL_DIR, "references/example-task.md")}`);
    process.exit(1);
  }
  if (!fs.existsSync(strategyFile)) {
    fs.mkdirSync(taskDir, { recursive: true });
    fs.writeFileSync(strategyFile, `# ${taskName} Navigation Skill\n\n(This will grow as the agent learns through iterations)\n`);
    console.error(`Created empty strategy.md for task "${taskName}"`);
  }

  const browseEnv = getArg("env", "local");
  const connectUrl = getArg("connect-url");
  if (connectUrl && browseEnv !== "remote") {
    console.error("ERROR: --connect-url requires --env remote");
    process.exit(1);
  }
  const client = new Anthropic();
  const runNumber = getNextRunNumber(tracesDir);
  const runId = `run-${String(runNumber).padStart(3, "0")}`;
  const traceDir = path.join(tracesDir, runId);

  fs.mkdirSync(path.join(traceDir, "screenshots"), { recursive: true, mode: TRACE_DIR_MODE });
  // mkdirSync's mode only applies to dirs it creates; ensure the run dir is
  // locked down even if a parent already existed with looser perms.
  fs.chmodSync(traceDir, TRACE_DIR_MODE);

  const strategy = fs.readFileSync(strategyFile, "utf-8");
  const task = fs.readFileSync(taskFile, "utf-8");
  const systemPrompt = buildSystemPrompt(strategy, traceDir, browseEnv, connectUrl);

  console.error(`\n${"=".repeat(60)}`);
  console.error(`  AUTOBROWSE — ${taskName} — Run ${runNumber}`);
  console.error(`${"=".repeat(60)}`);
  console.error(`Model: ${model} | Env: ${browseEnv}${connectUrl ? " (traced)" : ""} | Max turns: ${MAX_TURNS} | Trace: ${traceDir}\n`);

  const trace = [];
  const messages = [
    { role: "user", content: task },
  ];

  let totalInputTokens = 0;
  let totalOutputTokens = 0;
  let turn = 0;
  let lastAssistantText = "";
  let runStatus = "max_turns";
  let finalStopReason = null;
  const startTime = Date.now();

  while (turn < MAX_TURNS) {
    turn++;

    const response = await client.messages.create({
      model,
      max_tokens: MAX_TOKENS,
      system: systemPrompt,
      tools: TOOLS,
      messages,
    });

    totalInputTokens += response.usage.input_tokens;
    totalOutputTokens += response.usage.output_tokens;

    const toolUseBlocks = [];
    let assistantText = "";

    for (const block of response.content) {
      if (block.type === "text") {
        assistantText += block.text;
      }
      if (block.type === "tool_use") {
        toolUseBlocks.push(block);
      }
    }

    if (assistantText) {
      lastAssistantText = assistantText;
      const short = assistantText.slice(0, 200).replace(/\n/g, " ");
      console.error(`  [${turn}] reasoning: ${short}${assistantText.length > 200 ? "..." : ""}`);
      trace.push({
        turn,
        timestamp: new Date().toISOString(),
        role: "assistant",
        reasoning: assistantText,
        input_tokens: response.usage.input_tokens,
        output_tokens: response.usage.output_tokens,
      });
    }

    if (response.stop_reason === "end_turn") {
      console.error(`  [${turn}] done (${response.stop_reason})`);
      messages.push({ role: "assistant", content: response.content });
      runStatus = "completed";
      finalStopReason = response.stop_reason;
      break;
    }

    if (toolUseBlocks.length === 0) {
      finalStopReason = response.stop_reason ?? "unknown";
      runStatus = response.stop_reason === "max_tokens" ? "truncated" : "incomplete";
      console.error(`  [${turn}] incomplete (${finalStopReason})`);
      messages.push({ role: "assistant", content: response.content });
      break;
    }

    const toolResults = [];

    for (const toolUse of toolUseBlocks) {
      const command = toolUse.input.command;
      const isSnapshot = command.includes("browse snapshot");
      const isScreenshot = command.includes("browse screenshot");

      console.error(`  [${turn}] exec: ${command.slice(0, 120)}`);

      const { output, error, duration_ms } = executeCommand(command, connectUrl);

      if (error) {
        console.error(`  [${turn}] error: ${output.slice(0, 100)}`);
      } else if (isSnapshot) {
        const refCount = (output.match(/\[\d+-\d+\]/g) || []).length;
        console.error(`  [${turn}] snapshot: ${refCount} refs (${duration_ms}ms)`);
      } else if (isScreenshot) {
        console.error(`  [${turn}] screenshot saved (${duration_ms}ms)`);
      } else {
        console.error(`  [${turn}] ok: ${output.slice(0, 100)} (${duration_ms}ms)`);
      }

      trace.push({
        turn,
        timestamp: new Date().toISOString(),
        role: "assistant",
        tool_name: "execute",
        tool_input: { command },
      });
      trace.push({
        turn,
        timestamp: new Date().toISOString(),
        role: "tool_result",
        command,
        output,
        error,
        duration_ms,
      });

      toolResults.push({
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: output.slice(0, 50_000),
        is_error: error,
      });
    }

    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });

    // Write trace incrementally
    fs.writeFileSync(path.join(traceDir, "trace.json"), JSON.stringify(trace, null, 2), { mode: TRACE_FILE_MODE });
  }

  // ── Write final artifacts ──────────────────────────────────────
  const durationSec = (Date.now() - startTime) / 1000;
  const pricing = {
    "claude-opus-4-6": [5, 25],
    "claude-sonnet-4-6": [3, 15],
    "claude-haiku-4-5-20251001": [1, 5],
  };
  const [inputRate, outputRate] = pricing[model] ?? [3, 15];
  const costUsd = (totalInputTokens * inputRate + totalOutputTokens * outputRate) / 1_000_000;

  const summaryLines = [
    `# ${taskName} — Run ${runId} Summary`,
    "",
    `**Status:** ${runStatus}${finalStopReason ? ` (${finalStopReason})` : ""}`,
    `**Duration:** ${durationSec.toFixed(1)}s | **Turns:** ${turn} | **Cost:** ~$${costUsd.toFixed(2)}`,
    `**Tokens:** ${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out`,
    "",
    "## Decision Log",
    "",
  ];

  for (const entry of trace) {
    if (entry.role === "assistant" && entry.reasoning) {
      const short = entry.reasoning.slice(0, 150).replace(/\n/g, " ");
      summaryLines.push(`Turn ${entry.turn}: [reasoning] "${short}${entry.reasoning.length > 150 ? "..." : ""}"`);
    }
    if (entry.role === "assistant" && entry.tool_name) {
      summaryLines.push(`Turn ${entry.turn}: [execute] \`${entry.tool_input?.command}\``);
    }
    if (entry.role === "tool_result") {
      const isSnapshot = entry.command?.includes("snapshot");
      const isError = entry.error;
      if (isError) {
        summaryLines.push(`Turn ${entry.turn}: [error] ${entry.output?.slice(0, 100)}`);
      } else if (isSnapshot) {
        const refs = (entry.output?.match(/\[\d+-\d+\]/g) || []).length;
        summaryLines.push(`Turn ${entry.turn}: [snapshot] ${refs} refs (${entry.duration_ms}ms)`);
      } else {
        summaryLines.push(`Turn ${entry.turn}: [result] ${entry.output?.slice(0, 100)} (${entry.duration_ms}ms)`);
      }
    }
  }

  if (lastAssistantText) {
    summaryLines.push("", "## Agent Final Output", "", lastAssistantText);
  }

  const summary = summaryLines.join("\n");

  fs.writeFileSync(path.join(traceDir, "summary.md"), summary, { mode: TRACE_FILE_MODE });
  fs.writeFileSync(path.join(traceDir, "trace.json"), JSON.stringify(trace, null, 2), { mode: TRACE_FILE_MODE });
  fs.writeFileSync(path.join(traceDir, "messages.json"), JSON.stringify(messages, null, 2), { mode: TRACE_FILE_MODE });

  // Lock down everything under the run dir, including artifacts written by
  // subprocesses (screenshots, .o11y) whose creation mode we don't control.
  lockDownTrace(traceDir);

  // Update latest symlink
  const latestLink = path.join(tracesDir, "latest");
  try {
    try {
      fs.unlinkSync(latestLink);
    } catch (err) {
      if (err.code !== "ENOENT") throw err;
    }
    fs.symlinkSync(runId, latestLink);
  } catch (err) {
    console.warn(`Warning: failed to update latest symlink: ${err.message}`);
  }

  // Structured summary to stdout (data), diagnostics already went to stderr
  const result = {
    task: taskName,
    run: runId,
    status: runStatus,
    stop_reason: finalStopReason ?? (runStatus === "max_turns" ? "max_turns" : null),
    duration_sec: parseFloat(durationSec.toFixed(1)),
    cost_usd: parseFloat(costUsd.toFixed(2)),
    turns: turn,
    tokens_in: totalInputTokens,
    tokens_out: totalOutputTokens,
    trace_dir: traceDir,
  };
  console.log(JSON.stringify(result));
}

main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});

Requirements and compatibility

Written for: Claude Code

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Names Claude Code tools, Needs an API key, Needs Node.js

The author says: Requires Node.js 18+, browse CLI, and ANTHROPIC_API_KEY. Run from the autobrowse app directory.

Security

What our scanner noticed· Low risk

  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
browserbase
Publisher
Browserbase (Official)
Source
browserbase/skills · skills/autobrowse · commit 6811ca3
Licence
MIT License (redistribution allowed)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by browserbase or Browserbase.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download autobrowse.zip