Skip to content

Getting started

You need Node.js 22.18 or newer and an app with auth running on localhost.

Terminal window
npm install -D valetkey @valetkey/custom

valetkey 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.

Terminal window
npx valetkey init

This creates four things:

  • An identity keypair for your machine, stored in the OS keychain.
  • .valetkey/recipients.txt and .valetkey/vault.age, the encrypted vault. Commit both; the vault only ever holds encrypted data.
  • valetkey.config.ts with a starter persona.
  • A .gitignore entry for playwright/.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.

Terminal window
valetkey vault set SESSION_SECRET

The 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_....

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.

Terminal window
valetkey login admin --export playwright/.auth/admin.json

valetkey 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.

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:

Terminal window
npx @playwright/mcp@latest --storage-state playwright/.auth/admin.json

Connect your agent covers Claude in Chrome, Chrome DevTools MCP, multiple personas, and CI.

Terminal window
valetkey doctor

This 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.