Agents need credentials to do real work. A browser agent signs in to a dashboard with a username and password. A coding agent in a sandbox calls a model provider with an API key. The usual way to get them there is to put the value somewhere the agent can reach: a line in the prompt, an argument in a tool call, an environment variable baked into an image.
That works, but the value spreads. It ends up in transcripts, logs, traces and your model provider's request history, and anything that can read those can read the secret. A browser agent has it worst: to type a password into a login form, the model has to hold the password.
Credential fill and secrets remove that step. You store a value once as a secret, and your browser sessions and sandboxes use it by name. The value is decrypted only where it's used, and it never reaches your code, your logs or the model.
One secret store, encrypted before it leaves your machine
Every secret is encrypted on your machine, by the SDK, the hx CLI or the dashboard,
before it's uploaded. The API receives an encrypted envelope bound to that one secret and stores it as
is. No endpoint returns a value, and the plaintext is decrypted only in the VM of the browser or
sandbox that uses it.
- Allowed origins decide where it's typed. A secret can be filled into a page only after you list the sites it may be used on.
- Versioned. Storing a new value adds a version. Everything that uses the secret picks up the latest version unless you pin one, so rotating a password or key doesn't need a redeploy.
-
Permissioned. Using secrets needs an API key with
secrets.use, separate fromsecrets.managefor creating and rotating them. In the dashboard, only team owners and admins can manage them.
Fill secrets into login forms
A login form doesn't read environment variables. Something has to type the password into the page. Credential fill makes that the platform's job.
Store each value you want typed as its own secret, with the origins it may be used on. Attach the secrets when you create a browser session, then ask for a fill once the session is on the login page:
import os
from playwright.sync_api import sync_playwright
from hyperbrowser import Hyperbrowser
client = Hyperbrowser()
github = {"browser": {"allowedOrigins": ["https://github.com"]}}
client.secrets.create({
"name": "github-username",
"value": os.environ["GITHUB_USERNAME"],
"metadata": github,
})
client.secrets.create({
"name": "github-password",
"value": os.environ["GITHUB_PASSWORD"],
"metadata": github,
})
session = client.sessions.create({
"credentials": ["github-username", "github-password"],
})
with sync_playwright() as p:
page = p.chromium.connect_over_cdp(session.ws_endpoint).contexts[0].pages[0]
page.goto("https://github.com/login")
results = [
client.sessions.credentials.fill(session.id, {
"credential": name,
"fields": [{"selector": selector}],
})
for name, selector in [
("github-username", "#login_field"),
("github-password", "#password"),
]
]
if all(field.outcome == "filled" for result in results for field in result.fields):
page.click("input[type=submit]")Each fill names a secret and the CSS selectors of the inputs it goes into. One secret can go into several inputs at once, such as a password and its confirmation field. The platform decrypts the secret inside the browser's VM and types it in. The response reports, for each input, the outcome and the secret version it received, and never contains the value:
-
filled: the input holds exactly the value. -
not_foundorambiguous: no input, or more than one visible input, matched the selector. -
not_editable: the input is disabled, read-only, hidden or not a text field. -
not_accepted: the page didn't keep the value, for example because of a length limit.
Values are typed the way the element expects. A value read from a file usually ends with a newline,
which a single-line <input> can't hold, so it's typed without it; a
<textarea> keeps its line breaks.
Fill only types. It never submits, so you decide when to click "Sign in", and a fill is always safe to
retry. With several tabs open, pass pageUrl to pick the tab whose URL starts with it,
such as https://github.com/login.
Typed only where you said
Before typing anything, the platform checks the origin the browser reports for the target page against
the secret's allowed origins: https://github.com, or https://*.example.com
for a site's subdomains. On a look-alike domain, a redirect to an unexpected site or an unexpected
iframe, the fill is refused and the form stays empty.
That check runs in the platform, not in the agent. A model can be steered to the wrong site, but it can't make the platform fill a secret there.
- Attached up front. A session can only fill secrets attached when it was created.
- No saved passwords. Sessions with attached secrets run with Chrome's password manager turned off.
- Kept out of replays. Inputs a fill writes to are excluded from session replays.
- Tracked through snapshots. Snapshots of a session that received a secret are marked as containing secrets, and restoring one needs the same access as using them.
- Recorded. Every fill, and every refused attempt, is written to the session's event log with the secret and version each input received.
Let the agent sign in
The Python and Node SDKs include a ready-made fill_credential tool for OpenAI, Anthropic
and Gemini. Add it to your agent's tools, and the agent can sign in on its own:
import json
from hyperbrowser.tools import CredentialFillTool
tools = [CredentialFillTool.openai_tool_definition] # plus your click and navigate tools
for call in message.tool_calls:
if call.function.name == "fill_credential":
output = CredentialFillTool.runnable(
client, session.id, json.loads(call.function.arguments)
)Your application binds the session. The model only chooses which secret to use and which inputs to type into, by name and selector. Failures come back as structured results the model can act on: if several open tabs could hold the login form, it gets the list of tabs and retries with the right one.
In an end-to-end run, an OpenAI model and a Gemini model each got the tool, the names of the stored login and the login form. Each one chose the inputs, filled them and signed in. A search of everything the model sent and received found the password zero times.
Secrets for sandbox processes
The same secrets cover values your sandboxes need, such as API keys. A secret reaches a sandbox process as an environment variable. Pass it by name when you start the process, and the platform sets it in that process's environment, inside the sandbox's VM:
hx secrets create provider-key --from-file ./provider-key.txt
hx vm exec <sandbox-id> --secret API_KEY=provider-key -- node app.jsThe same flow from the Python SDK:
client.secrets.create({
"name": "provider-key",
"value": open("provider-key.txt", "rb").read(),
})
sandbox = client.sandboxes.create({"image_name": "node"})
result = sandbox.exec("node app.js", secrets={"API_KEY": "provider-key"})- One process at a time. Only the process you pass the secret to receives it. Other processes don't inherit it.
- Exact bytes. Environment variables get the value exactly as stored.
- Out of your shell history. The CLI reads values from a file or stdin, never from arguments.
- Startup commands. Pass secrets when you create a sandbox, and the image's startup command starts with its keys in place.
- Tracked through snapshots. A sandbox that received a secret, and every snapshot of it, is marked as containing secrets, just like browser sessions.
Know what stays readable
Credential fill and secrets keep values out of your code, prompts, transcripts and the API. Some things they can't change:
- A filled field belongs to the page. The site's own scripts, browser extensions and your own CDP connection to the session can read it.
- Screens show what's on them. Password fields are masked by the page, but a filled username is visible in video recordings and the live view.
- A process can read its environment. Inside a sandbox, anything running as the same user, or as root, can read the environment of a process that received a secret.
- Deleting isn't revoking. Deleting a secret doesn't take back a value that was already delivered. If a value may have leaked, rotate it where it was issued.
What's next
Today a username and a password are two secrets and two fills. Next, vaults will group a login's values so one fill can type them all, and one-time codes will be generated at fill time from a stored seed that never reaches the browser.
Get started
Credential fill is available in the Node.js SDK 1.3.0, the Python SDK 1.13.0, the hx CLI
and the REST API. Sandbox secrets are available from Node.js SDK 1.2.1 and Python SDK 1.12.0. Both are
enabled for teams on request, so please contact support to enable them for your team.
-
Fill a login: create a secret with allowed origins
(
metadata.browser.allowedOrigins, orhx secrets update <name> --allowed-origin <origin>). Attach it withcredentialswhen you create a session, and fill it withclient.sessions.credentials.fillorhx browser fill. -
Agents: add
CredentialFillToolto your OpenAI, Anthropic or Gemini tools. -
Sandbox secrets: create one with
client.secrets.createorhx secrets create, then pass it tosandbox.exec,sandbox.processes.startorhx vm exec --secret.
Both features are in public preview, and we're actively looking for feedback. Tell us which sites and login flows you want your agents to handle, what's missing for your use case, and anything that surprised you. Reach us on X, on Discord, or through support on hyperbrowser.ai.
