Auth state
Capture a named web login once, reuse it across tests and suites, and pass the active Playwright storage-state JSON to hooks without exposing credential material elsewhere.
Auth state lets a web test start from an already signed-in browser session. Capture the session once for a target, give it a logical name, then select that name from a test or suite with use.authState.
Use auth state when the product login flow is slow, protected by MFA, or not the thing the test is trying to verify. The first step of the product test should still prove the session is valid, such as verifying the dashboard, account menu, or tenant switcher is visible.
Auth state is credential material. Keep it local, ignore it from git, avoid printing it, and refresh it manually when the product session expires.
Configure storage
agent-qa init creates the auth-state directory setting for new workspaces. If you are wiring the config by hand, keep auth state under .agent-qa beside cache and artifacts.
services:
authState:
dir: .agent-qa/auth-statesAlso make sure the directory is ignored by git.
.agent-qa/auth-states/The state is scoped by target and logical name. The same logical name can exist for multiple environments because the target name is part of the internal resolution.
registry:
targets:
app-uat:
platform: web
url: https://uat.example.com
app-staging:
platform: web
url: https://staging.example.com
app-prod:
platform: web
url: https://app.example.comIf each target has an auth state called qa-admin, use.authState: qa-admin resolves to the state for the selected target.
Capture from Live Mode
Live Mode is the primary capture flow.
Start a Live Mode web session
Open the dashboard, start a web Live Mode session for the target, and let the browser open the product URL.
Sign in manually
Complete the product login flow as the QA account. This can include MFA, SSO, tenant selection, or any other interactive step that should not run in every test.
Save auth state
Use the Live Mode Save auth state action, enter the logical state name, and confirm replacement when updating an existing state.
Live Mode writes the same Playwright-compatible storage-state JSON and metadata used by CLI capture. Saving auth state is an explicit action; normal product test runs do not update auth state.
Capture from the CLI
Use CLI capture when you want the same flow outside the dashboard.
The command opens a headed browser for the target URL. Sign in manually, then return to the terminal and press Enter to save. If the browser closes before confirmation, agent-qa does not save the state.
The saved payload contains Playwright storage state with cookies, localStorage, and IndexedDB. SessionStorage is not part of the V1 contract.
Run a test with auth state
Select the auth state by logical name in a test.
test-id: t_vog-earing-wap-git-tim-assert-teras-mill-aus-ila
name: Verify billing dashboard
target: app-staging
use:
authState: qa-admin
steps:
- Open the Billing page.
- Verify the account menu is visible.
- Verify the latest invoice table is visible.use.authState is a per-test or per-suite capability grant. It is not a global default. A run can use exactly one primary auth state in V1.
If the state is missing or unreadable, the run fails before creating the browser context and points you back to CLI capture.
Run a suite with auth state
For suites, put use.authState on the suite when every child test should share the same authenticated browser context.
suite-id: s_far-gnu-mean-junk-aga-visual-knife-lend-few-vis
name: Billing smoke
target: app-staging
use:
authState: qa-admin
browser:
name: chromium
headless: true
tests:
- test: tests/billing-dashboard.yaml
id: t_vog-earing-wap-git-tim-assert-teras-mill-aus-ila
- test: tests/billing-invoice-download.yaml
id: t_aster-bloom-cloud-drift-ember-field-glade-hollow-ivory-jasperChild tests inherit the suite auth state when they omit use.authState. A child can repeat the same auth-state name, but a different child auth state is rejected because the suite has one shared authenticated runtime.
Share the same test across environments
Keep the test reusable by giving each environment the same logical auth-state name for its own target.
# tests/billing-dashboard.yaml
name: Verify billing dashboard
target: app-staging
use:
authState: qa-admin
steps:
- Open the Billing page.
- Verify the account menu is visible.Then run the same test against another configured target through the suite or CLI target selection used by your workflow. agent-qa resolves qa-admin inside the selected target's auth-state directory. Tests never reference auth-state file paths directly.
Hook contract
When setup, inline, or teardown hooks run inside an authenticated web test or suite, agent-qa exposes only the active auth state to that hook process.
Hooks receive two environment variables:
AGENT_QA_AUTH_STATE_JSON: a structured JSON object with auth-state metadata and the raw storage-state path.AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH: direct path to the raw Playwright storage-state JSON.
No auth-state environment variables are present when the run did not select an auth state. Hooks never receive all configured auth states.
The JSON shape is stable and runtime-neutral.
{
"version": 1,
"kind": "web",
"target": "app-staging",
"name": "qa-admin",
"capturedAt": "2026-05-17T15:06:00.000Z",
"storageStatePath": "/workspace/.agent-qa-auth-state/storage-state.json"
}Hook authors parse the raw JSON themselves. Do not install Playwright inside hooks just to read auth state.
Node
// scripts/read-auth-state.mjs
import { readFile } from "node:fs/promises"
const authStatePath = process.env.AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH
if (!authStatePath) {
throw new Error("This hook requires use.authState")
}
const authState = JSON.parse(await readFile(authStatePath, "utf-8"))
const sessionCookie = authState.cookies?.find((cookie) => cookie.name === "__session")
const appOrigin = authState.origins?.find((origin) => origin.origin === "https://staging.example.com")
const tenantId = appOrigin?.localStorage?.find((item) => item.name === "tenant_id")?.value
if (!sessionCookie?.value) {
throw new Error("Saved auth state did not contain the expected session cookie")
}
console.log(`Found tenant ${tenantId ?? "unknown"}`)Bun
// scripts/read-auth-state.js
const authStatePath = process.env.AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH
if (!authStatePath) {
throw new Error("This hook requires use.authState")
}
const authState = JSON.parse(await Bun.file(authStatePath).text())
const sessionCookie = authState.cookies?.find((cookie) => cookie.name === "__session")
const appOrigin = authState.origins?.find((origin) => origin.origin === "https://staging.example.com")
const tenantId = appOrigin?.localStorage?.find((item) => item.name === "tenant_id")?.value
if (!sessionCookie?.value) {
throw new Error("Saved auth state did not contain the expected session cookie")
}
console.log(`Found tenant ${tenantId ?? "unknown"}`)Python
# scripts/read_auth_state.py
import json
import os
auth_state_path = os.environ.get("AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH")
if not auth_state_path:
raise RuntimeError("This hook requires use.authState")
with open(auth_state_path, "r", encoding="utf-8") as auth_state_file:
auth_state = json.load(auth_state_file)
session_cookie = next((cookie for cookie in auth_state.get("cookies", []) if cookie.get("name") == "__session"), None)
app_origin = next((origin for origin in auth_state.get("origins", []) if origin.get("origin") == "https://staging.example.com"), {})
tenant_id = next((item.get("value") for item in app_origin.get("localStorage", []) if item.get("name") == "tenant_id"), None)
if not session_cookie or not session_cookie.get("value"):
raise RuntimeError("Saved auth state did not contain the expected session cookie")
print(f"Found tenant {tenant_id or 'unknown'}")Bash
#!/usr/bin/env bash
set -euo pipefail
auth_state_path="${AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH:-}"
if [[ -z "$auth_state_path" ]]; then
echo "This hook requires use.authState" >&2
exit 1
fi
session_cookie="$(jq -r '.cookies[]? | select(.name == "__session") | .value' "$auth_state_path" | head -n 1)"
tenant_id="$(jq -r '.origins[]? | select(.origin == "https://staging.example.com") | .localStorage[]? | select(.name == "tenant_id") | .value' "$auth_state_path" | head -n 1)"
if [[ -z "$session_cookie" || "$session_cookie" == "null" ]]; then
echo "Saved auth state did not contain the expected session cookie" >&2
exit 1
fi
printf 'Found tenant %s\n' "${tenant_id:-unknown}"Security boundary
Auth state follows a narrower boundary than ordinary test variables.
- Tests, suites, and hooks select auth state by logical name, not by filesystem path.
- A hook receives only the selected active state for that authenticated run.
- Auth-state payload contents and storage paths are excluded from run artifacts, dashboard run APIs, MCP run tools, logs, errors, and analytics by default.
- Normal product runs are read-only with respect to auth state.
- Capture and replacement require an explicit user action in Live Mode or the CLI.
The auth-state management UI and metadata APIs can show target and name values because users need those values to manage states. They still do not return cookies, localStorage, IndexedDB, or raw storage-state paths.
Do not echo the raw storage-state JSON from a hook. Redaction is best effort, and application cookies or tokens may not match values from workspace.secretsFile.
Refresh stale state
agent-qa does not try to prove that a saved session is still valid. Let the first product step validate the session.
steps:
- Open the Dashboard page.
- Verify the account menu for the QA user is visible.If that step fails because the product redirected to login, refresh the state manually by running CLI capture again or saving from Live Mode with replacement.
Native mobile boundary
use.authState is web-only. For native Android and iOS apps, use use.mobile.appState: preserve when you want to keep installed app data between sessions.
target: issue-tracker-android
use:
device: android-local
mobile:
appState: preserveApp-state preservation is broader than auth. It can keep app data, caches, preferences, and other local state depending on the device and app lifecycle. agent-qa does not promise generic export of secure storage, keychain entries, shared preferences, app-private files, or native mobile auth tokens.
Use mobile app-state preservation as the native-app fast path. If a product needs direct native token export, that is app-specific work and usually requires app cooperation, debug-only hooks, or a purpose-built test endpoint.