In the prompt or the chat
A password you paste becomes part of the conversation: kept in the chat history and its transcripts, and sent to the model provider with every turn that follows.
AI agents / Sign in without the password
Give an agent a password and it ends up in its context: the chat, the logs, the tool output and the model provider’s servers. In Browz you store the sign-in once, and the agent asks Browz to use it: Browz types the password and the authenticator code into the page itself, only on the sites you allow, and the agent is told only that the field was filled. You decide exactly what each agent may do: its own key, its own profiles, only the sites you allow, sign-ins it can use on your computer but never read, and a person for the login.
# You, once, in a terminal (or Automate › Saved sign-ins in the app)
browz credential add --label "Shop (Jane)" --username [email protected] \
--site shop.example.com --profile <profile id> --totp
# Password: (hidden prompt)
# TOTP seed (base32 or otpauth:// URI): (hidden prompt)
# A key for this agent only. Stored sign-ins need browz:credentials:use, never in the default set.
browz agent-token create --name "sign-in bot" --profiles <profile id> --sites shop.example.com \
--scopes browz:profiles:read,browz:browser,browz:credentials:use > ~/.browz-sign-in-bot.key
chmod 600 ~/.browz-sign-in-bot.keyThe problem
Most agents sign in the way a script does: someone hands them the password. From then on it is in places you do not control.
A password you paste becomes part of the conversation: kept in the chat history and its transcripts, and sent to the model provider with every turn that follows.
An agent that can run commands can print it, and anything it prints becomes tool output in its context, and from there in the same places as the chat.
To type a password, an ordinary browser tool takes it as an argument, so it is written into MCP logs, traces, debugging output and whatever records the agent’s calls.
A page that plants instructions can ask an agent to type the password somewhere else or repeat it. An agent that knows a password can be talked into using it.
How Browz does it
Six steps, the same for any agent on your computer or your own server.
Run browz credential add with a label, the username, the sites it may be typed on and who may use it (one profile with --profile, a group with --group or every profile with --any), or use Automate › Saved sign-ins in the app. Browz asks for the password, and with --totp the authenticator seed, with a hidden prompt: secrets are never taken from the command line, where they would land in your shell history.
Create a key with browz agent-token create, limited to that profile and that site, with the browz:credentials:use scope, which is never in a key’s default set. The key can use the sign-in; it can never add, change, remove or read one.
On a sign-in page, browz_snapshot already names the stored sign-ins this agent may use there, by label and id. browz_credential_list gives each one’s label, a masked username such as j•••@example.com, its sites and whether it has a code. Never a password or seed.
It calls browz_credential_fill with the field’s ref from the snapshot and which value goes there. Browz checks the page’s address, the field and the limits, then types the value itself at the profile’s human pace, and the agent is told only that the field was filled.
While a filled secret is in the page, the agent cannot run script, take screenshots or wait for page text in that profile, and snapshots show the field as [value=<redacted>]. Submitting the form, which loads the next page, lifts it.
Each fill, refused ones included, is a row in your local action log and, for an agent key, in that key’s own log: the time, the profile, the page’s origin and path, the sign-in’s id, the field and the outcome.
browz_credential_list {"id": "<profile id>"}
→ [{"id": "cred_3f…", "label": "Shop (Jane)", "usernameHint": "j•••@example.com",
"sites": ["shop.example.com"], "hasTotp": true}] # never a secret
browz_snapshot → the sign-in form, with refs: e14 email, e17 password
browz_credential_fill {"id": "<profile id>", "credentialId": "cred_3f…", "ref": "e14", "field": "username"}
browz_credential_fill {"id": "<profile id>", "credentialId": "cred_3f…", "ref": "e17", "field": "password", "submit": true}
→ {"filled": true, "credentialId": "cred_3f…", "field": "password",
"submitted": true, "origin": "https://shop.example.com"} # all the agent gets back
browz_credential_fill {"id": "<profile id>", "credentialId": "cred_3f…", "ref": "e9", "field": "totp", "submit": true}
# Browz works the code out at that momentbrowz agent-token log --agent <key id>
# one row per call, refused ones included, kept 90 days:
# time · tool browz_credential_fill · profile · page https://shop.example.com/login
# credential cred_3f… · field password · outcome ok · reason filled (never the value)Examples
The same sign-in and key, used from Claude Code over MCP and from any language over the Local API on 127.0.0.1. Replace <profile id> with a profile from browz list.
# 1. Store the sign-in and make Claude Code its own key (the two commands above), then:
claude mcp add browz -e BROWZ_LOCAL_AGENT_TOKEN_FILE=$HOME/.browz-sign-in-bot.key -- npx -y @browz/mcp
# 2. Ask Claude Code:
# Start the Browz profile <profile id>, open https://shop.example.com/login and sign in
# with the saved sign-in "Shop (Jane)". Never ask me for the password.
#
# It calls browz_profile_start, browz_navigate, browz_snapshot, browz_credential_list,
# browz_credential_fill for each field, then browz_snapshot to check it is signed in.KEY=$(cat ~/.browz-sign-in-bot.key) # the agent's key, not your Local API token
API=http://127.0.0.1:50333
curl -s -X POST $API/api/v1/browser/start -H "Authorization: Bearer $KEY" -d '{"id":"<profile id>"}'
curl -s -X POST $API/api/v1/browser/navigate -H "Authorization: Bearer $KEY" \
-d '{"id":"<profile id>","url":"https://shop.example.com/login"}'
curl -s "$API/api/v1/credential/list?profileId=<profile id>" -H "Authorization: Bearer $KEY"
curl -s -X POST $API/api/v1/browser/snapshot -H "Authorization: Bearer $KEY" -d '{"id":"<profile id>"}'
curl -s -X POST $API/api/v1/credential/fill -H "Authorization: Bearer $KEY" \
-d '{"id":"<profile id>","credentialId":"cred_3f…","ref":"e14","field":"username"}'
curl -s -X POST $API/api/v1/credential/fill -H "Authorization: Bearer $KEY" \
-d '{"id":"<profile id>","credentialId":"cred_3f…","ref":"e17","field":"password","submit":true}'
# {"code":0,"data":{"filled":true,"credentialId":"cred_3f…","field":"password","submitted":true,"origin":"https://shop.example.com"}}Claude Desktop, Cursor and every other MCP client take the key the same way, as BROWZ_LOCAL_AGENT_TOKEN_FILE in the server’s environment: see the setup docs. Never put the key on a command line, and never give an agent your Local API token.
To see it end to end, run the stored sign-in recipe: a demo site on your computer, a stored password and authenticator code, and an agent (a script or Claude Code) that signs in without seeing either.
By design
Browz signs an agent in only where you said it may, and hands everything else to a person.
browz_request_human: automation pauses on that profile, its window comes to the front with a notification, you finish the step, and the agent carries on in the same signed-in session. On a profile running headless, the agent stops and tells you.Security model
In plain words, from the code that stores and types it.
In the Browz agent’s own database on your computer, not on our servers and not in sync. The secret part is sealed twice: under your operating system’s keystore (the macOS Keychain or Windows DPAPI), then with this installation’s own key (AES-256-GCM), so a copy of the data folder alone opens nothing. On Linux, or where no keystore can be used, the installation’s key protects it alone, and the list says so.
Nobody, through Browz. No MCP tool, Local API route or screen returns a stored password, seed or secret field: you can replace one, not display it, and diagnostics bundles scrub them. A key that may use stored sign-ins never receives the browser’s debugging endpoint, which could read a typed value out of the page.
The label, a masked username, the sign-in’s id and “filled”. The password and the code never enter the agent’s context, so they are never part of its prompt, its transcript or its tool results, and a verbatim copy of a filled value in a page result is scrubbed.
Browz governs what an agent does through Browz. A page you allowed can still leak a value it was given, by copying it in a changed form or sending it to its own server, so store sign-ins only for sites you trust with them. A program that runs as the same computer user as Browz can read its data folder, so run an agent you do not trust under another user or in a container. If the Browz agent restarts, it closes the profile’s browser before it answers any request, so a filled page is never open to an agent without this protection.
browz agent-token revoke <key id> --stop-browsers # this agent, for good
browz agent-token stop-all # every agent at once
browz credential remove <credential id> # the sign-in itselfIn the app, Automate › Agent keys has the same revoke, resume and Stop all agents buttons, and the key’s activity log.
And the browser itself
The profile the agent signs in to is an ordinary Browz profile: its own cookies, storage and proxy, kept signed in for the next run. Underneath, every profile’s fingerprint is set in the browser engine itself, not by a script on top, and you can test it yourself (engine-level vs injected scripts).
Stored sign-ins are for accounts you are entitled to use. Use agents only where each site’s terms allow it: our acceptable use policy applies to agents the same as to people.
FAQ
No. It never receives it: Browz types the value into the page itself and answers only that the field was filled. While the value is in the page, script, screenshots and text waits are refused in that profile and snapshots show the field as redacted. One part stays with you: a page you allowed can still leak a value it was given, so store sign-ins only for sites you trust.
The password and the code never enter the agent’s context, so they are not in anything the agent sends to its model. What the model sees is the sign-in’s label, a masked username and the word “filled”. The username itself shows in the page once it is typed.
Then nothing is typed. Browz reads the tab’s address from the browser at the moment of the fill and compares whole host names in their ASCII form, so example.com never matches badexample.com, example.com.evil.net or a look-alike with a Cyrillic letter. The page must be https, every frame around the field must match too, and fields in frames from other sites cannot be reached. A key with allowed sites adds a second check of its own.
Yes, with authenticator (TOTP) codes: store the seed with --totp, and Browz works out the code at the moment of the fill (SHA1, SHA256 or SHA512, 6 to 8 digits). Codes sent by text or email, passkeys, security keys and push approvals go to a person through browz_request_human.
Run browz agent-token revoke with the key’s id to end that agent for good, or browz agent-token stop-all (Stop all agents, in the app) to stop every agent at once. A fill under way is checked again before the value is taken from the store and before the form is submitted. browz credential remove deletes the sign-in itself.
A hosted agent hands the sign-in to you with browz_request_human: stored sign-ins stay with agents on your computer, on purpose. Claude Code, Claude Desktop, Cursor and every agent that runs the local MCP server or calls the Local API can use them.
Agent keys and stored sign-ins are part of Browz on every plan, Free included, and run on your computer without our server. Free for 5 profiles. Paid plans from $4.99 a month, with a 14-day refund on your first payment.