Skip to content

[STG-2510] fix: source eval browser skill from packages/cli browse SKILL.md - #2334

Merged
shrey150 merged 3 commits into
mainfrom
shrey/evals-skill-from-cli
Jul 14, 2026
Merged

[STG-2510] fix: source eval browser skill from packages/cli browse SKILL.md#2334
shrey150 merged 3 commits into
mainfrom
shrey/evals-skill-from-cli

Conversation

@shrey150

@shrey150 shrey150 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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, Evals v2 #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_SOURCEBROWSE_SKILL_SOURCE, now pointing at packages/cli/skills/browse/SKILL.md (built from getRepoRootDir(), matching the existing BROWSE_CLI_ENTRYPOINT-style constants).

  2. installBrowserSkillinstallBrowseSkill 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): 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 (new packages/evals devDependency, private package, no changeset), but only for boundary detectionmatter(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

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


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 installBrowserSkillinstallBrowseSkill; 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.

Written for commit 486cab8. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 486cab8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

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

Click here to learn what changesets are, and how to add one.

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

@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.

All reported issues were addressed across 3 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
    participant Harness as Eval Harness
    participant Adapter as claudeCodeToolAdapter
    participant Fs as File System
    participant Skill as CLI Browse Skill (packages/cli/skills/browse/SKILL.md)
    participant Agent as Claude Code Agent (Eval)
    participant Prompt as Harness Prompt
    
    Note over Harness,Prompt: Eval Setup: Skill Installation
    
    Harness->>Adapter: prepareBrowseCliHarnessAdapter()
    Adapter->>Adapter: installBrowseSkill(wrapperDir)
    Adapter->>Fs: readFile(BROWSE_SKILL_SOURCE)
    Fs-->>Adapter: CLI skill markdown (355 lines, single source of truth)
    Adapter->>Adapter: insertAfterFrontmatter(cliSkill, EVAL_HARNESS_ADDENDUM)
    Note over Adapter: Injects addendum between YAML frontmatter and CLI skill body
    Adapter->>Fs: writeFile(.claude/skills/browse/SKILL.md)
    Fs-->>Adapter: written
    Adapter-->>Harness: Installed browse skill at ...
    
    Note over Agent,Skill: Eval Runtime: Agent Loads & Uses Skill
    
    Agent->>Skill: Skill tool: load browse skill
    Skill-->>Agent: Combined skill content (CLI skill + addendum)
    Note over Agent: Addendum overrides CLI cloud/functions/skills examples
    Agent->>Agent: Follows addendum rules (one browse command per Bash call)
    Agent->>Adapter: Bash: browse open <url>
    Adapter->>Adapter: isAllowedBrowseCommand() - checks starts with "browse " + no shell metacharacters
    
    alt Valid browse command
        Adapter-->>Agent: Executes browse command
        Agent->>Adapter: Bash: browse snapshot
        Adapter->>Adapter: isAllowedBrowseCommand()
        Adapter-->>Agent: Executes browse snapshot
        Agent->>Adapter: Bash: browse doctor
        Adapter->>Adapter: isAllowedBrowseCommand()
        Adapter-->>Agent: Executes browse doctor
    else Command violates rules
        Agent->>Adapter: Bash: browse cloud fetch
        Adapter->>Adapter: isAllowedBrowseCommand()
        Note over Adapter: Allowed syntactically, but addendum says ignore
        Agent->>Agent: Addendum prohibits cloud commands, agent should skip
    end
    
    Note over Agent,Harness: Eval Runtime: Result Reporting
    
    Agent->>Prompt: EVAL_RESULT format
    Prompt-->>Harness: Benchmark result
    
    alt Agent attempts disallowed pattern
        Agent->>Adapter: Bash: browse snapshot | grep title
        Adapter->>Adapter: isAllowedBrowseCommand() - detects shell operators
        Adapter-->>Agent: Rejected: only single browse command allowed
    end
Loading

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

Re-trigger cubic

Comment thread packages/evals/framework/claudeCodeToolAdapter.ts Outdated
Comment thread packages/evals/framework/claudeCodeToolAdapter.ts Outdated
shrey150 added a commit that referenced this pull request Jul 9, 2026
…idance (#2336)

## Summary

A 3-provider bare-loop smoke test (E2B/Modal/Vercel, real sandboxes,
local CLI build) surfaced two systematic gaps in the bundled `browse`
skill (`packages/cli/skills/browse/SKILL.md`):

1. **3/3 agents ignored the pre-wired `BROWSE_SESSION` env var** and
self-named sessions via `--session <name>` (one run orphaned a
Browserbase session as a result). The skill taught named `--session`
usage but never said "if `BROWSE_SESSION` is already set, every command
already targets that session — don't pass `--session` or invent a new
name." This defeats the sandbox-template env-steering pattern
(`BROWSERBASE_API_KEY` + `BROWSE_SESSION=<name>` → flagless remote
commands).
2. **2/3 agents tried `--local` first inside Chrome-less containers**,
hit "No Chrome or Chromium found", then recovered on retry. The skill
lacked explicit guidance that `--local` requires local Chrome/Chromium
and that containers/CI/sandboxes without Chrome should go straight to
`--remote`.

## What changed

Two minimal additions to `packages/cli/skills/browse/SKILL.md`, both in
the existing "Browser Target Selection" section (the section that
already teaches local/remote choice and named-session usage) — no
restructuring, no new sections:

```
`--local` requires Chrome or Chromium already installed on the machine. In containers, CI, and sandboxes with no browser installed, use `--remote` instead of `--local`. If `--local` fails with "No Chrome or Chromium found" and `BROWSERBASE_API_KEY` is set, switch to `--remote` — do not retry `--local`.
```

```
If `BROWSE_SESSION` is already set in the environment, every command already targets that session — do not pass `--session` or invent a new name. An explicit `--session <name>` always overrides `BROWSE_SESSION` for that command, so only pass it to deliberately target a different session.
```

Verified the precedence claim against the actual implementation before
writing the wording (not just trusting the smoke-test writeup):

- `packages/cli/src/lib/driver/flags.ts:73` — `export function
sessionName(value?: string): string { return value ??
process.env.BROWSE_SESSION ?? "default"; }`. An explicit `--session`
flag always wins over `BROWSE_SESSION`; when omitted, `BROWSE_SESSION`
becomes the effective session; with neither, it's `"default"`.
- `packages/cli/src/lib/driver/command-cli.ts:75` —
`runDriverCommandFromFlags` calls `sessionName(flags.session)` for every
driver command, so this precedence applies uniformly across `open`,
`snapshot`, `click`, `stop`, etc.
- `packages/cli/src/lib/driver/remote.ts:130` — exact no-Chrome error
string is `"No Chrome or Chromium found on this machine. Install one
(Linux: apt install chromium · macOS: brew install --cask google-chrome,
or Chromium with CHROME_PATH set), attach to a running browser with
--cdp <port>, or set BROWSERBASE_API_KEY to use a remote browser."`

## No changeset

This is a skill-text-only change (`SKILL.md`), same category as PR #2329
(skill-text-only fix, no changeset). The updated text ships with the
next `browse` release — a release is already queued by #2335's changeset
— and is consumed at runtime by the evals harness added in #2334.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm install && pnpm turbo run build --filter=browse` (worktree,
local build) | `Tasks: 4 successful, 4 total` | Proves the local CLI
build succeeds with the change in place; no build regressions from a
docs-only edit. |
| Resolve `bundledCliSkillPath()` from the compiled
`dist/lib/skills/install.js` via `require.resolve` + manual path join
(no `skills show` command exists on `main` yet — it ships in PR #2335,
unmerged at time of this PR) | `resolved bundledCliSkillPath:
<worktree>/packages/cli/skills/browse` | Proves the exact file `browse
skills install` resolves to at runtime is the file this PR edits, not a
stale copy. |
| `grep -n "already set in the environment\|do not retry"
packages/cli/skills/browse/SKILL.md` | `76:If \`BROWSE_SESSION\` is
already set in the environment, every command already targets that
session...` `68:...If \`--local\` fails with "No Chrome or Chromium
found"...do not retry \`--local\`.` | Confirms the exact new lines are
present at the bundled path resolved above — the new guidance flows
through to the real command path. |
| `node bin/run.js skills install --help` | Prints usage for `browse
skills install` (no side effects triggered — did not run the real
install, which mutates the global `~/.agents/skills` pool via `npx
skills add --global`) | Confirms the command is wired and reachable;
deliberately did not execute the real install to avoid mutating local
global skill state outside this change's scope. |
| `pnpm test:cli` (vitest, worktree build) | `Test Files 23 passed (23)`
/ `Tests 344 passed (344)` | Full existing CLI test suite still green;
this change touches no runtime code. |
| `pnpm eslint .` | No output / exit 0 | Lint clean. |
| `pnpm format:check` (prettier) | `All matched files use Prettier code
style!` | Formatting clean, including the edited Markdown. |
| `tsc --noEmit -p tsconfig.json` | No output / exit 0 | Typecheck clean
(unaffected by a Markdown-only change, included for completeness). |

## Related

- Linear:
https://linear.app/browserbase/issue/STG-2513/browse-skill-document-browse-session-precedence-container-remote
- Evidence source: 3-provider (E2B/Modal/Vercel) bare-loop smoke test,
2026-07-09
- Text will be exercised by the evals harness landing in #2334
- Release vehicle: changeset already queued in #2335

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Clarifies the `browse` skill docs to prevent session misrouting and
container failures by documenting `BROWSE_SESSION` precedence and when
to use `--remote` instead of `--local`. Addresses Linear STG-2513.

- **Bug Fixes**
- Documented that if `BROWSE_SESSION` is set, commands target that
session; only pass `--session` to override (precedence: `--session` >
`BROWSE_SESSION` > `default`).
- Stated that `--local` requires Chrome/Chromium; in
containers/CI/sandboxes use `--remote`. If you see "No Chrome or
Chromium found," switch to `--remote` instead of retrying `--local`.

<sup>Written for commit 829ff7b.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2336?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>
shrey150 and others added 3 commits July 9, 2026 18:27
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses cubic review comment on #2334.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…regex

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shrey150
shrey150 force-pushed the shrey/evals-skill-from-cli branch from 2b7bbfc to 486cab8 Compare July 9, 2026 22:28
@shrey150
shrey150 merged commit f7631d4 into main Jul 14, 2026
915 of 935 checks passed
shrey150 added a commit that referenced this pull request Jul 14, 2026
…, gate skills-discovery under none, shorten session names for macOS's socket limit

Three related bugs in the external-harness skill-delivery path:

- externalHarnessToolAdapter.ts's "injected" mode read the stale
  packages/evals/skills/browser/SKILL.md, which #2334 deleted in favor of
  the real packages/cli/skills/browse/SKILL.md + EVAL_HARNESS_ADDENDUM
  (claudeCodeToolAdapter.ts). Factored that read+splice into an exported
  buildAddendedSkillText() so both paths share one source of truth.
- "prompt_show" ran the real `browse skills show`, which prints the raw CLI
  skill with no addendum -- so the model never saw the addendum's "don't
  pass --session/--local/--remote yourself" rule and collided with this
  wrapper's own flag injection ("Flag --session can only be specified once",
  "Pass either --local or --remote, not both"). runBareBrowseCommand now
  appends EVAL_HARNESS_ADDENDUM to a successful `skills show` call's output,
  so prompt_show and injected deliver identical content and differ only in
  delivery mechanism -- the actual controlled variable this experiment
  measures.
- "none" is supposed to be a zero-skill-content baseline, but the model
  could defeat it by running `--help`, which points at `browse skills
  show`. runBareBrowseCommand now rejects any `skills ...` command when
  skillMode=none.

Also shortens the eval session-name generator (createBrowseSessionName):
`evals-claude-${pid}-${Date.now()}-${suffix}` produced an ~111-byte Unix
socket path once combined with the daemon's runtime dir, over macOS's
~104-byte sockaddr_un limit, so the daemon exited silently on bind and every
local run timed out waiting 30s. Hashed down to a short fixed-length name
(measured: 87 bytes on this machine, vs 111 before).
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
…idance (browserbase#2336)

## Summary

A 3-provider bare-loop smoke test (E2B/Modal/Vercel, real sandboxes,
local CLI build) surfaced two systematic gaps in the bundled `browse`
skill (`packages/cli/skills/browse/SKILL.md`):

1. **3/3 agents ignored the pre-wired `BROWSE_SESSION` env var** and
self-named sessions via `--session <name>` (one run orphaned a
Browserbase session as a result). The skill taught named `--session`
usage but never said "if `BROWSE_SESSION` is already set, every command
already targets that session — don't pass `--session` or invent a new
name." This defeats the sandbox-template env-steering pattern
(`BROWSERBASE_API_KEY` + `BROWSE_SESSION=<name>` → flagless remote
commands).
2. **2/3 agents tried `--local` first inside Chrome-less containers**,
hit "No Chrome or Chromium found", then recovered on retry. The skill
lacked explicit guidance that `--local` requires local Chrome/Chromium
and that containers/CI/sandboxes without Chrome should go straight to
`--remote`.

## What changed

Two minimal additions to `packages/cli/skills/browse/SKILL.md`, both in
the existing "Browser Target Selection" section (the section that
already teaches local/remote choice and named-session usage) — no
restructuring, no new sections:

```
`--local` requires Chrome or Chromium already installed on the machine. In containers, CI, and sandboxes with no browser installed, use `--remote` instead of `--local`. If `--local` fails with "No Chrome or Chromium found" and `BROWSERBASE_API_KEY` is set, switch to `--remote` — do not retry `--local`.
```

```
If `BROWSE_SESSION` is already set in the environment, every command already targets that session — do not pass `--session` or invent a new name. An explicit `--session <name>` always overrides `BROWSE_SESSION` for that command, so only pass it to deliberately target a different session.
```

Verified the precedence claim against the actual implementation before
writing the wording (not just trusting the smoke-test writeup):

- `packages/cli/src/lib/driver/flags.ts:73` — `export function
sessionName(value?: string): string { return value ??
process.env.BROWSE_SESSION ?? "default"; }`. An explicit `--session`
flag always wins over `BROWSE_SESSION`; when omitted, `BROWSE_SESSION`
becomes the effective session; with neither, it's `"default"`.
- `packages/cli/src/lib/driver/command-cli.ts:75` —
`runDriverCommandFromFlags` calls `sessionName(flags.session)` for every
driver command, so this precedence applies uniformly across `open`,
`snapshot`, `click`, `stop`, etc.
- `packages/cli/src/lib/driver/remote.ts:130` — exact no-Chrome error
string is `"No Chrome or Chromium found on this machine. Install one
(Linux: apt install chromium · macOS: brew install --cask google-chrome,
or Chromium with CHROME_PATH set), attach to a running browser with
--cdp <port>, or set BROWSERBASE_API_KEY to use a remote browser."`

## No changeset

This is a skill-text-only change (`SKILL.md`), same category as PR browserbase#2329
(skill-text-only fix, no changeset). The updated text ships with the
next `browse` release — a release is already queued by browserbase#2335's changeset
— and is consumed at runtime by the evals harness added in browserbase#2334.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm install && pnpm turbo run build --filter=browse` (worktree,
local build) | `Tasks: 4 successful, 4 total` | Proves the local CLI
build succeeds with the change in place; no build regressions from a
docs-only edit. |
| Resolve `bundledCliSkillPath()` from the compiled
`dist/lib/skills/install.js` via `require.resolve` + manual path join
(no `skills show` command exists on `main` yet — it ships in PR browserbase#2335,
unmerged at time of this PR) | `resolved bundledCliSkillPath:
<worktree>/packages/cli/skills/browse` | Proves the exact file `browse
skills install` resolves to at runtime is the file this PR edits, not a
stale copy. |
| `grep -n "already set in the environment\|do not retry"
packages/cli/skills/browse/SKILL.md` | `76:If \`BROWSE_SESSION\` is
already set in the environment, every command already targets that
session...` `68:...If \`--local\` fails with "No Chrome or Chromium
found"...do not retry \`--local\`.` | Confirms the exact new lines are
present at the bundled path resolved above — the new guidance flows
through to the real command path. |
| `node bin/run.js skills install --help` | Prints usage for `browse
skills install` (no side effects triggered — did not run the real
install, which mutates the global `~/.agents/skills` pool via `npx
skills add --global`) | Confirms the command is wired and reachable;
deliberately did not execute the real install to avoid mutating local
global skill state outside this change's scope. |
| `pnpm test:cli` (vitest, worktree build) | `Test Files 23 passed (23)`
/ `Tests 344 passed (344)` | Full existing CLI test suite still green;
this change touches no runtime code. |
| `pnpm eslint .` | No output / exit 0 | Lint clean. |
| `pnpm format:check` (prettier) | `All matched files use Prettier code
style!` | Formatting clean, including the edited Markdown. |
| `tsc --noEmit -p tsconfig.json` | No output / exit 0 | Typecheck clean
(unaffected by a Markdown-only change, included for completeness). |

## Related

- Linear:
https://linear.app/browserbase/issue/STG-2513/browse-skill-document-browse-session-precedence-container-remote
- Evidence source: 3-provider (E2B/Modal/Vercel) bare-loop smoke test,
2026-07-09
- Text will be exercised by the evals harness landing in browserbase#2334
- Release vehicle: changeset already queued in browserbase#2335

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Clarifies the `browse` skill docs to prevent session misrouting and
container failures by documenting `BROWSE_SESSION` precedence and when
to use `--remote` instead of `--local`. Addresses Linear STG-2513.

- **Bug Fixes**
- Documented that if `BROWSE_SESSION` is set, commands target that
session; only pass `--session` to override (precedence: `--session` >
`BROWSE_SESSION` > `default`).
- Stated that `--local` requires Chrome/Chromium; in
containers/CI/sandboxes use `--remote`. If you see "No Chrome or
Chromium found," switch to `--remote` instead of retrying `--local`.

<sup>Written for commit 829ff7b.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2336?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. -->
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