Skip to content

[STG-2450] feat(cli): browse snapshot lean by default, add --full for ref maps - #2296

Merged
shrey150 merged 6 commits into
mainfrom
shrey/browse-lean-snapshot-default
Jul 7, 2026
Merged

[STG-2450] feat(cli): browse snapshot lean by default, add --full for ref maps#2296
shrey150 merged 6 commits into
mainfrom
shrey/browse-lean-snapshot-default

Conversation

@shrey150

@shrey150 shrey150 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

browse snapshot now prints the formatted accessibility tree only by default, omitting the xpathMap/urlMap ref maps that were previously included on every call.

  • browse snapshot (default): formatted tree only (~14KB on a content-heavy page).
  • browse snapshot --full: tree + xpathMap + urlMap.
  • browse snapshot --compact: deprecated no-op alias of the default (prints a stderr-only, TTY-gated deprecation notice).
  • browse refs: prints the cached maps on demand.

Ref-based element commands (click, fill, select, …) are unaffected — the maps are still captured and cached server-side, so refs resolve exactly as before.

Why

The default snapshot emitted ~241KB (~60K tokens) on a content-heavy page, of which ~217KB (90%) is the xpathMap (a ref→XPath entry for every node) + urlMap. The tree an agent actually reasons over is only ~13.5KB.

Because refs resolve from a server-side cache (not from stdout), the printed maps were dead weight in the context of any agent consuming browse snapshot output. Making the tree lean by default cuts the default payload ~17× with no loss to element interaction; --full and browse refs recover the maps when needed.

E2E Test Matrix

Behavior + size (local build, live session on capeair.com)

Verified against packages/cli/bin/run.js (the local build), not the published browse.

Command / flow Observed output Confidence / sufficiency
browse snapshot (default) 13,845 bytes, keys = tree only, hasMaps=false Lean default; ~17× smaller than before
browse snapshot --full 241,104 bytes, keys = tree,urlMap,xpathMap (1,640 entries) --full returns the full tree + maps
browse snapshot --compact (piped) 13,868 bytes, hasMaps=false, 0 bytes stderr Deprecated alias == default; no noise on non-TTY
browse refs count=1640 Maps still captured + retrievable on demand
browse click @0-1588 after a lean snapshot {"clicked": true} → navigated to /about_us/ Ref interaction unaffected by dropping maps from stdout
default tree vs --full tree identical (341 lines both) Lean mode omits only the maps; it does not prune tree content
pnpm --dir packages/cli build (tsc) + evals tsc --noEmit success / clean Typechecks
driver-commands unit test 15/15 pass Locks in: default omits maps, --full includes them, maps cached in both modes

Agent task-success A/B (lean vs full)

Controlled A/B — same task / model (Sonnet) / session per pair; the only variable is snapshot mode (--full = maps vs default = lean), against the local build.

Task Arm Outcome Snapshots Agent tokens
capeair (regional booking) full (maps) ✅ SUCCESS 9 109.8K
capeair lean ✅ SUCCESS 8 93.5K
Google Flights full (maps) ✅ SUCCESS 15 85.3K
Google Flights lean ✅ SUCCESS 15 86.4K

4/4 success — lean-by-default held task success with zero capability loss. (A standard-benchmark A/B via the evals package is in progress and will be added here.)

What's in the diff

  • packages/cli/src/commands/snapshot.ts — lean default (compact decoupled from map omission); add --full; deprecate --compact.
  • packages/cli/src/lib/driver/commands/snapshot.ts — driver handler emits the full tree and includes ref maps only when full is requested.
  • packages/cli/skills/browse/SKILL.md — document lean default + --full.
  • packages/cli/tests/driver-commands.test.ts — test the default/--full/caching behavior.
  • packages/evals/core/tools/browse_cli.ts — derive refCount from the tree when maps are absent.
  • .changeset/lean-browse-snapshot.mdbrowse patch.

Linear: STG-2450

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8754565

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/cli/src/commands/snapshot.ts Outdated
Comment thread packages/cli/src/commands/snapshot.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Confidence score: 3/5

  • In packages/cli/src/commands/snapshot.ts, the new default path still applies the old compact tree filter, so lean mode may remove accessibility-tree lines (not just ref maps), which can change snapshot content unexpectedly for users — split “omit maps” logic from “compact tree” before merging the default switch.
  • In packages/cli/src/commands/snapshot.ts, the default flip plus new --full flag and deprecated flag behavior are untested, so regressions in typical CLI usage could ship unnoticed — add unit tests for default lean output, --full, and deprecated-flag compatibility before merging.
Architecture diagram
sequenceDiagram
    participant Agent as Agent (LLM/Client)
    participant CLI as CLI (browse commands)
    participant Session as Session Manager
    participant Daemon as Driver Daemon
    participant Cache as Refs Cache (in-memory)

    Note over Agent,Cache: NEW: Lean snapshot flow

    Agent->>CLI: browse snapshot
    CLI->>CLI: Parse flags (--full default=false)
    alt --full flag absent (default)
        CLI->>CLI: Set compact=true (lean output)
        CLI->>Daemon: snapshot(compact=true)
        Daemon->>Daemon: Capture accessibility tree only
        Daemon-->>CLI: { tree: formattedTree }
        CLI-->>Agent: JSON with tree only (hasMaps=false)
    else --full flag present
        CLI->>CLI: Set compact=false
        CLI->>Daemon: snapshot(compact=false)
        Daemon->>Daemon: Capture tree + xpathMap + urlMap
        Daemon-->>CLI: { tree, xpathMap, urlMap }
        CLI-->>Agent: JSON with tree + ref maps
    end

    Note over CLI,Daemon: --compact deprecation path (TTY only)

    opt --compact flag AND stderr is TTY
        CLI->>CLI: Print deprecation warning to stderr
    end

    Note over Agent,Cache: CHANGED: Refs still cached server-side

    Agent->>CLI: browse click @0-1588
    CLI->>Daemon: click(ref="0-1588")
    Daemon->>Cache: Resolve ref to XPath
    Cache-->>Daemon: XPath
    Daemon->>Daemon: Execute click on element
    Daemon-->>CLI: { clicked: true }
    CLI-->>Agent: Result

    Note over Agent,Cache: CHANGED: browse refs still works

    Agent->>CLI: browse refs
    CLI->>Daemon: getRefs()
    Daemon->>Cache: Retrieve cached maps
    Cache-->>Daemon: { xpathMap, urlMap }
    Daemon-->>CLI: Maps
    CLI-->>Agent: Formatted ref maps

    Note over Agent,Cache: Eval represent() with BROWSE_SNAPSHOT_FULL toggle

    alt BROWSE_SNAPSHOT_FULL=1
        Agent->>CLI: represent() → snapshot --full
        CLI-->>Agent: { tree, xpathMap, urlMap }
        Agent->>Agent: refCount = Object.keys(xpathMap).length
    else default (lean)
        Agent->>CLI: represent() → snapshot
        CLI-->>Agent: { tree }
        Agent->>Agent: refCount = count refs in tree text
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/cli/src/commands/snapshot.ts Outdated
Comment thread packages/cli/src/commands/snapshot.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/tests/driver-commands.test.ts">

<violation number="1" location="packages/cli/tests/driver-commands.test.ts:363">
P3: `toHaveBeenCalledWith` doesn't verify caching happens on the lean call specifically. If the handler is refactored to only cache maps in the `--full` path the test would still pass.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/cli/tests/driver-commands.test.ts Outdated
shrey150 and others added 3 commits June 30, 2026 20:14
… maps

browse snapshot previously emitted the formatted tree plus xpathMap+urlMap
(~217KB / ~60K tokens on content-heavy pages) on every call. Refs resolve from
a server-side cache, so the printed maps were dead weight in agent context and
~18x larger than Playwright MCP / agent-browser / our own managed Agents output.

- Default output is now the formatted tree only (no ref maps).
- Add --full to restore tree + xpathMap + urlMap (the previous default).
- --compact is now a deprecated no-op alias of the default (stderr + TTY-gated notice).
- browse refs still prints the cached maps on demand; ref-based commands unaffected.
- evals(browse_cli): derive refCount from the tree when maps are absent; add
  BROWSE_SNAPSHOT_FULL toggle for the core-tier represent() path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses PR review:
- Default snapshot now emits the full tree (no line pruning) and only omits
  the ref maps; --full adds xpathMap/urlMap. Verified default tree === --full tree.
- Driver snapshot handler takes `full` instead of overloading `compact`.
- Simplify the command description and drop verbose/historical comments.
- Add a driver test: default omits maps, --full includes them, maps cached either way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verify setRefMaps runs on the default (lean) call specifically via call-count
+ last-called-with, so caching moving into the --full path would fail the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shrey150
shrey150 force-pushed the shrey/browse-lean-snapshot-default branch from ab29d17 to c05d2a4 Compare July 1, 2026 03:14
Comment thread packages/cli/src/commands/snapshot.ts Outdated
shrey150 and others added 2 commits July 6, 2026 19:43
The `browse refs` command was removed on main; the snapshot command
description still pointed users to it. Remove the stale clause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop implementation details (byte/token counts, server-side caching,
`--compact` no-op mechanics) per changeset-brevity convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@seanmcguire12

Copy link
Copy Markdown
Member

@shrey150 dont forget to update the readme. it still says --compact

`browse snapshot` is lean by default now; replace `--compact` (deprecated
no-op) with `--full` for including the ref maps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shrey150

shrey150 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 8754565: README quick-start + snapshot examples now use browse snapshot (lean by default) and --full instead of the deprecated --compact. --compact is gone from all tracked docs (SKILL.md was already correct).

@shrey150
shrey150 merged commit 2f5e085 into main Jul 7, 2026
61 of 62 checks passed
shrey150 added a commit that referenced this pull request Jul 14, 2026
…ILL.md (#2334)

## Why

The Claude Code eval harness installs a browser skill for the agent it
spawns from `packages/evals/skills/browser/SKILL.md` — a 68-line
eval-local copy created once in "Evals v2" (#2011) and never touched
again. The real, maintained browse skill lives at
`packages/cli/skills/browse/SKILL.md` (355 lines, ships with the CLI).
Checking git history:

- `packages/evals/skills/browser/SKILL.md`: **1 commit ever**
(2026-05-01, #2011), never edited since.
- `packages/cli/skills/browse/SKILL.md`: **10 commits since
2026-06-05**, most recently 2 days ago — roughly one edit every 3 days.

That gap already produced a concrete factual error: the most recent
CLI-skill commit (#2296, STG-2450) made `browse snapshot`
lean-by-default with `--full` needed for ref maps, but the frozen eval
skill still documented the old always-full behavior. It also never
mentioned `--verified`/`--proxies`/`--auto-connect`, `browse doctor`,
retry discipline, tab/network/cdp/mouse/viewport commands, or Browse.sh
skill discovery. The harness was testing an outdated mental model of
`browse`.

## Design

Single source of truth with an install-time eval addendum:

1. `BROWSER_SKILL_SOURCE` → `BROWSE_SKILL_SOURCE`, now pointing at
`packages/cli/skills/browse/SKILL.md` (built from `getRepoRootDir()`,
matching the existing `BROWSE_CLI_ENTRYPOINT`-style constants).
2. `installBrowserSkill` → `installBrowseSkill` now reads the CLI skill
and inserts a code-level `EVAL_HARNESS_ADDENDUM` template literal
**immediately after the YAML frontmatter** (not appended at the end)
before writing the combined file. The addendum:
- States `browse` is preinstalled/pinned by the harness (no `npm
install`, no `--local`/`--remote`/`--session` — the wrapper injects
those).
- Requires exactly one `browse ...` command per Bash call (shell
operators rejected by the harness).
- Tells the model to ignore the CLI skill's
install/Browse.sh-discovery/cloud/Functions/templates sections — out of
scope during evals.
- Reiterates no repo edits, no non-`browse` network tools, and the
`EVAL_RESULT` reporting format.

Why prepend and not append: `isAllowedBrowseCommand` only checks that a
Bash command starts with `browse ` and has no shell metacharacters — it
does **not** restrict which `browse` subcommand runs. So the addendum's
"ignore cloud/functions/skills" instruction is the actual
scope-enforcement mechanism, not just a courtesy note, and it needs to
be read before the model encounters the CLI skill's concrete (and
tempting) examples of those commands, not after. A live smoke run (see
below) shows the model reaching for `browse cloud fetch` and `browse
skills find` once it got stuck on a bot-protected page — evidence this
ordering concern is real, not theoretical.

3. Skill name consistency: installed skill dir renamed
`.claude/skills/browser/` → `.claude/skills/browse/` to match the CLI
skill's own `name: browse` frontmatter; all harness prompt/log
references to "a project skill named browser" updated to "browse". The
`stagehand_browser` MCP server name (used by the unrelated
`playwright_code`/`cdp_code` tool surfaces) is untouched.
4. Deleted `packages/evals/skills/browser/SKILL.md` and the now-empty
`packages/evals/skills/` directory.
5. Updated
`packages/evals/tests/framework/claudeCodeToolAdapter.test.ts` for the
renamed export/path/skill-name, plus new assertions that the installed
file contains both the CLI-skill content and the addendum, with the
addendum's string index before `## Cloud APIs`'s index (regression guard
against the addendum silently drifting back to append-at-end).

**Follow-up (review comment from
[ajmcquilkin](#2334 (comment)
the hand-rolled regex in `insertAfterFrontmatter` had already needed a
CRLF patch and still failed silently on BOM-prefixed files or a `---`
line embedded in a YAML multiline string. Swapped it for
[`gray-matter`](https://www.npmjs.com/package/gray-matter) (new
`packages/evals` devDependency, private package, no changeset), but only
for *boundary detection* — `matter(markdown)` locates where the
frontmatter block ends; reassembly still uses the original raw string
(`markdown.slice(0, markdown.length - parsed.content.length)` for the
frontmatter, `parsed.content` for the body) rather than
`matter.stringify()`, since that would re-serialize the YAML through
js-yaml and reformat the shipped skill's frontmatter (e.g. its folded
`description: >` block). `insertAfterFrontmatter` is now exported and
directly unit-tested.

No changeset — `packages/evals` is private, eval-infra only.

**Overlap note:** this touches `claudeCodeToolAdapter.ts`; open PR #2299
also touches that file but in a different region (contract fix, not the
skill-install path). Trivial rebase for whichever lands second.

Linear:
[STG-2510](https://linear.app/browserbase/issue/STG-2510/evals-source-browse-skill-from-packagescli-skillmd-instead-of-stale)

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm turbo run build --filter=@browserbasehq/stagehand
--filter=browse` then `pnpm --dir packages/evals build` | All 4 turbo
tasks + evals `build:esm`/`build:cli` completed successfully in
`<worktree>` | Confirms the changed adapter compiles against the real
CLI/core build artifacts it now depends on
(`packages/cli/skills/browse/SKILL.md`, `packages/cli/dist/...`). |
| `pnpm --dir packages/evals exec vitest run
tests/framework/claudeCodeToolAdapter.test.ts` | `Test Files 1 passed
(1)`, `Tests 17 passed (17)` | Covers the renamed export, new install
path/skill name, the addendum-ordering assertion, and (as of the
gray-matter follow-up below) the frontmatter boundary-detection cases.
Narrow to this file. |
| `pnpm --dir packages/evals run test:unit` (full evals suite) | `Test
Files 48 passed (48)`, `Tests 362 passed (362)` | Confirms no other test
in the package depends on the old `browser` skill name/path,
`installBrowserSkill` export, or the removed regex helper. |
| `node -e` script importing `installBrowseSkill` from built
`packages/evals/dist/esm/framework/claudeCodeToolAdapter.js`, run
against a temp dir | Installed at `<temp
dir>/.claude/skills/browse/SKILL.md`; head matches CLI skill frontmatter
(`name: browse`, full description); tail/body contains `## Eval Harness
Addendum` positioned before `## Cloud APIs` (indices 1054 vs 9018);
frontmatter YAML block intact as the first bytes of the file | Direct
artifact proof of the exact shape described above — installed path,
single-source content, and addendum placement — independent of the eval
harness runtime. |
| Live harness run: `EVAL_CLAUDE_CODE_ALLOW_UNSANDBOXED_LOCAL=true
EVAL_CLAUDE_CODE_MAX_TURNS=45 node packages/evals/dist/cli/cli.js run
b:webtailbench -l 1 -t 1 -c 1 --harness claude_code -e local -m
anthropic/claude-haiku-4-5-20251001` (verbose logging on) | Log line
`Installed browse skill at <temp dir>/.claude/skills/browse/SKILL.md`;
agent then issued `browse open`, `browse snapshot`, `browse doctor`,
`browse status`, `browse stop --force`, `browse cloud fetch`, `browse
skills find` — every one of them accepted by the harness with no "Only
browse commands are allowed" / "Only Skill and Bash are allowed"
contract denial anywhere in the log. Task itself ended `error_max_turns`
fighting a bot-protected United.com page (unrelated to this change) |
Proves the real install → load → drive pipeline works end-to-end with
zero contract errors. Task pass/fail is expected to be noisy on this
benchmark target and is not the bar here; the pipeline mechanics are
what this row proves. |
| **Follow-up (review comment):** `pnpm --dir packages/evals build` (esm
+ cli) after adding `gray-matter` as a devDependency and rewriting
`insertAfterFrontmatter` to use it for boundary detection only | Both
`build:esm`/`build:cli` completed;
`dist/esm/framework/claudeCodeToolAdapter.js` shows `import matter from
"gray-matter"` and the new exported `insertAfterFrontmatter` | Confirms
the new dependency resolves and the adapter still builds against
`packages/cli/skills/browse/SKILL.md`. |
| `node` script against the **built**
`installBrowseSkill`/`insertAfterFrontmatter` (not source), run in a
temp dir | `frontmatter byte-identical to source? true`; text
immediately after the frontmatter is only whitespace before `## Eval
Harness Addendum` (no leftover body content); `## Cloud APIs` still
present after it; direct `insertAfterFrontmatter` calls for the
no-frontmatter fallback and an embedded `---` inside a YAML multiline
string both produced correctly-bounded output | Real artifact proof (not
unit-test mocks) that swapping in gray-matter didn't change the
installed file's byte layout — the specific regression the reviewer's
suggested library could introduce via `matter.stringify()`, which this
implementation deliberately avoids. |
| New unit tests in `claudeCodeToolAdapter.test.ts`:
LF/CRLF/BOM-prefixed frontmatter, a `---` line inside an indented YAML
`>` block, no-frontmatter fallback, unterminated/invalid-YAML fallback
(gray-matter throws; now caught), and a byte-identical-to-source
frontmatter assertion via `installBrowseSkill` | All pass as part of the
17/17 and 362/362 runs above | Locks in the exact boundary-detection
contract the reviewer flagged as fragile; the byte-identical assertion
is a standing regression guard against ever switching to
`matter.stringify()`. |
| `pnpm --dir packages/evals run lint` (prettier + eslint + tsc) | `All
matched files use Prettier code style!`; eslint clean; `tsc --noEmit`
clean | Confirms the gray-matter typings
(`matter.GrayMatterFile<string>`) satisfy the package's strict TS config
and formatting rules. |

## Future work

A separate in-flight PR (`shrey/cli-skills-show`, not yet on main) adds
`browse skills show` (prints the bundled skill to stdout) plus a "Start
here (for AI agents)" pointer in `browse --help`, so a real sandboxed
agent with no eval scaffolding could self-discover the skill instead of
having it handed to it.

This PR's shape — inject the real skill + eval addendum into
`.claude/skills/browse/` at prepare time — stays the right *default*:
it's the conventional eval pattern (benchmarks inject tool docs
deterministically, the agent can't skip it), and it mirrors the actual
supported CLI workflow (a user who already ran `browse skills install`).
It also can't reference `browse skills show` today since that command
doesn't exist on main yet.

Once `browse skills show` ships, "agent discovers the skill itself via
`browse --help`" becomes a good **second, more sandbox-realistic eval
arm** (skill-injected vs. self-discovered A/B), not a replacement —
worth noting for whoever builds it: `isAllowedBrowseCommand` in this
file only checks for a `browse ` prefix and absence of shell
metacharacters, so `browse skills show` already passes that gate today
with zero adapter changes needed for permissions. The one thing that arm
would still need is its own way to deliver the eval-specific overrides
(session/environment pinning, one-command-per-call, out-of-scope
sections) — `browse skills show` would print the bundled skill verbatim,
so that arm likely wants the same `EVAL_HARNESS_ADDENDUM` content
delivered via the top-level task prompt instead of a pre-installed skill
file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Switches the eval harness to install the real `browse` skill from
`packages/cli/skills/browse/SKILL.md`, injecting a small eval-only
addendum right after the frontmatter. Fixes stale guidance and keeps
eval behavior aligned with the CLI (Linear STG-2510).

- **Bug Fixes**
- Source `browse` skill from `packages/cli/skills/browse/SKILL.md` to
stop drift and correct outdated docs/behavior.
- Insert `EVAL_HARNESS_ADDENDUM` after frontmatter to pin env/session,
require one `browse` command per Bash call, and de-scope
cloud/functions/templates/skills install.
- Parse frontmatter with `gray-matter` to handle BOM/CRLF/embedded
`---`, ensuring correct insertion across platforms.

- **Refactors**
- Rename `installBrowserSkill` → `installBrowseSkill`; install to
`.claude/skills/browse/`; update prompts/logs to "browse". Keep
`stagehand_browser` MCP name unchanged.
  - Remove `packages/evals/skills/browser/SKILL.md`.
- Export `insertAfterFrontmatter`, add unit tests for boundary cases and
addendum-before-"Cloud APIs"; add `gray-matter` as a devDependency.

<sup>Written for commit 486cab8.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2334?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
… ref maps (browserbase#2296)

## Summary

`browse snapshot` now prints the formatted accessibility tree only by
default, omitting the `xpathMap`/`urlMap` ref maps that were previously
included on **every** call.

- **`browse snapshot`** (default): formatted tree only (~14KB on a
content-heavy page).
- **`browse snapshot --full`**: tree + `xpathMap` + `urlMap`.
- **`browse snapshot --compact`**: deprecated no-op alias of the default
(prints a stderr-only, TTY-gated deprecation notice).
- **`browse refs`**: prints the cached maps on demand.

Ref-based element commands (`click`, `fill`, `select`, …) are
**unaffected** — the maps are still captured and cached server-side, so
refs resolve exactly as before.

## Why

The default snapshot emitted **~241KB (~60K tokens)** on a content-heavy
page, of which **~217KB (90%) is the `xpathMap`** (a ref→XPath entry for
every node) + `urlMap`. The tree an agent actually reasons over is only
~13.5KB.

Because refs resolve from a **server-side cache** (not from stdout), the
printed maps were dead weight in the context of any agent consuming
`browse snapshot` output. Making the tree lean by default cuts the
default payload ~17× with no loss to element interaction; `--full` and
`browse refs` recover the maps when needed.

## E2E Test Matrix

### Behavior + size (local build, live session on capeair.com)

Verified against `packages/cli/bin/run.js` (the local build), not the
published `browse`.

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `browse snapshot` (default) | `13,845` bytes, keys = `tree` only,
`hasMaps=false` | Lean default; ~17× smaller than before |
| `browse snapshot --full` | `241,104` bytes, keys =
`tree,urlMap,xpathMap` (1,640 entries) | `--full` returns the full tree
+ maps |
| `browse snapshot --compact` (piped) | `13,868` bytes, `hasMaps=false`,
**0 bytes stderr** | Deprecated alias == default; no noise on non-TTY |
| `browse refs` | `count=1640` | Maps still captured + retrievable on
demand |
| `browse click @0-1588` after a **lean** snapshot | `{"clicked": true}`
→ navigated to `/about_us/` | Ref interaction unaffected by dropping
maps from stdout |
| default tree vs `--full` tree | identical (341 lines both) | Lean mode
omits **only** the maps; it does not prune tree content |
| `pnpm --dir packages/cli build` (`tsc`) + evals `tsc --noEmit` |
success / clean | Typechecks |
| `driver-commands` unit test | 15/15 pass | Locks in: default omits
maps, `--full` includes them, maps cached in both modes |

### Agent task-success A/B (lean vs full)

Controlled A/B — same task / model (Sonnet) / session per pair; **the
only variable is snapshot mode** (`--full` = maps vs default = lean),
against the local build.

| Task | Arm | Outcome | Snapshots | Agent tokens |
| --- | --- | --- | --- | --- |
| capeair (regional booking) | full (maps) | ✅ SUCCESS | 9 | 109.8K |
| capeair | **lean** | ✅ SUCCESS | 8 | 93.5K |
| Google Flights | full (maps) | ✅ SUCCESS | 15 | 85.3K |
| Google Flights | **lean** | ✅ SUCCESS | 15 | 86.4K |

**4/4 success — lean-by-default held task success with zero capability
loss.** (A standard-benchmark A/B via the evals package is in progress
and will be added here.)

## What's in the diff

- `packages/cli/src/commands/snapshot.ts` — lean default (`compact`
decoupled from map omission); add `--full`; deprecate `--compact`.
- `packages/cli/src/lib/driver/commands/snapshot.ts` — driver handler
emits the full tree and includes ref maps only when `full` is requested.
- `packages/cli/skills/browse/SKILL.md` — document lean default +
`--full`.
- `packages/cli/tests/driver-commands.test.ts` — test the
default/`--full`/caching behavior.
- `packages/evals/core/tools/browse_cli.ts` — derive `refCount` from the
tree when maps are absent.
- `.changeset/lean-browse-snapshot.md` — `browse` patch.

Linear: [STG-2450](https://linear.app/browserbase/issue/STG-2450)

---------
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
…ILL.md (browserbase#2334)

## Why

The Claude Code eval harness installs a browser skill for the agent it
spawns from `packages/evals/skills/browser/SKILL.md` — a 68-line
eval-local copy created once in "Evals v2" (browserbase#2011) and never touched
again. The real, maintained browse skill lives at
`packages/cli/skills/browse/SKILL.md` (355 lines, ships with the CLI).
Checking git history:

- `packages/evals/skills/browser/SKILL.md`: **1 commit ever**
(2026-05-01, browserbase#2011), never edited since.
- `packages/cli/skills/browse/SKILL.md`: **10 commits since
2026-06-05**, most recently 2 days ago — roughly one edit every 3 days.

That gap already produced a concrete factual error: the most recent
CLI-skill commit (browserbase#2296, STG-2450) made `browse snapshot`
lean-by-default with `--full` needed for ref maps, but the frozen eval
skill still documented the old always-full behavior. It also never
mentioned `--verified`/`--proxies`/`--auto-connect`, `browse doctor`,
retry discipline, tab/network/cdp/mouse/viewport commands, or Browse.sh
skill discovery. The harness was testing an outdated mental model of
`browse`.

## Design

Single source of truth with an install-time eval addendum:

1. `BROWSER_SKILL_SOURCE` → `BROWSE_SKILL_SOURCE`, now pointing at
`packages/cli/skills/browse/SKILL.md` (built from `getRepoRootDir()`,
matching the existing `BROWSE_CLI_ENTRYPOINT`-style constants).
2. `installBrowserSkill` → `installBrowseSkill` now reads the CLI skill
and inserts a code-level `EVAL_HARNESS_ADDENDUM` template literal
**immediately after the YAML frontmatter** (not appended at the end)
before writing the combined file. The addendum:
- States `browse` is preinstalled/pinned by the harness (no `npm
install`, no `--local`/`--remote`/`--session` — the wrapper injects
those).
- Requires exactly one `browse ...` command per Bash call (shell
operators rejected by the harness).
- Tells the model to ignore the CLI skill's
install/Browse.sh-discovery/cloud/Functions/templates sections — out of
scope during evals.
- Reiterates no repo edits, no non-`browse` network tools, and the
`EVAL_RESULT` reporting format.

Why prepend and not append: `isAllowedBrowseCommand` only checks that a
Bash command starts with `browse ` and has no shell metacharacters — it
does **not** restrict which `browse` subcommand runs. So the addendum's
"ignore cloud/functions/skills" instruction is the actual
scope-enforcement mechanism, not just a courtesy note, and it needs to
be read before the model encounters the CLI skill's concrete (and
tempting) examples of those commands, not after. A live smoke run (see
below) shows the model reaching for `browse cloud fetch` and `browse
skills find` once it got stuck on a bot-protected page — evidence this
ordering concern is real, not theoretical.

3. Skill name consistency: installed skill dir renamed
`.claude/skills/browser/` → `.claude/skills/browse/` to match the CLI
skill's own `name: browse` frontmatter; all harness prompt/log
references to "a project skill named browser" updated to "browse". The
`stagehand_browser` MCP server name (used by the unrelated
`playwright_code`/`cdp_code` tool surfaces) is untouched.
4. Deleted `packages/evals/skills/browser/SKILL.md` and the now-empty
`packages/evals/skills/` directory.
5. Updated
`packages/evals/tests/framework/claudeCodeToolAdapter.test.ts` for the
renamed export/path/skill-name, plus new assertions that the installed
file contains both the CLI-skill content and the addendum, with the
addendum's string index before `## Cloud APIs`'s index (regression guard
against the addendum silently drifting back to append-at-end).

**Follow-up (review comment from
[ajmcquilkin](browserbase#2334 (comment)
the hand-rolled regex in `insertAfterFrontmatter` had already needed a
CRLF patch and still failed silently on BOM-prefixed files or a `---`
line embedded in a YAML multiline string. Swapped it for
[`gray-matter`](https://www.npmjs.com/package/gray-matter) (new
`packages/evals` devDependency, private package, no changeset), but only
for *boundary detection* — `matter(markdown)` locates where the
frontmatter block ends; reassembly still uses the original raw string
(`markdown.slice(0, markdown.length - parsed.content.length)` for the
frontmatter, `parsed.content` for the body) rather than
`matter.stringify()`, since that would re-serialize the YAML through
js-yaml and reformat the shipped skill's frontmatter (e.g. its folded
`description: >` block). `insertAfterFrontmatter` is now exported and
directly unit-tested.

No changeset — `packages/evals` is private, eval-infra only.

**Overlap note:** this touches `claudeCodeToolAdapter.ts`; open PR browserbase#2299
also touches that file but in a different region (contract fix, not the
skill-install path). Trivial rebase for whichever lands second.

Linear:
[STG-2510](https://linear.app/browserbase/issue/STG-2510/evals-source-browse-skill-from-packagescli-skillmd-instead-of-stale)

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm turbo run build --filter=@browserbasehq/stagehand
--filter=browse` then `pnpm --dir packages/evals build` | All 4 turbo
tasks + evals `build:esm`/`build:cli` completed successfully in
`<worktree>` | Confirms the changed adapter compiles against the real
CLI/core build artifacts it now depends on
(`packages/cli/skills/browse/SKILL.md`, `packages/cli/dist/...`). |
| `pnpm --dir packages/evals exec vitest run
tests/framework/claudeCodeToolAdapter.test.ts` | `Test Files 1 passed
(1)`, `Tests 17 passed (17)` | Covers the renamed export, new install
path/skill name, the addendum-ordering assertion, and (as of the
gray-matter follow-up below) the frontmatter boundary-detection cases.
Narrow to this file. |
| `pnpm --dir packages/evals run test:unit` (full evals suite) | `Test
Files 48 passed (48)`, `Tests 362 passed (362)` | Confirms no other test
in the package depends on the old `browser` skill name/path,
`installBrowserSkill` export, or the removed regex helper. |
| `node -e` script importing `installBrowseSkill` from built
`packages/evals/dist/esm/framework/claudeCodeToolAdapter.js`, run
against a temp dir | Installed at `<temp
dir>/.claude/skills/browse/SKILL.md`; head matches CLI skill frontmatter
(`name: browse`, full description); tail/body contains `## Eval Harness
Addendum` positioned before `## Cloud APIs` (indices 1054 vs 9018);
frontmatter YAML block intact as the first bytes of the file | Direct
artifact proof of the exact shape described above — installed path,
single-source content, and addendum placement — independent of the eval
harness runtime. |
| Live harness run: `EVAL_CLAUDE_CODE_ALLOW_UNSANDBOXED_LOCAL=true
EVAL_CLAUDE_CODE_MAX_TURNS=45 node packages/evals/dist/cli/cli.js run
b:webtailbench -l 1 -t 1 -c 1 --harness claude_code -e local -m
anthropic/claude-haiku-4-5-20251001` (verbose logging on) | Log line
`Installed browse skill at <temp dir>/.claude/skills/browse/SKILL.md`;
agent then issued `browse open`, `browse snapshot`, `browse doctor`,
`browse status`, `browse stop --force`, `browse cloud fetch`, `browse
skills find` — every one of them accepted by the harness with no "Only
browse commands are allowed" / "Only Skill and Bash are allowed"
contract denial anywhere in the log. Task itself ended `error_max_turns`
fighting a bot-protected United.com page (unrelated to this change) |
Proves the real install → load → drive pipeline works end-to-end with
zero contract errors. Task pass/fail is expected to be noisy on this
benchmark target and is not the bar here; the pipeline mechanics are
what this row proves. |
| **Follow-up (review comment):** `pnpm --dir packages/evals build` (esm
+ cli) after adding `gray-matter` as a devDependency and rewriting
`insertAfterFrontmatter` to use it for boundary detection only | Both
`build:esm`/`build:cli` completed;
`dist/esm/framework/claudeCodeToolAdapter.js` shows `import matter from
"gray-matter"` and the new exported `insertAfterFrontmatter` | Confirms
the new dependency resolves and the adapter still builds against
`packages/cli/skills/browse/SKILL.md`. |
| `node` script against the **built**
`installBrowseSkill`/`insertAfterFrontmatter` (not source), run in a
temp dir | `frontmatter byte-identical to source? true`; text
immediately after the frontmatter is only whitespace before `## Eval
Harness Addendum` (no leftover body content); `## Cloud APIs` still
present after it; direct `insertAfterFrontmatter` calls for the
no-frontmatter fallback and an embedded `---` inside a YAML multiline
string both produced correctly-bounded output | Real artifact proof (not
unit-test mocks) that swapping in gray-matter didn't change the
installed file's byte layout — the specific regression the reviewer's
suggested library could introduce via `matter.stringify()`, which this
implementation deliberately avoids. |
| New unit tests in `claudeCodeToolAdapter.test.ts`:
LF/CRLF/BOM-prefixed frontmatter, a `---` line inside an indented YAML
`>` block, no-frontmatter fallback, unterminated/invalid-YAML fallback
(gray-matter throws; now caught), and a byte-identical-to-source
frontmatter assertion via `installBrowseSkill` | All pass as part of the
17/17 and 362/362 runs above | Locks in the exact boundary-detection
contract the reviewer flagged as fragile; the byte-identical assertion
is a standing regression guard against ever switching to
`matter.stringify()`. |
| `pnpm --dir packages/evals run lint` (prettier + eslint + tsc) | `All
matched files use Prettier code style!`; eslint clean; `tsc --noEmit`
clean | Confirms the gray-matter typings
(`matter.GrayMatterFile<string>`) satisfy the package's strict TS config
and formatting rules. |

## Future work

A separate in-flight PR (`shrey/cli-skills-show`, not yet on main) adds
`browse skills show` (prints the bundled skill to stdout) plus a "Start
here (for AI agents)" pointer in `browse --help`, so a real sandboxed
agent with no eval scaffolding could self-discover the skill instead of
having it handed to it.

This PR's shape — inject the real skill + eval addendum into
`.claude/skills/browse/` at prepare time — stays the right *default*:
it's the conventional eval pattern (benchmarks inject tool docs
deterministically, the agent can't skip it), and it mirrors the actual
supported CLI workflow (a user who already ran `browse skills install`).
It also can't reference `browse skills show` today since that command
doesn't exist on main yet.

Once `browse skills show` ships, "agent discovers the skill itself via
`browse --help`" becomes a good **second, more sandbox-realistic eval
arm** (skill-injected vs. self-discovered A/B), not a replacement —
worth noting for whoever builds it: `isAllowedBrowseCommand` in this
file only checks for a `browse ` prefix and absence of shell
metacharacters, so `browse skills show` already passes that gate today
with zero adapter changes needed for permissions. The one thing that arm
would still need is its own way to deliver the eval-specific overrides
(session/environment pinning, one-command-per-call, out-of-scope
sections) — `browse skills show` would print the bundled skill verbatim,
so that arm likely wants the same `EVAL_HARNESS_ADDENDUM` content
delivered via the top-level task prompt instead of a pre-installed skill
file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Switches the eval harness to install the real `browse` skill from
`packages/cli/skills/browse/SKILL.md`, injecting a small eval-only
addendum right after the frontmatter. Fixes stale guidance and keeps
eval behavior aligned with the CLI (Linear STG-2510).

- **Bug Fixes**
- Source `browse` skill from `packages/cli/skills/browse/SKILL.md` to
stop drift and correct outdated docs/behavior.
- Insert `EVAL_HARNESS_ADDENDUM` after frontmatter to pin env/session,
require one `browse` command per Bash call, and de-scope
cloud/functions/templates/skills install.
- Parse frontmatter with `gray-matter` to handle BOM/CRLF/embedded
`---`, ensuring correct insertion across platforms.

- **Refactors**
- Rename `installBrowserSkill` → `installBrowseSkill`; install to
`.claude/skills/browse/`; update prompts/logs to "browse". Keep
`stagehand_browser` MCP name unchanged.
  - Remove `packages/evals/skills/browser/SKILL.md`.
- Export `insertAfterFrontmatter`, add unit tests for boundary cases and
addendum-before-"Cloud APIs"; add `gray-matter` as a devDependency.

<sup>Written for commit 486cab8.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2334?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants