Browser Use logo

Browser Use

OrganizationPopular
letta-ai
browser-use

Control a real browser to navigate pages, click, type, fill forms, inspect rendered UI, take screenshots, or record video. Load only when the user asks to open or automate a browser, interact with or test rendered page UI, scrape a site that needs browser execution, or capture a browser screenshot or video. Do not load for backend logs, traces, API or stream events, source-code inspection, or plain HTTP or web research that does not require a browser.

Overview

Publisherletta-ai
Repositoryletta-code
Skill namebrowser-use
Stars
3.4K
Forks
411
Bundled files
1
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by letta-ai on GitHub. Read the source before you install it.

Installation

Install the Browser Use AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/letta-ai/letta-code.git /tmp/letta-code
mkdir -p .claude/skills
cp -r /tmp/letta-code/src/skills/builtin/browser-use .claude/skills/browser-use
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Browser Use in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Browser Use on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Browser Use is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Browser Use with CDP

Drive the browser through its native Chrome DevTools Protocol over the remote-debugging WebSocket. This works with zero dependencies: launch the browser with --remote-debugging-port, then talk JSON over fetch and the built-in WebSocket global (available in Bun and Node ≥ 22 — no ws package).

If the project already has Playwright or Puppeteer installed, using it is usually simpler — reach for raw CDP when no automation library is available, when protocol-level control is needed, or when recording a deterministic visual demo.

Protocol reference: https://chromedevtools.github.io/devtools-protocol/. The running browser's exact schema is at http://127.0.0.1:<port>/json/protocol; tip-of-tree docs can differ from the installed version.

Visible by default when a display exists

When the computer has a display, prefer a visible (headful) browser for any task the user might watch or take over: clicking or typing, forms, sign-in, checkout/payment, CAPTCHAs or bot protection, and user handoff. Most browser tasks exist because plain HTTP is not enough; a headless browser is more likely to trigger bot protection and gives the user no way to observe or step in. Visible does not mean pixel-driven: keep operating the page over CDP, and the user sees every action in the window.

Use headless mode only for work the user explicitly wants in the background and that cannot require interaction or handoff, such as read-only scraping, CI, or screenshot/PDF generation, or when no display exists. A headless page does not satisfy a request to open or reopen a site in a browser the user can see.

When the user asks to review, watch, or take over, leave that browser window open after the task. Do not kill or close it before replying.

Workflow

  1. Find a Chromium-based browser (below). If none exists, see "No Chrome installed".
  2. Launch with a dedicated profile and remote debugging. Never attach to the user's normal profile unless explicitly asked.
  3. Discover targets via /json/list; pick the "page" target by URL or title.
  4. Connect to its webSocketDebuggerUrl and enable only the domains you need (usually Page, Runtime, DOM, Input; add Network, Log when debugging).
  5. Inspect before acting: find elements by accessible name, label, text, role, stable ID, or placeholder — not generated classes or child indexes.
  6. Act through Input.* for user-like interactions; use Runtime.evaluate for inspection, coordinate math, and setup with no meaningful user interaction.
  7. Wait on observable state, never fixed sleeps alone.
  8. Verify the result (DOM state, URL, screenshot, console/network events).
  9. Clean up temporary background work: stop screencasts and close the WebSocket. Kill a browser you launched only when the user did not ask to review, watch, or take over the visible window.

Finding the browser

Any Chromium-based browser supports CDP (Chrome, Chromium, Edge, Brave). Probe in order:

bash
# macOS
for c in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
         "/Applications/Chromium.app/Contents/MacOS/Chromium" \
         "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" \
         "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"; do
  [ -x "$c" ] && { echo "$c"; break; }
done

# Linux
for c in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge brave-browser; do
  command -v "$c" && break
done

On Windows, check %ProgramFiles%\Google\Chrome\Application\chrome.exe, %ProgramFiles(x86)%\..., %LocalAppData%\Google\Chrome\Application\chrome.exe, and the same patterns for Microsoft\Edge.

No Chrome installed

Any browser found by the probe above works identically — use it. If truly no Chromium-based browser exists, do not install or download one automatically. Tell the user that browser use requires Chrome or another Chromium-based browser and recommend either:

  1. Install Chrome on the current computer, then retry the browser task.
  2. Teleport the conversation back to its Cloud sandbox, where a browser is already installed.

Wait for the user to choose. Do not silently replace the browser task with plain HTTP or claim browser automation succeeded.

Launching

Use a disposable profile and a fixed port. Chrome refuses to run as root without --no-sandbox, so add that flag when id -u is 0:

bash
chrome_args=( \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/cdp-profile \
  --window-size=1440,900 \
  --force-device-scale-factor=1 \
  --no-first-run \
  --no-default-browser-check \
)
[ "$(id -u)" -eq 0 ] && chrome_args+=(--no-sandbox)
"$CHROME" "${chrome_args[@]}" https://example.com

Add --headless=new only for explicitly invisible work or when no display exists (see "Visible by default" above). With --remote-debugging-port=0, read the chosen port from <user-data-dir>/DevToolsActivePort. Launch in the background and poll http://127.0.0.1:9222/json/version until it responds.

HTTP endpoints: /json/version (browser metadata + browser-level WebSocket URL), /json/list (targets), PUT /json/new?<url> (open tab), /json/activate/<id>, /json/close/<id>, /json/protocol (schema).

Attach to the page target for Page/DOM/Runtime/Input work; use the browser target only for browser-wide commands (target control, downloads, browser contexts).

Minimal CDP client

CDP messages are JSON with monotonically increasing request ids. Run with bun:

ts
const targets = await (await fetch("http://127.0.0.1:9222/json/list")).json();
const target = targets.find((t: any) => t.type === "page");
if (!target) throw new Error("No page target");

const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
  ws.onopen = resolve;
  ws.onerror = reject;
});

let nextId = 0;
const pending = new Map();
ws.onmessage = (event) => {
  const msg = JSON.parse(String(event.data));
  if (!msg.id) return handleEvent(msg); // Page.loadEventFired, Log.entryAdded, ...
  const p = pending.get(msg.id);
  if (!p) return;
  pending.delete(msg.id);
  msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result);
};
ws.onclose = () => {
  for (const p of pending.values()) p.reject(new Error("socket closed"));
  pending.clear();
};

function send(method: string, params: object = {}): Promise<any> {
  return new Promise((resolve, reject) => {
    const id = ++nextId;
    pending.set(id, { resolve, reject });
    ws.send(JSON.stringify({ id, method, params }));
  });
}

await send("Page.enable");
await send("Runtime.enable");

Navigations destroy execution contexts and can invalidate in-flight Runtime.evaluate calls; retry after observing the new document.

Inspecting the page

Start with a concise UI inventory:

ts
const result = await send("Runtime.evaluate", {
  expression: `JSON.stringify({
    buttons: [...document.querySelectorAll('button')].map((el) => ({
      text: el.innerText.trim(), aria: el.getAttribute('aria-label'), title: el.title
    })).filter((x) => x.text || x.aria || x.title),
    inputs: [...document.querySelectorAll('input, textarea, [contenteditable=true]')].map((el) => ({
      tag: el.tagName, type: el.type, placeholder: el.placeholder,
      aria: el.getAttribute('aria-label'), value: el.value
    }))
  })`,
  returnByValue: true,
});

Use awaitPromise: true for async expressions and userGesture: true when the page requires user activation. Treat exceptionDetails in the result as an error even though the CDP command itself succeeded.

document.querySelector does not cross shadow boundaries — traverse open shadow roots explicitly; for closed shadow roots or remote-object work use the DOM domain (DOM.getDocument, DOM.querySelector, DOM.getBoxModel).

Clicking and typing

Compute coordinates in CSS pixels immediately before acting, then send native input events:

ts
async function point(expr: string) {
  const r = await send("Runtime.evaluate", {
    expression: `(() => {
      const el = ${expr};
      if (!el) return null;
      el.scrollIntoView({ block: 'center', inline: 'center' });
      const b = el.getBoundingClientRect();
      return { x: b.left + b.width / 2, y: b.top + b.height / 2 };
    })()`,
    returnByValue: true,
  });
  if (!r.result.value) throw new Error(`Element not found: ${expr}`);
  return r.result.value;
}

async function click(expr: string) {
  const { x, y } = await point(expr);
  await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
  await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", buttons: 1, clickCount: 1 });
  await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", buttons: 0, clickCount: 1 });
}

Focus an editable element (click it), then insert text:

ts
await click(`document.querySelector('input[aria-label="Search"]')`);
await send("Input.insertText", { text: "search terms" });

Use Input.insertText for text and Unicode; use paired Input.dispatchKeyEvent (keyDown + keyUp with key, code, windowsVirtualKeyCode) for Enter, Escape, arrows, Tab, and shortcuts. Modifier bits: Alt=1, Ctrl=2, Meta=4, Shift=8.

Native <select> and framework-controlled inputs may need the prototype setter plus bubbling events:

ts
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
setter.call(input, "new value");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));

Prefer real Input.* events for the behavior being demonstrated or tested; direct DOM mutation is fine for deterministic setup and inspection.

Waiting reliably

A returned command does not mean the action completed. Poll the state that proves completion:

ts
async function waitFor(expr: string, timeoutMs = 30_000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const r = await send("Runtime.evaluate", { expression: `Boolean(${expr})`, returnByValue: true });
    if (r.result.value) return;
    await new Promise((res) => setTimeout(res, 250));
  }
  throw new Error(`Timed out waiting for ${expr}`);
}

await waitFor(`document.body.innerText.includes('Saved')`);

For navigation, wait on Page.loadEventFired or a lifecycle networkIdle event — but SPA route changes may emit neither, so prefer the UI condition that actually matters.

Screenshots, PDF, and video

ts
const shot = await send("Page.captureScreenshot", { format: "png" });
await Bun.write("screenshot.png", Buffer.from(shot.data, "base64"));

captureBeyondViewport: true for full-page; Page.getLayoutMetrics + clip for exact regions; Page.printToPDF for PDFs. For video recording with Page.startScreencast and demo-polish tips, read references/recording.md.

Debugging failures

Enable Network and Log, then watch Network.requestWillBeSent, Network.responseReceived, Network.loadingFailed, Runtime.consoleAPICalled, Runtime.exceptionThrown, and Log.entryAdded. Fetch bodies with Network.getResponseBody. Never log authorization headers, cookies, API keys, passwords, or response bodies containing secrets — scrub before returning output to context.

Common failure modes:

  • ECONNREFUSED on the port: browser exited, wrong port, or debugging not enabled — check /json/version first.
  • No matching target: inspect /json/list; match by URL/title, don't take the first page blindly.
  • Execution context destroyed: the page navigated; wait for the new document and re-evaluate.
  • Click misses an existing element: scroll into view and recalculate the box immediately before dispatching.
  • Typed text doesn't stick in a controlled input: focus + Input.insertText, or the prototype-setter pattern above.
  • Opening DevTools disconnects automation: embedded DevTools can detach other clients — don't open DevTools during a run.
  • Page commands fail on the browser endpoint: attach to the page target.

Safety

Browser automation acts with the user's browser authority.

  • Use a disposable profile by default.
  • Do not submit purchases, publish content, send messages, delete data, or accept consequential dialogs without explicit authorization.
  • Do not extract saved passwords, tokens, cookies, or unrelated browsing data.
  • Stay within the requested origin and workflow.
  • Keep the remote-debugging listener on loopback (127.0.0.1) unless the user explicitly needs remote access and has authentication in place.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Browser Use AI skill do?

Control a real browser to navigate pages, click, type, fill forms, inspect rendered UI, take screenshots, or record video. Load only when the user asks to open or automate a browser, interact with or test rendered page UI, scrape a site that needs browser execution, or capture a browser screenshot or video. Do not load for backend logs, traces, API or stream events, source-code inspection, or plain HTTP or web research that does not require a browser.

Why use Browser Use on TypingMind?

Because you install it once and use it with any model. Browser Use is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Browser Use in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/letta-ai/letta-code/tree/main/src/skills/builtin/browser-use. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Browser Use?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Browser Use?

As many as you like. As long as a model supports skills, you can use Browser Use with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Browser Use AI skill free?

Yes. It is published on GitHub by letta-ai under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇