# Session operations
The `targets`, `cookies`, `fetch`, `storage`, `eval`, `wait`, and
`wait-for-cookie` subcommands — and their MCP-tool equivalents
(`list_targets`, `cookies`, `storage_get`, `storage_set`, `wait_for_cookie`,
`fetch`) — are all thin wrappers over an engine-agnostic session module in
`src/session/`. This document describes the model that surface implements
and the guarantees you can rely on when integrating.
## The `PageSession` model
A `PageSession` is a handle to a single page in a running browser. It is
engine-agnostic: under the hood it is either a CDP target (Chrome, Edge,
Chromium, Brave) or a BiDi browsing context (Firefox), but the operations
exposed are the same.
```rust
let session = PageSession::attach(&endpoint, engine, target_url_regex).await?;
let value = session.evaluate("document.title", true).await?;
session.navigate("https://example.com/").await?;
let png_b64 = session.screenshot(true).await?;
session.close().await;
```
`target_url_regex` is the same selector used by `--target` on the CLI and
`--url` on `targets`: an unanchored Rust regex matched against each open page
target's URL. When `None`, the engine's default page is used (CDP: first
`type == "page"`; BiDi: top-level browsing context). When the regex matches
no targets, attach fails with a descriptive error.
## `TargetInfo` shape
`session::targets::list(endpoint, engine, url_regex)` returns a normalised
list of `TargetInfo`:
| `id` | string | CDP target id, or BiDi browsing-context id. |
| `url` | string | Live URL at probe time. |
| `title` | string | Document title (`""` when none). |
| `kind` | string | `"page"` for normal pages; engines may add more later. |
Both the CLI table (`KIND ID URL TITLE`) and the MCP `list_targets` tool
return this shape directly. Tools that take `--target URLREGEX` use the same
regex semantics as `targets --url`.
## `cookies --format netscape`
The `netscape` formatter emits the classic Mozilla `cookies.txt` format,
byte-compatible with what `curl --cookie`, `wget --load-cookies`, and
`yt-dlp --cookies` expect.
Layout:
```
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by browser-control. Edit at your own risk.
<domain>\t<include_subdomains>\t<path>\t<secure>\t<expires>\t<name>\t<value>
```
Column rules:
1. **`domain`** — exact cookie domain. If the cookie applies to subdomains
(i.e. CDP `domain` starts with `.` or `Network.getAllCookies` reports
`hostOnly == false`), a leading `.` is prepended; host-only cookies are
written without one.
2. **`include_subdomains`** — `TRUE` when the domain has the leading dot,
`FALSE` otherwise. Always uppercase.
3. **`path`** — verbatim cookie path (typically `/`).
4. **`secure`** — `TRUE` or `FALSE`, uppercase.
5. **`expires`** — Unix epoch seconds as an integer. `0` denotes a session
cookie (no expiry); writers that expect missing expiries should treat `0`
as "session". Negative expirations are clamped to `0`.
6. **`name`** — cookie name.
7. **`value`** — cookie value, verbatim. No quoting or escaping is applied;
the format does not support tabs or newlines in values and `browser-control`
does not produce them.
Fields are separated by single tab characters; lines end with `\n`. Files
written via `-o FILE` are `chmod 0600` on Unix.
## Profile semantics
When `browser-control start` is invoked without `--profile`, it uses a single
persisted profile per browser kind, located under the OS app-data dir:
- macOS: `~/Library/Application Support/browser-control/profiles/<kind>/default/`
- Linux: `~/.config/browser-control/profiles/<kind>/default/`
- Windows: `%APPDATA%\browser-control\profiles\<kind>\default\`
(`<kind>` is `chrome`, `edge`, `chromium`, `brave`, or `firefox`.
`BROWSER_CONTROL_CONFIG_DIR` overrides the root for tests.)
Consequences:
- The same browser state (cookies, logins, extensions) is reused across all
`browser-control` invocations, MCP agents, and shells on the machine.
- Restarting the browser, the user's shell, or the host does not lose state.
- Per-kind separation exists because Chrome and Firefox profile formats are
not interchangeable.
- Pass `--profile <absolute-path>` to opt out per-invocation.
Named profiles (`profiles/<kind>/<name>/`) are deferred to a future release;
the current layout leaves room for them with no migration.
## Writing your own integrations
If shelling out to the CLI is not a fit — say, you're embedding browser
control in a larger Rust tool — call into `src/session/` directly. The
public surface mirrors what the CLI uses:
- `session::PageSession::attach(endpoint, engine, target_url_regex)` —
attach to a page; returns a `PageSession`.
- `session::PageSession::evaluate(expression, await_promise)` — run JS,
returns `serde_json::Value`.
- `session::PageSession::navigate(url)` — drive navigation.
- `session::PageSession::screenshot(full_page)` — base64 PNG.
- `session::PageSession::close()` — release the attachment.
- `session::targets::list(endpoint, engine, url_regex)` — enumerate pages.
Higher-level operations (cookie export, storage get/set, fetch, wait loops)
currently live in the `cli::` modules and call `session::*` under the hood.
If you want to reuse them as a library, the cleanest path today is to shell
out to the `browser-control` binary; promoting them into `session::` is on
the roadmap. Engine dispatch is internal; callers pass an `Engine` and the
right protocol path is used.
The registry (`src/registry.rs`) and `BrowserRow` give you the `endpoint`
and `engine` for any browser the user has started; see
`cli::env_resolver::resolve` for the canonical resolution rules.