What is the safe pattern?
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.
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?
- ›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?
Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.
npm install @runnerid/stagehandlogin(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');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?
Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.
npm install @runnerid/playwrightPass 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');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?
Set RUNNERID_BASE_URL to the API address, https://api.runnerid.torchrunner.ai.
pip install runneridBrowser 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())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
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-localIn 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.
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.
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
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.
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.
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?
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.
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
Keep the seed out of your code; fill_code and get_authenticator_code.
Read →Saved sessions in Playwright, Browser Use, Browserbase, Kernel, Steel and runnerID.
Read →Steel, Kernel, Browserbase with 1Password, and runnerID over a CDP address.
Read →Claude sign-in, Cursor mcp.json, and the tools.
Read →Sources, checked October 24, 2026
- ›1. @runnerid/stagehand on npm: https://www.npmjs.com/package/@runnerid/stagehand
- ›2. @runnerid/playwright on npm: https://www.npmjs.com/package/@runnerid/playwright
- ›3. @runnerid/mcp-local on npm: https://www.npmjs.com/package/@runnerid/mcp-local
- ›4. runnerID API docs: https://api.runnerid.torchrunner.ai/llms.txt
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.