Communitygithub.com

browser-use/browser-harness-js

Controla Chrome a través del Protocolo DevTools desde JavaScript. Ejecuta fragmentos JS mediante la CLI `browser-harness-js`, que inicia automáticamente un servidor HTTP persistente con una `Session` CDP completamente tipada.

¿Qué es browser-harness-js?

**browser-harness-js** es una habilidad para agentes de IA (compatible con Claude Code, Cursor, Codex, etc.) que permite controlar Chrome mediante el Protocolo DevTools (CDP) directamente desde JavaScript. Proporciona una CLI (`browser-harness-js`) que inicia automáticamente un servidor HTTP persistente basado en Bun, el cual mantiene una única conexión WebSocket y una `Session` CDP completamente tipada (652 métodos agrupados en 56 dominios). Esto permite ejecutar fragmentos JS de forma interactiva, como `browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'`, conservando el estado (sesión, target activo, variables globales) entre llamadas. Es ideal para automatizar, inspeccionar o depurar navegadores Chromium (Chrome, Edge, Brave, Arc, etc.) de forma estable y eficiente, evitando tener que reconectar en cada comando.

Compatible con✓Claude Code~Codex CLI✓Cursor
npx skills add browser-use/browser-harness-js

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

CDP — Habilidad browser-harness-js

SDK de CDP personalizado (cada método de browser_protocol.json + js_protocol.json tiene un envoltorio tipado) más un pequeño servidor HTTP que mantiene una Session CDP persistente. La CLI browser-harness-js inicia automáticamente el servidor en el primer uso y envía fragmentos JS a él.

El SDK reside en el directorio sdk/ de la habilidad. En el resto de este documento, <directorio-habilidad> se refiere a donde npx skills add instaló la habilidad (Claude Code: ~/.claude/skills/cdp; Cursor: ~/.cursor/skills/cdp; otros agentes varían). La CLI debería estar en el PATH como browser-harness-js.

Configuración (una vez, primer uso)

npx skills add coloca la habilidad en el directorio de habilidades de tu agente, pero NO pone la CLI en el PATH. Antes de la primera llamada, verifica que sea accesible y crea un enlace simbólico en cualquier directorio de tu PATH si no lo está:

# macOS (Apple Silicon + Homebrew)
command -v browser-harness-js >/dev/null || ln -sf <directorio-habilidad>/sdk/browser-harness-js /opt/homebrew/bin/browser-harness-js

# macOS (Intel) / la mayoría de Linux — puede requerir sudo
command -v browser-harness-js >/dev/null || ln -sf <directorio-habilidad>/sdk/browser-harness-js /usr/local/bin/browser-harness-js

# Linux sin sudo (asegúrate de que ~/.local/bin esté en el PATH)
command -v browser-harness-js >/dev/null || { mkdir -p ~/.local/bin && ln -sf <directorio-habilidad>/sdk/browser-harness-js ~/.local/bin/browser-harness-js; }

La CLI instala automáticamente bun en la primera ejecución si falta (el servidor es nativo de Bun). Establece BROWSER_HARNESS_SKIP_BUN_INSTALL=1 para no hacerlo.

Cómo usar

Simplemente ejecuta browser-harness-js '<JS>'. La primera llamada inicia el servidor en segundo plano; las llamadas siguientes utilizan el mismo proceso y, por lo tanto, reutilizan la misma session, el mismo WebSocket con Chrome y cualquier variable global que hayas definido.

browser-harness-js 'await session.connect()'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
browser-harness-js '(await session.Runtime.evaluate({expression:"document.title",returnByValue:true})).result.value'

La salida es el contenido del resultado bruto — sin envoltorio {ok,result}.

Tipo de resultadostdout
stringtexto sin formato, sin comillas JSON (ej. Example Domain)
number / boolean42, true
object / array (no vacío)JSON compacto (ej. {"frameId":"..."}, [1,2,3])
undefined / null / "" / {} / []vacío (sin salida)

Los errores van a stderr, código de salida 1. El mensaje de error CDP y la traza JS se imprimen textualmente, ej.:

Error: CDP -32602: invalid params
    at _call (.../session.ts:117:33)
    ...

Detecta fallos con if browser-harness-js '...'; then ...; else handle_error; fi o verificando $?.

Fragmentos multi-línea mediante stdin (heredoc). Importante: un fragmento con varias sentencias NO retorna automáticamente la última expresión — escribe return X explícitamente. Los fragmentos de una sola expresión pasados como primer argumento SÍ retornan automáticamente.

browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF

Comandos de la CLI

ComandoComportamiento
browser-harness-js '<js>'Inicia automáticamente el servidor si es necesario, evalúa el JS, imprime el resultado.
browser-harness-js <<EOF…EOFIgual, código desde stdin.
browser-harness-js --statusImprime JSON de estado (tiempo de actividad, conectado, sessionId) o sale con código 1 si está caído.
browser-harness-js --startInicio explícito (no hace nada si ya está en ejecución).
browser-harness-js --stopApagado controlado. Borra el estado de la sesión.
browser-harness-js --restartDetener + iniciar de nuevo.
browser-harness-js --logstail -f del registro del servidor (/tmp/browser-harness-js.log).

Variables de entorno: CDP_REPL_PORT (por defecto 9876), CDP_REPL_LOG (por defecto /tmp/browser-harness-js.log).

Superficie API dentro de los fragmentos

Estas globales están precargadas — no se necesitan imports:

  • session — la Session persistente. Tiene todos los dominios CDP montados: session.Page, session.DOM, session.Runtime, session.Network, … 56 dominios, 652 métodos en total.
  • listPageTargets() — lista los targets de página reales mediante Target.getTargets de CDP (funciona también en Chrome 144+), filtrando las URL chrome:// y devtools://. Sin argumentos — usa la sesión conectada.
  • detectBrowsers() — escanea los directorios de perfil específicos del SO en busca de navegadores basados en Chromium en ejecución con depuración remota activada. Devuelve [{name, profileDir, port, wsPath, wsUrl, mtimeMs}], ordenados por el más recientemente lanzado.
  • resolveWsUrl(opts) — resuelve una URL WS a partir de {wsUrl} | {port, host?} | {profileDir}. Para el flujo de detección automática sin argumentos, llama directamente a session.connect().
  • CDP — los espacios de nombres generados (CDP.Page, CDP.Runtime, …) para referencia de nombres de tipos.

Llamar a un método CDP

Cada método toma un único argumento de objeto que coincide con los parámetros wire de CDP; se resuelve al valor de retorno tipado (sin envoltorio result, sin correlación id — se maneja por ti).

// sin parámetros
await session.DOM.enable()

// parámetros requeridos
await session.Page.navigate({ url: 'https://example.com' })

// parámetros todos opcionales (el objeto también es opcional)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })

// los retornos se reducen a la forma tipada
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })

Conexión

Por defecto: simplemente llama a session.connect() sin argumentos. Detecta automáticamente los navegadores basados en Chromium en ejecución (Chrome, Chromium, Edge, Brave, Arc, Vivaldi, Opera, Comet, Canary) escaneando los directorios de perfil específicos del SO en busca de un archivo DevToolsActivePort, ordenados por el más recientemente lanzado, y elige el primero cuyo WebSocket acepte la conexión. Agnóstico al SO — funciona en macOS, Linux y Windows.

await session.connect()   // detección automática

Usa detectBrowsers() primero si quieres ver qué está disponible (o dejar que el usuario elija) antes de conectarte:

const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]

Formas explícitas — úsalas solo cuando la detección automática elija el navegador incorrecto, o cuando ya sepas dónde conectarte:

FormaCuándo usarla
{ profileDir }Apuntar a un navegador específico cuando hay varios en ejecución. Lee <profileDir>/DevToolsActivePort directamente.
{ wsUrl }Ya tienes ws://…/devtools/browser/<uuid> (ej. canalizado desde otro lugar).
await session.connect({ profileDir: '/Users/<tu>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })

Rutas de perfil por SO — úsalas con { profileDir }:

  • macOS: ~/Library/Application Support/<Navegador> (ej. Google/Chrome, Comet, BraveSoftware/Brave-Browser, Arc/User Data)
  • Linux: ~/.config/<navegador> (ej. google-chrome, chromium, BraveSoftware/Brave-Browser)
  • Windows: %LOCALAPPDATA%\<Navegador>\User Data (ej. Google\Chrome, Microsoft\Edge, BraveSoftware\Brave-Browser)

El tiempo de espera por candidato para la apertura WS es por defecto de 5s — los navegadores activos responden con open/close en ~100ms, por lo que 5s ya es generoso. El único caso en que 5s es demasiado corto es cuando Chrome muestra la ventana emergente Permitir y espera a que el usuario haga clic. Si esperas eso, pasa timeoutMs: 30000:

await session.connect({ profileDir: '/Users/<tu>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })

Si ves No detected browser accepted a connection — los navegadores tienen archivos DevToolsActivePort, pero ninguno está sirviendo WS actualmente. La causa más común: la depuración remota está habilitada pero el usuario no ha hecho clic en Permitir en la solicitud. Indícale que haga clic en Permitir y luego reintente (o aumenta timeoutMs).

Seleccionar un target (pestaña)

Después de connect(), llama a session.use(targetId) una vez; las llamadas posteriores a nivel de página (Page/DOM/Runtime/Network/etc.) se enrutan automáticamente al sessionId de ese target. Las llamadas Browser.* y Target.* siempre van al endpoint del navegador.

const tabs = await listPageTargets()                     // sin argumentos; usa la sesión conectada
const sid  = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })

listPageTargets() usa Target.getTargets de CDP (no /json), por lo que funciona también en Chrome 144+. Ya filtra las URL chrome:// y devtools://. Llamada bruta equivalente:

const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))

Para cambiar de pestaña: session.use(otroTargetId). Para desconectar: session.setActiveSession(undefined).

Eventos

// Suscribirse (devuelve una función para cancelar la suscripción)
const off = session.onEvent((method, params, sessionId) => { ... })

// O esperar un único evento que coincida con un predicado opcional y tiempo de espera
await session.Network.enable()
const ev = await session.waitFor(
  'Page.frameNavigated',
  (p) => p.frame.url.includes('example.com'),
  10_000
)

Persistir el estado entre llamadas

Cada fragmento se ejecuta dentro de su propio envoltorio asíncrono, por lo que sus declaraciones let/const desaparecen cuando retorna. Para transportar datos hacia adelante, asigna a globalThis:

browser-harness-js '(await listPageTargets()).forEach((t,i)=>globalThis["tab"+i]=t.targetId)'
browser-harness-js 'await session.use(globalThis.tab0)'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'

La propia session, el sessionId activo y los suscriptores de eventos ya son preservados por el servidor — las globales solo son necesarias para datos ad-hoc.

Conectarse a un Chrome en ejecución (flujo chrome://inspect)

Al adjuntarse al navegador ya en ejecución del usuario:

  1. Prueba await session.connect() primero (sin argumentos) — la detección automática maneja todos los navegadores basados en Chromium mediante DevToolsActivePort. Si retorna, ya está.
  2. Si la detección automática falla con No running browser with remote debugging detected, el usuario debe activarlo. Abre la página de inspección:
    # macOS — prefiere AppleScript sobre `open -a` (reutiliza el perfil actual, evita el selector de perfiles)
    osascript -e 'open location "chrome://inspect/#remote-debugging"'
    
    # Linux
    google-chrome 'chrome://inspect/#remote-debugging'     # o: chromium, google-chrome-stable
    
    # Windows (PowerShell)
    Start-Process chrome 'chrome://inspect/#remote-debugging'
    
    Solo la ruta de AppleScript en macOS evita el selector de perfiles; Linux/Windows pueden solicitar al usuario que elija un perfil primero.
  3. Marca "Descubrir destinos de red" en chrome://inspect, luego haz clic en Permitir cuando Chrome lo solicite.
  4. Si la detección automática elige el navegador incorrecto (varios en ejecución, quieres uno específico): enuméralos con await detectBrowsers(), luego await session.connect({ profileDir: <el que quieres> }).
  5. Si session.connect() retorna No detected browser accepted a connection, el usuario tiene la depuración remota activada pero aún no ha hecho clic en Permitir. Indícale que haga clic y reintente, o pasa timeoutMs: 30000 para esperar el clic.

Trabajar con targets (pestañas)

  • Filtrar internos de Chrome. listPageTargets() ya elimina las URL chrome:// y devtools://. Si llamas a Target.getTargets() directamente, filtra manualmente.
  • El orden de los targets CDP ≠ el orden visible de la barra de pestañas. Cuando el usuario dice "la primera pestaña que veo", usa una captura de pantalla o el título de la página para identificarla — Target.activateTarget solo cambia a un targetId conocido.

Buscar un método

La superficie tipada completa está en <directorio-habilidad>/sdk/generated.ts (~655 KB, solo se carga si la lees). Cada método tiene su descripción CDP como comentario JSDoc más las interfaces *Params / *Return tipadas en los espacios de nombres por dominio.

grep -n "navigate" <directorio-habilidad>/sdk/generated.ts | head

Regenerar el SDK

Cuando cambien los JSON del protocolo upstream, reemplaza sdk/browser_protocol.json y/o sdk/js_protocol.json y vuelve a ejecutar:

cd <directorio-habilidad>/sdk && bun gen.ts
browser-harness-js --restart   # recoge los nuevos bindings

Archivos

Todas las rutas son relativas a <directorio-habilidad> (la ruta de instalación — ver el inicio de este documento).

  • /usr/local/bin/browser-harness-js → <directorio-habilidad>/sdk/browser-harness-js (la CLI)
  • sdk/repl.ts — Servidor HTTP (Bun.serve en 127.0.0.1:9876)
  • sdk/session.ts — Clase Session (transporte, conexión, enrutamiento de targets, eventos)
  • sdk/generated.ts — Salida de generación de código: cada método CDP como un envoltorio tipado
  • sdk/gen.ts — Script de generación de código
  • sdk/{browser,js}_protocol.json — Protocolo upstream (vendido)

Skills relacionados