Guide · Updated October 24, 2026

How to give a Playwright, Stagehand or Browser Use agent a login without exposing the password

Keep the password in a vault that types it into the page, so your code and the model only ask for a login by site name. With runnerID by Torchrunner, you save the login and authenticator key once in the owner console. Your Playwright, Stagehand or Browser Use code calls a helper, and the password is typed into the page. The model never receives it.

What is the safe pattern?

Something outside the model fills it

Never put the password in a prompt, a task string or a tool result. Something outside the model fills it.

  • ›If runnerID can reach the browser (a hosted browser or one on your own server): Agents that drive a browser runnerID can reach, such as a hosted browser (Browserbase, Steel) or one on your own server, get the strongest mode: runnerID types the password itself.
  • ›If the browser is on your own laptop: When the browser runs on your own laptop, the helper uses its local route: the password passes through your own code once to be typed, and still never reaches the model.
  • ›If a hosted agent's browser is out of runnerID's reach: Hosted agents that connect over MCP get authenticator and emailed codes returned to them, and approvals and the log work the same. Typing a password into the page needs the runnerID helper next to the browser (Stagehand, Playwright or Browser Use), on a laptop or in a cloud container.
What your code sees

When runnerID types it, your code only learns which fields were filled. The password is not returned and not logged, and the model never receives it.

How do you set it up once?

Five steps
  • ›1. Start free at https://api.runnerid.torchrunner.ai/signup (1 identity, 100 logins and codes a month, no card), or pick a plan at https://runnerid.torchrunner.ai: Solo $12 a month for 1 identity, Builder $29 for 5, Team $99 for 25.
  • ›2. Open the emailed sign-up link and create the account. It shows your API key (rid_...) once, so copy it somewhere safe.
  • ›3. In the owner console at https://api.runnerid.torchrunner.ai/console, add the site's login and its authenticator key to an identity, for the agent's own seat where the site offers one. Neither is shown back.
  • ›4. Set the identity's site rules: which sites it may use, and which actions need your approval.
  • ›5. For sites that email a code, use the identity's own inbox address as the email on the agent's own seat.

How does it look in Stagehand?

Install

Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.

npm install @runnerid/stagehand
Log in and fill the code

login(site) fills the saved password. code(site) fills the authenticator code. emailCode(site) waits for an inbox code and types it. save(site) and restore(site) store the browser session. approve(action, details) emails the account owner.

import { Stagehand } from '@browserbasehq/stagehand';
import { runnerid } from '@runnerid/stagehand';

const stagehand = new Stagehand({ env: 'LOCAL' });
await stagehand.init();

const rid = runnerid(stagehand, {
  apiKey: process.env.RUNNERID_API_KEY,
  identityId: 'IDENTITY_ID',
  baseUrl: process.env.RUNNERID_BASE_URL,
});

await rid.login('example.com');
await rid.code('example.com');
Where the browser is

The helper takes the browser's CDP address from stagehand.connectURL() after init(). When that address is on your own laptop (loopback or a private range), the hosted service refuses it, and the helper uses its local route: it asks for a one-time token, redeems it, and types into the active Stagehand page. Right before typing it reads the page URL again, and if the page moved to another site it types nothing.

How does it look in Playwright?

Install

Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.

npm install @runnerid/playwright
Log in and fill the code

Pass either a Browser or a BrowserContext. The methods match @runnerid/stagehand: login(site) fills the saved password. code(site) fills the authenticator code. emailCode(site) waits for an inbox code and types it. save(site) and restore(site) store the browser session. approve(action, details) emails the account owner.

import { chromium } from 'playwright-core';
import { runnerid } from '@runnerid/playwright';

const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const rid = runnerid(browser, {
  apiKey: process.env.RUNNERID_API_KEY,
  identityId: 'IDENTITY_ID',
  baseUrl: process.env.RUNNERID_BASE_URL,
});

await rid.login('example.com');
await rid.code('example.com');
Where the browser is

Launch Chrome with remote debugging and --enable-automation, then connect with chromium.connectOverCDP. The helper finds the browser's CDP address and runnerID types into the page; the secret does not come back in the response.

On a laptop, the helper types the password locally when the server can't reach your browser. localOnly: true makes it skip the server attempt.

How does it look in Browser Use?

Install

Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.

pip install runnerid
Log in inside a Browser Use agent

Browser Use is driving Chrome at a CDP address. Pass that same address to Runner, then call login before or during the task.

import asyncio
import os
from browser_use import Agent, Browser
from playwright.async_api import async_playwright
from runnerid import Runner

CDP = "http://127.0.0.1:9222"

async def main() -> None:
    browser = Browser(cdp_url=CDP)
    # Playwright page for the same Chrome. The local route types into it.
    playwright = await async_playwright().start()
    chrome = await playwright.chromium.connect_over_cdp(CDP)
    page = chrome.contexts[0].pages[0]
    rid = Runner(
        api_key=os.environ["RUNNERID_API_KEY"],
        identity_id="IDENTITY_ID",
        base_url=os.environ["RUNNERID_BASE_URL"],
        cdp_url=CDP,
        page=page,
    )
    await rid.login("example.com")
    agent = Agent(
        task="Finish signing in on example.com. The password is already typed.",
        browser=browser,
    )
    await agent.run()

asyncio.run(main())
Methods and the laptop route

The calls match the Stagehand helper: login, code, email_code, restore, save and approve. A public CDP address is sent to runnerID, and the server types the secret into the page; the return value does not include the secret.

When cdp_url is a laptop address (loopback or private), the hosted service cannot connect, so pass the Playwright page Browser Use exposes as page. The helper then uses the local route and types with page.fill, after checking the page's host again just before typing. save and restore still need a CDP address the server can reach.

When runnerID can't reach the browser

The local MCP server

For an agent whose browser runnerID can't reach, such as a hosted agent's own cloud machine, run the local MCP server on that machine. It needs Node 20 or later and a Chrome or Chromium started with a debugging port.

npx -y @runnerid/mcp-local
Set it up in the agent

In the agent's MCP settings, add a command-type server: command npx, arguments -y @runnerid/mcp-local. Put RUNNERID_API_KEY in the server's environment, never in chat. Set RUNNERID_IDENTITY_ID if the account has more than one identity, and RUNNERID_CDP_URL on a machine with more than one browser.

What it does

It types the saved password, the authenticator code or an emailed code into the open tab over local CDP. The password goes from runnerID to this process to the page. It is never in a tool result, so the model never sees it. Each fill checks the tab's host right before typing and types nothing if the page moved to another site.

  • ›browser_status: shows which debugging address answered and the hosts of open tabs. Run it first.
  • ›fill_login: types the saved username and password for a site into the open tab on that host.
  • ›fill_code: types the current authenticator code.
  • ›fill_email_code: waits up to 2 minutes for an emailed code and types it.
Everything else

Use the hosted connector at https://api.runnerid.torchrunner.ai/mcp for saving logins, approvals, the log and codes. See /guides/mcp-connector-claude-cursor.

Codes, sessions and approvals

Emailed codes and sign-in links

Each identity has its own inbox address. Call emailCode(site), or email_code in Python, and the code that arrives is typed into the page. Over MCP, fill_email_code does the same and open_magic_link opens a sign-in link only when its host matches the site.

Stay signed in between runs

After the first login, call save(site). At the start of the next run, call restore(site), and the agent starts signed in. They use the same CDP address as fill: http, https, ws or wss, as long as runnerID can reach it, so they do not run on the laptop route. Save sessions for the agent's own seat where the site offers one.

Keep a person in the loop

Before a risky step, call approve(action, details). The owner gets an email link to approve or deny, and can also decide in the console. Every request and decision goes into the append-only log.

What if your agent uses MCP instead?

The hosted connector

Add runnerID as an MCP connector at https://api.runnerid.torchrunner.ai/mcp. In Claude you sign in on runnerID's own page; Cursor and other clients use your API key as a Bearer token. Hosted agents that connect over MCP get authenticator and emailed codes returned to them, and approvals and the log work the same. Typing a password into the page needs the runnerID helper next to the browser (Stagehand, Playwright or Browser Use), on a laptop or in a cloud container.

FAQ

Questions

How do I handle 2FA in a Stagehand agent?

Install the helper with npm install @runnerid/stagehand. Save the site's login and authenticator key once in the runnerID owner console, for the agent's own seat where the site offers one; neither is shown back. Your Stagehand code calls rid.login(site) and rid.code(site), and the password and the current code are typed into the page. The model never receives either value. For emailed codes, use the identity's own inbox address and rid.emailCode(site).

How do I automate 2FA logins in Playwright for an AI agent?

Install the helper with npm install @runnerid/playwright. Store the login and the authenticator (TOTP) key in runnerID, for the agent's own seat where the site offers one, then call rid.login(site) and rid.code(site) when your script reaches the login page. The password and the current code are typed into the page, so your prompts and the model never hold them. Save the browser session after the first login with rid.save(site), so later runs start signed in.

Is there a Browser Use helper?

Yes. Install it with pip install runnerid, pass the CDP address Browser Use drives (and its Playwright page) to Runner, and call login(site) before or during the task.

Does the helper return the password to my code?

No, when runnerID types it: your code learns which fields were filled, never the value. On the laptop route the secret passes through your own code once to be typed, and it is not returned or logged.

Related guides

Authenticator (TOTP) codes for AI agents

Keep the seed out of your code; fill_code and get_authenticator_code.

Read →
Keep an AI agent signed in between runs

Saved sessions in Playwright, Browser Use, Browserbase, Kernel, Steel and runnerID.

Read →
Log a cloud browser agent into sites without prompts

Steel, Kernel, Browserbase with 1Password, and runnerID over a CDP address.

Read →
Add runnerID to Claude and Cursor as an MCP connector

Claude sign-in, Cursor mcp.json, and the tools.

Read →
Sources

Sources, checked October 24, 2026

Last updated October 24, 2026
About the names

Playwright, Stagehand, Browser Use, Browserbase and Steel belong to their owners. Chrome and Chromium are trademarks of Google. Claude is a trademark of Anthropic. Cursor is a trademark of Anysphere. npm is a trademark of npm, Inc. runnerID and Torchrunner are not affiliated with, endorsed by or sponsored by any of them.

Give your agent its own identity

Start free with one identity and 100 logins and codes a month, no card. Paid plans start at $12 a month.