diff --git a/README.md b/README.md
index 49aa9bd..89f778f 100644
--- a/README.md
+++ b/README.md
@@ -1,137 +1,243 @@
-# dev-browser
+
+
+
-Browser automation CLI for coding agents: Puppeteer scripts, named pages, snapshot refs, one warm daemon.
+Brought to you by [Do Browser](https://dobrowser.io).
-An agent (Claude Code, Codex, ...) pipes a short JavaScript snippet into `dev-browser`; the snippet runs against a real Chrome
-that stays open between calls. Pages have names (`browser.getPage("checkout")`) so each step resumes where the last one
-stopped. `page.snapshot()` returns an ARIA tree with refs (`e12`), and `page.click("ref/e12")` acts on them. Typical
-warm call: about 15 ms.
+`dev-browser` lets coding agents control Chrome with short JavaScript scripts. The browser stays open between calls,
+so an agent can navigate once, inspect the page, act, and verify the result without starting over each time.
+
+**Key features:**
+
+- **Persistent pages.** Named tabs carry state across scripts.
+- **Compact snapshots.** Accessibility trees give agents readable output and stable element refs.
+- **Real Puppeteer.** Scripts use the Puppeteer Page API plus a small set of agent-focused helpers.
+- **Launch or attach.** Start an isolated Chrome profile or connect to a browser you already have open.
+
+## Demo
+
+https://github.com/user-attachments/assets/c6cf7fb9-b1dc-46ed-93b9-6e7240990c53
+
+## CLI installation
```bash
-dev-browser <<'EOF'
+npm install -g dev-browser
+dev-browser install # only needed if dev-browser cannot find Chrome
+```
+
+The release binary includes Bun and Puppeteer. Node is not required after installation. macOS and glibc Linux are
+supported; Windows and musl Linux are not yet supported.
+
+Snap-packaged Chromium cannot read the default `~/.dev-browser/v1` directory. On Ubuntu, use `dev-browser install`
+or point `DEV_BROWSER_CHROME` at another Chrome binary.
+
+If your package manager blocks lifecycle scripts, run `npm rebuild -g dev-browser`, `pnpm approve-builds -g
+dev-browser`, or `bun pm -g trust dev-browser`. The CLI will also try to download its binary on first use. For a
+private mirror, set `DEV_BROWSER_DOWNLOAD_BASE`; set `DEV_BROWSER_SKIP_DOWNLOAD=1` if you install the binary yourself.
+
+### Quick start
+
+```bash
+# Launch a headless browser and run a script
+dev-browser --headless <<'EOF'
const page = await browser.getPage("main");
await page.goto("https://example.com");
-await page.snapshot({ interactive: true })
+console.log(await page.title());
EOF
-# - heading "Example Domain" [level=1] [ref=e3]
-# - link "Learn more" [ref=e6]:
-# - /url: https://iana.org/domains/example
-dev-browser -e 'const p = await browser.getPage("main"); await p.click("ref/e6"); await p.waitForLoad(); p.url()'
+# Attach to Chrome started with `dev-browser chrome`
+dev-browser chrome
+dev-browser --connect <<'EOF'
+console.log(await browser.listPages());
+EOF
```
-## Install
+Chrome 136 and newer ignore remote-debugging flags on the default profile. `dev-browser chrome` handles this by using
+a dedicated profile and checking that Chrome actually started.
+
+### Using it with coding agents
+
+Tell the agent to run `dev-browser --help`. The built-in guide covers the current API and the preferred
+look → act → verify workflow.
+
+Agents that discover local skills can install the bundled skill explicitly:
```bash
-npm install -g dev-browser # or download a binary from GitHub Releases
-dev-browser install # only if the first run says "No Chrome found" (downloads Chrome for Testing, ~150 MB)
-dev-browser install-skill # optional: SKILL.md for Claude Code / Codex / ~/.agents
+dev-browser install-skill --codex # ~/.codex/skills/dev-browser/SKILL.md
+dev-browser install-skill --claude # ~/.claude/skills/dev-browser/SKILL.md
+dev-browser install-skill --agents # ~/.agents/skills/dev-browser/SKILL.md
```
-macOS and Linux. Windows is not yet supported. Requires nothing else at runtime: the binary embeds Bun, Puppeteer and
-the zip extractor used by `dev-browser install` (no `unzip` needed); the npm package has no runtime dependencies, its
-postinstall only downloads the binary for your platform. Snap-packaged Chromium on Ubuntu cannot read `~/.dev-browser/v1`;
-use `dev-browser install` or `DEV_BROWSER_CHROME` there.
+Run `dev-browser install-skill` without flags to update all three locations.
-If your package manager blocks install scripts (`bun add -g dev-browser`, pnpm before `pnpm approve-builds`, `npm --ignore-scripts`),
-the first `dev-browser` run downloads the binary itself (`dev-browser: downloading binary v...`). To fetch it eagerly instead:
-`bun pm -g trust dev-browser`, `pnpm approve-builds -g dev-browser`, or `npm rebuild -g dev-browser`. Behind a mirror set
-`DEV_BROWSER_DOWNLOAD_BASE=https://mirror/path/v1.0.0` (must serve `dev-browser--` and `SHA256SUMS`); with
-`DEV_BROWSER_SKIP_DOWNLOAD=1` nothing is downloaded and you place the binary at `/bin/dev-browser-bin` yourself.
+### Idle browser cleanup
-## Quick start
+Launched browsers close after 30 minutes without a script by default. Change that per command, in the environment, or
+in `~/.dev-browser/v1/config.json`:
```bash
-dev-browser --help # the full agent-facing guide (also docs/help.md)
-dev-browser help workflow # one topic
-dev-browser pages # open tabs and their names
-dev-browser --headless -e 'const p = await browser.getPage("x"); await p.goto("https://news.ycombinator.com"); await p.title()'
-dev-browser --connect # attach to a Chrome started with `dev-browser chrome` (real profile, Google sign-in works)
-dev-browser stop # close browsers and the daemon
+dev-browser --idle-timeout 5m < script.js
+DEV_BROWSER_IDLE_TIMEOUT=1h dev-browser -e 'await browser.listPages()'
```
+```json
+{
+ "idleTimeout": "5m"
+}
+```
+
+Durations accept `30s`, `5m`, `1h`, or raw milliseconds. Set the value to `0` to keep a launched browser open until
+`dev-browser stop`. Attached browsers are never closed by idle cleanup. Profiles, cookies, and login state remain on
+disk when a launched browser closes.
+
+
+Allowing dev-browser in Claude Code without permission prompts
+
+Add `dev-browser` to the `allow` list in `.claude/settings.json` for one project or `~/.claude/settings.json` for every
+project:
+
+```json
+{
+ "permissions": {
+ "allow": ["Bash(dev-browser *)"]
+ }
+}
+```
+
+This allows any matching command without another prompt. Only do this where you trust the scripts being run:
+`node:vm` gives each script fresh globals, but it is not a security sandbox.
+
+
+
+
+Legacy Claude Code plugin installation
+
+```text
+/plugin marketplace add sawyerhood/dev-browser
+/plugin install dev-browser@sawyerhood/dev-browser
+```
+
+Restart Claude Code after installation.
+
+
+
+## Script API
+
+Scripts get these globals:
+
+```javascript
+// Browser control
+browser.getPage(nameOrId) // Get/create a named page, or attach by target ID
+browser.newPage() // Create an anonymous page; close it yourself
+browser.listPages() // [{ id, url, title, name }]
+browser.closePage(name) // Close a named page
+
+// File I/O, restricted to ~/.dev-browser/v1/tmp
+saveFile(name, data)
+readFile(name)
+
+// Output
+console.log()
+console.warn()
+console.error()
+```
+
+Top-level `await` works, and the last expression becomes the command result. Pages are real [Puppeteer Page
+objects](https://pptr.dev/api/puppeteer.page) with a few additions:
+
+```javascript
+await page.snapshot({ interactive: true }) // Accessibility tree with refs such as e12
+await page.click("ref/e12") // Refs work in Puppeteer selector methods
+await page.ref("e12") // ElementHandle for a ref
+await page.shot() // JPEG path and CSS-pixel dimensions
+await page.waitForLoad() // Wait for navigation, requests, and DOM activity to settle
+await page.fill("#email", "me@example.com")
+```
+
+Each command runs in a fresh `node:vm` context inside the daemon. This keeps script globals separate, but it is not a
+security boundary. When a command finishes or times out, its page, locator, browser-context, and registry operations
+are closed so detached work cannot interfere with the next command.
+
+Scripts may run concurrently. Browser and page creation are serialized, as are input operations on different tabs
+through a bring-to-front lock. Two scripts using the same named page can still interleave.
+
+See [`dev-browser --help`](docs/help.md) for the full API, error behavior, configuration, JSON output, MCP tools, and
+examples.
+
+## Connecting to an existing browser
+
+`--connect` accepts auto-discovery, a port, an HTTP URL, a WebSocket URL, or a raw CDP Unix socket:
+
+```bash
+dev-browser chrome --profile work
+dev-browser --connect -e 'await browser.listPages()'
+dev-browser --connect 9222 -e 'await browser.listPages()'
+dev-browser --connect 'wss://provider.example?token=…' -e 'await browser.listPages()'
+```
+
+Attached browsers belong to the user. dev-browser extends only the tabs a script asks for and never closes the browser
+because of an idle timeout. Credentials are redacted from logs and status output, while differently authenticated
+endpoints remain separate sessions.
+
+## MCP
+
+```bash
+claude mcp add dev-browser -- dev-browser mcp --headless
+```
+
+The server exposes `dev_browser_run`, `dev_browser_pages`, `dev_browser_browsers`, `dev_browser_stop`, and
+`dev_browser_help` over the same warm daemon as the CLI.
+
## Upgrading to 1.0
-Version 1.0 replaces the 0.2 Playwright/QuickJS implementation with the faster Puppeteer/Bun implementation developed
-as doobie. State lives under `~/.dev-browser/v1`, so it cannot collide with a running 0.2 daemon or its incompatible
-profiles. Existing 0.2 state remains untouched.
+Version 1.0 replaces the Playwright/QuickJS implementation from dev-browser 0.2 with the Puppeteer/Bun implementation
+developed as doobie. Its state lives under `~/.dev-browser/v1`, separate from both older installations.
-To copy durable state from doobie, stop it first and run:
+To copy durable doobie state:
```bash
doobie stop
dev-browser migrate-from-doobie
```
-The migration copies state into an empty v1 directory and leaves `~/.doobie` unchanged. Existing scripts must move
-from Playwright helpers such as `snapshotForAI()` and `getByRef()` to `snapshot()` and `ref/eN`; the runtime is a
-`node:vm` isolation context, not a security sandbox. Windows and musl Linux binaries are not available in 1.0.
+`migrate-from-doobie` leaves `~/.doobie` untouched. Scripts written for dev-browser 0.2 need to replace helpers such as
+`snapshotForAI()` and `getByRef()` with `snapshot()` and `ref/eN`. Computer-use helpers under `page.cua` and
+`page.domCua` are not part of 1.0.
-Scripts get `browser` (`getPage`, `newPage`, `listPages`, `closePage`), `console`, `saveFile`/`readFile` (jailed to
-`~/.dev-browser/v1/tmp`) and the full Puppeteer `Page` API plus `page.snapshot`, `page.ref`, `page.shot`, `page.waitForLoad`,
-`page.fill`. Top-level `await` works and the last expression is the return value. Everything else is in
-[docs/help.md](docs/help.md).
+## Benchmarks
-## MCP
+Measured on Linux with a headless browser and warm daemon, medians of 9 runs:
-`dev-browser mcp --headless` is a stdio MCP server exposing `dev_browser_run`, `dev_browser_pages`, `dev_browser_browsers`, `dev_browser_stop` and `dev_browser_help` over the same daemon (`claude mcp add dev-browser -- dev-browser mcp --headless`).
+| Scenario | Time |
+| --- | ---: |
+| Empty script | ~13 ms |
+| `getPage("x")` + `page.title()` | ~14 ms |
+| `page.snapshot()` on a SERP-like page | ~18 ms |
+| `page.shot()` | ~36 ms |
+| Cold daemon and Chrome launch | ~280 ms |
-## How it works
-
-- **Thin client, warm daemon.** The `dev-browser` binary connects to `~/.dev-browser/v1/daemon.sock`, sends one request, streams the
- output, exits. If no daemon is running it starts one. The daemon keeps Chrome, Puppeteer, and page handles warm and
- exits 15 minutes after its last browser closes.
-- **Puppeteer + `node:vm`.** Each script is wrapped in an async function and run in a fresh `vm` context inside the
- daemon with real Puppeteer objects. This gives clean globals and fast startup; it is not a security boundary.
-- **Named pages.** `getPage(name)` maps a name to a Chrome tab (`~/.dev-browser/v1/pages/`), so state survives across scripts
- and even daemon restarts. Names are scoped to one browser key (`default`, `default:headless`, `work`, ...). Launched
- browsers use persistent profiles under `~/.dev-browser/v1/browsers//profile` (headless: `profile-headless`); headed
- and headless are separate Chromes with separate profiles.
-- **Snapshot refs.** An in-page script renders an ARIA YAML tree and assigns stable refs to every visible element that
- receives pointer events (`interactive: true` prunes to controls plus headings/landmarks); `ref/e12` is a registered
- Puppeteer query handler, so it works in every selector API.
-- **One deadline.** `--timeout` (default 30 s) bounds connect + script + teardown; Puppeteer waits default to 5 s
- (navigation 15 s). When a script finishes normally, reaches its deadline, or disconnects, its page, browser-context,
- locator and browser-registry operations reject further work so detached continuations cannot affect later runs.
- Exit codes: 0 ok, 1 error, 2 usage, 124 deadline.
-- **Attach, not just launch.** `--connect` attaches to any Chrome over CDP (auto-discovery, port, http, ws, or a raw
- CDP unix socket) and only touches the tabs a script asks for (the user's other tabs keep their dialogs and scripts);
- `dev-browser chrome` starts your installed Chrome with remote debugging on a dedicated profile and verifies it came up.
- Credentials and query parameters are redacted from status and logs, while distinct authenticated endpoints remain
- isolated as separate browser sessions.
-- **Concurrent scripts.** Scripts run in parallel; only launch/connect, page creation and input on different tabs of one
- browser (a bring-to-front lock, since background tabs do not process input) are serialized. Two scripts on the same
- named page interleave. Per-script state (default timeouts, request interception) is reset when the script ends.
-
-## Performance
-
-Measured on Linux, headless, warm daemon, medians of 9 runs (`bun run build && bun run bench/run.ts --runs 9`):
-
-| scenario | time |
-| ------------------------------------- | -------- |
-| `dev-browser -e '1+1'` (warm) | ~13 ms |
-| `getPage("x")` + `page.title()` (warm)| ~14 ms |
-| `page.snapshot()` (SERP-like page, in-script) | ~18 ms |
-| `page.shot()` (viewport, in-script) | ~36 ms |
-| cold start (spawn daemon + launch headless Chrome) | ~280 ms |
+Run them with `bun run build && bun run bench/run.ts --runs 9`. The older end-to-end comparison is in
+[dev-browser-eval](https://github.com/SawyerHood/dev-browser-eval).
## Development
```bash
bun install
-bun run dev -- -e '1+1' # run from source
-bun test # real headless Chrome tests
-bun run build # build/daemon.js + dist/dev-browser (single binary)
+bun run dev -- -e '1+1'
+bun test
+bun run build
```
-Design decisions and rationale: [docs/design-decisions.md](docs/design-decisions.md). Agent-facing reference:
-[docs/help.md](docs/help.md) (embedded verbatim in `dev-browser --help`).
+Design notes live in [docs/design-decisions.md](docs/design-decisions.md).
## Releasing
-See [RELEASING.md](RELEASING.md) for release-candidate testing, the historical `v1.0.0` tag cleanup, trusted npm
-publishing, and post-release verification.
+See [RELEASING.md](RELEASING.md) for release-candidate testing, npm trusted publishing, and post-release verification.
## License
MIT
+
+## Author
+
+[Sawyer Hood](https://github.com/sawyerhood)