Drive a real browser for a task that needs one — a web login, an end-to-end check, verifying a UI, reaching something behind auth — with credentials pulled from a vault so charter hands the password to the browser instead of you typing it into the conversation. Use when browser automation needs to authenticate, or when several workers each need their own logged-in session.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add diazoxide/charter --skill browser --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Browser?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/diazoxide-browser)More formats (shields.io, HTML) on the badges page.
---
name: browser
description: Drive a real browser for a task that needs one — a web login, an end-to-end check, verifying a UI, reaching something behind auth — with credentials pulled from a vault so charter hands the password to the browser instead of you typing it into the conversation. Use when browser automation needs to authenticate, or when several workers each need their own logged-in session.
---
# Driving a browser with vault credentials
Two halves, owned by different projects:
- **How to drive a page** — snapshots, clicking, network mocking, tracing — is
Playwright's. Generate its reference into this plane once:
```bash
charter browser install # writes .claude/skills/playwright-cli/
```
Charter ships none of those pages: they are Apache-2.0 and they change far more often
than charter releases. Regenerate with that command rather than editing them.
That command also gitignores `.playwright-cli/` — where traces and snapshots land, and a
trace holds the network traffic of whatever it recorded, logins included — and states,
without deciding, whether the generated reference and `.playwright/cli.config.json` should
be committed. All three postures are in `docs/browser.md`.
- **Where credentials come from, and how parallel workers stay isolated** — this skill.
## One session per worker
Each worker passes its own `-s=<name>`. Sessions hold independent cookies, localStorage,
IndexedDB, cache and tabs, so N workers can each be logged in as a different user at once.
```bash
npx @playwright/cli@<version> -s=owner open https://example.test/
npx @playwright/cli@<version> -s=viewer open https://example.test/
npx @playwright/cli@<version> list # live sessions
npx @playwright/cli@<version> -s=owner close
npx @playwright/cli@<version> kill-all # reap stale processes
```
**Pin the version explicitly in every command** — and this is a hard requirement, not
style. A session belongs to the *version* that opened it: state lives in
`~/Library/Caches/ms-playwright/daemon/<hash>/<name>.session`, and that hash keys on the
installation, not on your working directory. Two commands that resolve different versions
therefore look at different daemons, and the second reports:
```
The browser 'owner' is not open, please run open first
```
…while the first browser is alive and still logged in. Drop the pin on one command in a
flow — leaving `npx` to fetch whatever is latest — and you get exactly that. Sessions do
cross directories; they do not cross versions.
## Credentials — never typed, never printed
`charter secret exec --dotenv` resolves vault keys into one 0600 temp file and points
`PLAYWRIGHT_MCP_SECRETS_FILE` at it. You then refer to a secret **by name**; Playwright
substitutes the value and scrubs it from the output it captures, so a step that
accidentally echoes it is masked. That is the same net the vault has, with the same limit:
it is not a boundary. A step that *transforms* or forwards the value — a screenshot, an
`eval`, a POST — is not scrubbed, so the credential goes wherever you sent it.
> **`not open` is not a cue to re-open.** A second `open` with the same `-s=<name>` starts
> a *new* browser and orphans the first — which is the one that is logged in. Worse, the
> new one cannot be authenticated: `charter secret exec` deletes its dotenv on exit, so
> filling a secret into it means re-running the whole bridged flow. Check the session is
> really gone first (a `snapshot` against it is cheap and answers the question); if it is,
> re-run the flow rather than `open`.
> **Open the session *inside* the bridge.** `playwright-cli` reads
> `PLAYWRIGHT_MCP_SECRETS_FILE` once, when the session daemon starts. Setting it later, on
> a `fill` against an already-open session, **fails silently** — the literal string `PASS`
> is typed into the password field and no error is raised. Wrap the whole flow, `open`
> through the last `fill`, in a single `charter secret exec`.
> **Wait for each field before filling it.** `open` returns as soon as the first response
> lands, not when the page you are logging in to exists — an SPA typically redirects to an
> identity provider afterwards. Filling immediately fails with
> `"#username" does not match any elements`, which reads as a **wrong selector** and sends
> you hunting for a better one when the flow was already right. It is not free to get wrong
> here: `charter secret exec` deletes the dotenv file when it exits, so the still-open
> session can no longer be filled with a secret, and recovering means re-running the whole
> flow rather than the failed step. The example waits so it never needs to.
```bash
charter secret exec <vault> \
--dotenv PLAYWRIGHT_MCP_SECRETS_FILE=USER:<user-key> \
--dotenv PLAYWRIGHT_MCP_SECRETS_FILE=PASS:<pass-key> \
-- bash -c '
P="npx @playwright/cli@<version> -s=owner"
# `open` returning is not the page existing. Poll for the element itself rather than
# sleeping a fixed amount: a redirect to an IdP can take a moment or several.
wait_for() {
for _ in $(seq 1 15); do
$P eval "() => !!document.querySelector(\"$1\")" 2>/dev/null | grep -q true && return 0
sleep 2
done
echo "TIMEOUT waiting for $1" >&2; return 1
}
$P open https://example.test/
wait_for "#username" || exit 1
$P fill "#username" USER
wait_for "#password" || exit 1
$P fill "#password" PASS
$P click "button[type=submit]"
$P snapshot
'
```
The second `wait_for` is not redundant: an identity provider may ask for the username on
one screen and the password on the next, so `#password` can be absent at the moment the
first field is submitted.
Use `charter persona secret exec` to read the **active persona's** vault instead of naming
one. A different fixture account is a different pair of keys, not a different mechanism.
To avoid re-authenticating on every run, save the logged-in state once and reload it — see
`storage-state` in the generated Playwright reference.
## Reading the token the session is holding
The step after logging in: call the API **as the user you just logged in**, so the UI and
the API can be checked against each other.
Do not read it into a shell variable. Playwright's own reference documents
`TOKEN=$(playwright-cli --raw cookie-get session_id)`, and that is command substitution
into a transcript with nothing redacting it — the outcome this whole lane exists to
prevent. Register it as a reference instead and let the bridge carry it:
```bash
charter secret set <vault> API_TOKEN --value 'browser://owner/localstorage/access_token'
charter secret exec <vault> --env TOKEN=API_TOKEN -- \
curl -sH "Authorization: Bearer $TOKEN" https://api.example.test/me
```
`browser://<session>/localstorage/<key>` and `browser://<session>/cookie/<name>` are the two
forms. Charter builds the invocation, so the version pin is right by construction. The
session must already be open — resolving does not open one — so do this in the same flow
that logged in.
This is not an exception to the hard rule below. That rule forbids reading back a secret
**you filled in**; the value here is one the *session* minted, it is never printed, and it
reaches the API through the same bridge the password came in on. Reading it any other way —
`eval`, a screenshot, a bare `cookie-get` — is still out.
## Hard rules
- **Never read a filled secret back.** No evaluating `el.value`, no screenshot of a filled
password field, no dumping the dotenv file. Substitution and redaction exist precisely to
keep the value out of the conversation; reading it back defeats both.
- Never type a credential in directly, even "just once to test". Use the bridge.
- Never commit a session directory or a storage-state file — they carry live cookies, which
are the credential in another form. The same goes for traces: a trace records requests
with their headers and bodies, so tracing a bridged login writes the credential to disk
even though charter kept it out of your transcript. `charter browser install` gitignores
`.playwright-cli/` for exactly this.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!