Getting started
You need Node.js 22.18 or newer and an app with auth running on localhost.
Install
Section titled “Install”npm install -D valetkey @valetkey/custompnpm add -D valetkey @valetkey/customvaletkey is the CLI and the defineConfig entry point. @valetkey/custom signs into homegrown auth with a mint function you write. Dedicated plugins for Better Auth, Auth.js, and Supabase are next on the roadmap.
In a hurry, or working with auth you do not control? Install @valetkey/record instead and sign in once by hand; valetkey captures the session and there is no mint function to write.
Set up the project
Section titled “Set up the project”npx valetkey initThis creates four things:
- An identity keypair for your machine, stored in the OS keychain.
.valetkey/recipients.txtand.valetkey/vault.age, the encrypted vault. Commit both; the vault only ever holds encrypted data.valetkey.config.tswith a starter persona.- A
.gitignoreentry forplaywright/.auth/, where exported sessions usually go.
Running in a script or an agent? valetkey init --yes --name myapp --origin http://localhost:3000 skips the prompts.
Store a secret
Section titled “Store a secret”valetkey vault set SESSION_SECRETThe value is prompted (or piped via stdin), never passed as an argument, so it stays out of your shell history. valetkey refuses values that look like production keys, such as sk_live_....
Tell valetkey how to sign in
Section titled “Tell valetkey how to sign in”Open valetkey.config.ts and implement mint(). For a typical app with a password sign-in endpoint:
import { createSessionArtifact, custom } from "@valetkey/custom";import { defineConfig } from "valetkey";
export default defineConfig({ app: { name: "myapp", origins: ["http://localhost:3000"], }, provider: custom({ mint: async (ctx, persona) => { const password = await ctx.secrets.get("DEV_USER_PASSWORD"); const response = await ctx.fetch("http://localhost:3000/api/sign-in", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: persona.seed.email, password }), redirect: "manual", }); const setCookie = response.headers.getSetCookie()[0] ?? ""; const [pair = ""] = setCookie.split(";"); const separator = pair.indexOf("="); return createSessionArtifact({ origins: ctx.origins, cookies: [ { name: pair.slice(0, separator), value: pair.slice(separator + 1), domain: "localhost", path: "/", expires: -1, httpOnly: true, secure: false, sameSite: "Lax", }, ], meta: { email: String(persona.seed.email) }, }); }, }), personas: { admin: { seed: { email: "admin@myapp.test" } }, },});The Write a provider guide covers seeding users, verification, and revocation.
Mint a session
Section titled “Mint a session”valetkey login admin --export playwright/.auth/admin.jsonvaletkey runs your mint(), clamps the session to a 60 minute expiry, saves the artifact under ~/.valetkey/sessions/ with 0600 permissions, and writes a Playwright storage-state file to the path you gave.
Connect your agent
Section titled “Connect your agent”Register valetkey as an MCP server, and the agent signs itself in whenever it needs to:
{ "mcpServers": { "valetkey": { "command": "npx", "args": ["valetkey", "mcp"] } }}That is .mcp.json for Claude Code, .cursor/mcp.json for Cursor, and ~/.codex/config.toml for Codex. The agent calls valetkey_login_as with a persona, valetkey starts a Chrome profile it manages, injects the session, and the agent browses your app signed in. No credential value ever appears in a tool response.
Prefer files, or driving the agent’s own browser? Point Playwright MCP at what you exported above:
npx @playwright/mcp@latest --storage-state playwright/.auth/admin.jsonConnect your agent covers Claude in Chrome, Chrome DevTools MCP, multiple personas, and CI.
Check the wiring
Section titled “Check the wiring”valetkey doctorThis verifies the config, your identity, the recipients list, and that the vault decrypts. valetkey list shows each persona and whether it has an active session.