Sessions & Auth
Login & Credentials
Login is the step automation gets stuck on most often. This page lays out three reliable paths:
drive the login form directly with
snapshot → fill → click → wait, tuck accounts into the
chrome-use auth credential vault for one-command replay, and handle
Sign in with Google / OAuth plus Bitwarden passwords and passkeys.
The goal is to have the agent complete login in a real browser without leaving passwords in your shell history.
The basic login flow
The most direct approach: open the login page, use snapshot -i to read the interactive
elements in the form, pick out the @refs for the email, password, and submit button,
fill them one by one, click, and finally use wait --url to
confirm you've landed on the post-login page.
$ chrome-use open https://app.example.com/login
$ chrome-use snapshot -i
# Pick the @ref for the email / password / submit button out of the snapshot, then:
$ chrome-use fill @e3 "user@example.com"
$ chrome-use fill @e4 "hunter2"
$ chrome-use click @e5
$ chrome-use wait --url "**/dashboard"
$ chrome-use snapshot -i
@ref is a handle to an element in the current snapshot — it can go stale the moment the
page changes. Get into the habit of
running snapshot -i to grab the current refs before acting; it's far more
stable than hardcoding selectors. The final snapshot -i is there to confirm login actually
succeeded (and you didn't stall on a login page showing an error).
Credential vault (auth save / login)
For sites you log into over and over, store the credential in the local vault once and then replay it with
a single command that auto-fills and submits.
--password-stdin reads the password from standard input, keeping it out of shell history.
# Save a credential (password read from stdin — press Ctrl+D when done)
$ chrome-use auth save my-app --url https://app.example.com/login \
--username user@example.com --password-stdin
# From then on, one command: open the login page, fill, click, wait for the form to finish
$ chrome-use auth login my-app
auth save tucks credentials into chrome-use's local vault; the bwu below instead
pulls them from Bitwarden. The two work together: keep the password in Bitwarden and fetch it just-in-time
at login, so the credential never lands on the command line.
Sign in with Google / OAuth
Click the site's own Google login button, then snapshot -i: even when the GSI
"Continue as <user>" button sits inside a cross-origin iframe, you'll still get a clickable
@ref (Chrome 125+). If a account-picker popup opens instead, just switch to that popup and
drive it — the relay follows the popup your click opened.
# Kick off the OAuth flow
$ chrome-use open https://app.example.com/auth/google
# Wait for the redirect to Google, then read a fresh snapshot
$ chrome-use wait --url "**/accounts.google.com**"
$ chrome-use snapshot -i
# Fill in the Google account, step by step
$ chrome-use fill @e1 "user@gmail.com"
$ chrome-use click @e2
$ chrome-use wait 2000
$ chrome-use snapshot -i
$ chrome-use fill @e3 "password"
$ chrome-use click @e4
# Wait for the redirect back to the app site
$ chrome-use wait --url "**/app.example.com**"
Sign in with Bitwarden passwords and passkeys
On the login page, run auth login --bwu to select the site's account through bitwarden-use (0.7.0+). Among several matching accounts it uses the one that signed in to that host last time (recorded by id only in ~/.chrome-use/bwu-items.json), else the one named exactly the site's host or the host of its sign-in frame (App Store Connect signs in through an idmsa.apple.com iframe), and says which and why on stderr; otherwise use --item. A site adapter that finds the site signed out reports a login wall: too (see site adapters). The first login wall on a site asks whether to sign in from the vault this time, always for that site, or never; an agent gets the question in loginWall.ask and must relay it to you. auth autologin status lists the decisions (stored per site in ~/.chrome-use/autologin.json), auth autologin always|never|off <host> changes one. Passwords, TOTP and private keys are passed through bwu run to the child, never placed in arguments or output.
Some QR sign-ins ask for a second, security-verification scan after the phone confirmed the first. Xiaohongshu shows a "Security Verification" (安全验证) dialog with a new QR code over the page (it expires in about a minute), its QR status poll answers HTTP 471, and the sign-in box underneath still says to confirm on the phone. Every command then warns login verification: … and carries loginVerification in --json (the code is in qrSrc): give that QR code (screenshot or qrSrc) to the user to scan with the signed-in app, and do not reload, open another page or sign in again until they have, or the verification is lost. Auto-login and site login walls leave the page alone meanwhile.
In a --launch browser, add --passkey to sign in using only the vault passkey (bwu 0.9.0+), without reading passwords, TOTP or custom fields. Ordinary password login can also answer a passkey second factor. Credentials go into a temporary WebAuthn authenticator on the tab and Chrome signs the request; the authenticator is removed after the attempt. While it is installed, navigator.credentials.create() is refused in the current page, its existing same-process and attached cross-process iframes, and every new document; afterwards each is restored, and any guard left in place is an error. Known limits: page script that ran before the guard (the page already open when the login starts) can have kept the original function or replaced Proxy and bypass it, and a cross-process iframe that attaches after the install is not guarded. In both cases the registration is still detected and aborts the command; unexpected registration or unconfirmed cleanup aborts the command. Only synced passkeys with counter 0 are supported; nonzero counters need vault write-back and are refused. Passkeys are unsupported on the extension relay: passkey-only login fails immediately; ordinary login keeps the password/TOTP flow. Without WebAuthn support, ordinary login falls back to passwords; passkey-only login fails.
$ curl -fsSL https://raw.githubusercontent.com/leeguooooo/bitwarden-use/main/install.sh | sh
$ chrome-use open https://github.com/login
$ chrome-use auth login --bwu --item github.com
$ chrome-use --session passkey-demo --launch open https://github.com/login
$ chrome-use --session passkey-demo --launch auth login --bwu --item github.com --passkey
$ chrome-use --session passkey-demo --launch snapshot -i
--no-submit fills only, skips TOTP and installs no passkey authenticator; it cannot be combined with --passkey. A signed assertion does not prove site acceptance: check the authenticated destination with snapshot or the site's authenticated state.
A password login is reported only when the site took it: after submitting, it waits until the sign-in form is gone from a loaded page (read twice in a row) and returns signedIn: true with the evidence; when the form is still there after about 12 s it fails with the sign-in was not confirmed, quoting the page's own message. The Enter is checked too: on a background tab of your own Chrome a key can be accepted by Chrome and never reach the page, so nothing was submitted, and the form's sign-in button is clicked instead (submittedWith says so).
Handling two-factor authentication (2FA)
When you hit a second step that needs SMS / TOTP / a hardware key, the least painful approach is to let a human
do that step while the script waits alongside for the redirect. On the default extension path driving your real
Chrome, the window is already visible, so just do the second step yourself; hand control back formally with
session handoff (see Sessions & Persistence) — after handoff the
agent refuses driving commands on that session until you return it. --headed is only needed when you
--launch a separate browser, paired with a longer --timeout to wait for the post-login URL.
# Show the browser window, log in with credentials first
$ chrome-use open https://app.example.com/login --headed
$ chrome-use snapshot -i
$ chrome-use fill @e1 "user@example.com"
$ chrome-use fill @e2 "password123"
$ chrome-use click @e3
# Complete 2FA by hand in the window; the script waits for the redirect (up to 120s)
$ chrome-use wait --url "**/dashboard" --timeout 120000
HTTP Basic and cookie auth
Not every site uses a form. For HTTP Basic Auth, set the credentials before navigating; when you need to inject a session manually, you can set the cookie directly.
# HTTP Basic: set username / password before navigating
$ chrome-use set credentials username password
$ chrome-use open https://protected.example.com/api
# Set an auth cookie manually
$ chrome-use cookies set session_token "abc123xyz"
$ chrome-use open https://app.example.com/dashboard
Log in once, skip it afterward
Login is expensive — don't redo it every time. After a successful login, save the cookies and localStorage
to a state file, then load it later with --state to start up already "logged in." It's the most
practical trick for skipping the whole OAuth / 2FA dance.
# Log in once, save cookies + localStorage
$ chrome-use state save ./auth.json
# Later runs start up already logged in
$ chrome-use --state ./auth.json open https://app.example.com
--session-name (or AGENT_BROWSER_SESSION_NAME) to automatically save and load
a session by name, with no files to manage by hand. State files contain plaintext session tokens — the
details on encryption, gitignore, and named sessions are all in
Sessions & persistence.
Security notes
Login inevitably touches credentials and session tokens. A few baselines:
- Don't commit state files to a repo — they hold plaintext session tokens; add them to
.gitignoreand delete them when you're done. - Route credentials through env vars or a vault — use
auth save/bwu/--stdin/ environment variables instead of hardcoding the password in a command. - Encrypt at rest — set
AGENT_BROWSER_ENCRYPTION_KEYso saved session state is encrypted. - Clean up when done —
chrome-use cookies clear --domain <site>plus deleting the temporary state file; in CI, don't persist to disk at all and runclosewhen the job finishes.