Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/l-xiaoshen/handstage/llms.txt

Use this file to discover all available pages before exploring further.

Every Handstage session starts with a V3 instance — the top-level handle that owns the CDP connection to a running Chrome process. From there, you work through one or more V3Context objects that manage pages, cookies, and storage. Understanding the relationship between these two layers lets you build multi-session automation flows without state leaking between them.

The V3 class

V3 is the entry point for connecting to a browser. It launches or attaches to a local Chrome instance over the Chrome DevTools Protocol (CDP) and exposes a default context through its context property.
import { V3 } from "@handstage/core"

const browser = await V3.connectLocal()

// Access the default context
const context = browser.context

// When you are done
await browser.close()
V3.connectLocal() accepts an optional HandstagesLocalOptions object that controls viewport size, proxy settings, locale, headless mode, and more.

The V3Context class

A V3Context owns the CDP session and is responsible for:
  • Creating and tracking top-level pages (browser tabs)
  • Holding cookies and local storage for a browser profile
  • Managing init scripts that run on every new document
  • Routing CDP target events to the correct page
Every context has a browserContextId — a string Chrome uses to route targets and storage commands to the right profile. You read it from context.browserContextId.

Default context vs isolated contexts

When you call V3.connectLocal(), Handstage resolves the browser’s default context and wraps it in a V3Context with isDefaultContext: true. All pages in the default context share the same cookie jar and storage. If you need isolated sessions — for example, to simulate two different logged-in users — create a dedicated context with browser.create(). Dedicated contexts behave like incognito profiles: they share the same CDP connection but have completely separate cookies, local storage, and session state.
const browser = await V3.connectLocal()

// Default context — shared cookie jar
const defaultCtx = browser.context
const page1 = await defaultCtx.newPage("https://example.com")

// Isolated context — its own cookie jar
const isolatedCtx = await browser.create()
const page2 = await isolatedCtx.newPage("https://example.com")

// page1 and page2 share no cookies or storage
await isolatedCtx.close()
await browser.close()
Init scripts and extra HTTP headers are not inherited by isolated contexts. Call context.addInitScript() and context.setExtraHTTPHeaders() on each context that needs them.

Creating multiple isolated contexts

You can create as many isolated contexts as you need. Each one calls Target.createBrowserContext on the CDP connection and returns a fully independent V3Context.
const browser = await V3.connectLocal()

// Spin up three isolated profiles
const [ctxA, ctxB, ctxC] = await Promise.all([
  browser.create(),
  browser.create(),
  browser.create(),
])

const [pageA, pageB, pageC] = await Promise.all([
  ctxA.newPage("https://app.example.com/login"),
  ctxB.newPage("https://app.example.com/login"),
  ctxC.newPage("https://app.example.com/login"),
])

// Each page logs in as a different user — no session bleed between them
// ...

await Promise.all([ctxA.close(), ctxB.close(), ctxC.close()])
await browser.close()
Every active V3Context registers its own root-level listeners on the shared CDP connection, so each Target.* event fans out to O(N) handlers. This is fine for a handful of contexts. If you need many dozens of parallel sessions, consider sharing one context across tasks or batching their lifetimes.

The disposeOnDetach option

When you create an isolated context, disposeOnDetach defaults to true. This instructs Chrome to automatically clean up the context and its storage if the CDP connection drops unexpectedly, which prevents orphaned browser profiles on crash.
const isolated = await browser.create({ disposeOnDetach: true })
You can set it to false if you want the context to persist across reconnections, but you are then responsible for calling isolated.close() to release the resources.

The keepAlive option

By default, closing the V3 instance kills the Chrome process it launched. Pass keepAlive: true in HandstagesLocalOptions to leave Chrome running after your script exits. This is useful in CI pipelines where a single browser instance is shared across multiple test runs.
const browser = await V3.connectLocal({ keepAlive: true })

// Chrome stays alive after browser.close()
await browser.close()
When keepAlive is true, Handstage does not start the shutdown supervisor process. If your script exits abnormally, Chrome and any temporary profile directories may remain on disk. You are responsible for cleanup in that case.

Shutdown supervisor

When keepAlive is false (the default), Handstage spawns a lightweight crash-only shutdown supervisor as a separate process. The supervisor monitors the main process PID and cleans up the Chrome process plus any temporary profile directories if the main process exits unexpectedly — for example, due to an unhandled exception or SIGKILL. On a normal browser.close() call, the supervisor is stopped first, then Chrome is killed gracefully, and temporary profile directories are removed.

Closing resources

1

Close isolated contexts

Call context.close() on each dedicated context you created. This disposes the browser context in Chrome and releases all per-page resources like network managers and console handlers.
2

Close the browser

Call browser.close() to close the default context, kill the Chrome process (unless keepAlive is set), and stop the shutdown supervisor.
If you forget to close a child context, browser.close() will attempt a best-effort close of all tracked children before tearing down the connection.

Build docs developers (and LLMs) love