For agents

Local dashboard auth

An autonomous agent (Claude Code, gstack /browse, a custom QA bot) often needs to inspect or interact with /dashboard. The dashboard is Clerk-protected, so the agent has to authenticate first. This page documents the supported way.

Quickstart — one HTTP call

With the dev server running (npm run dev):

bash
curl -s http://localhost:3000/api/dev/auth | jq -r .url

Navigate to the returned URL in a browser. Clerk creates an authenticated session for the deterministic test user and redirects to /dashboard.

The full response shape:

json
{
  "url": "http://localhost:3000/clerk/agent-task/...",
  "expiresAt": "2026-05-27T18:42:11Z",
  "expiresIn": 1800,
  "_meta": {
    "usage": "Navigate to `url` in a browser to authenticate as the dev test user. You will land on the redirect target.",
    "redirectTarget": "/dashboard",
    "docs": "http://localhost:3000/docs/agents/local-auth"
  }
}

Custom redirect:

bash
curl -s 'http://localhost:3000/api/dev/auth?target=/dashboard/sites' | jq -r .url

Alternative — CLI

bash
npm run dev:auth              # prints URL on stdout, metadata on stderr
npm run dev:auth -- --open    # also opens the URL in your browser
npm run dev:auth -- --json    # full JSON on stdout (machine-parseable)
npm run dev:auth -- --target /dashboard/api-keys

The CLI hits Clerk directly — it doesn't need npm run dev to be running to mint the URL, but you'll need the dev server up to actually use it.

Test user

By default the test user is agent+clerk_test@dropfast.dev. The +clerk_test local-part is Clerk's convention that suppresses email delivery and bypasses verification challenges. The user is auto-provisioned on first call.

Per-workspace isolation (parallel Conductor workspaces):

bash
# .env.local in each workspace
TEST_USER_EMAIL=agent+honolulu@dropfast.dev

This avoids races on the single-use sign-in token when two workspaces try to authenticate the same user simultaneously.

How it's gated

The route returns 404 unless all three independent gates pass:

  1. CLERK_SECRET_KEY starts with sk_test_
  2. process.env.NODE_ENV !== 'production'
  3. Request host is localhost or 127.0.0.1 and VERCEL_ENV is unset or 'development'

A 404 on gate failure means the route is invisible in production and preview deploys. Don't change this without reading lib/clerk-test-auth.ts carefully — the assertDevOnly function has a load-bearing comment block.

If you need to test against a preview deploy, point a local Playwright suite at the preview URL — the route doesn't need to be deployed.

Error shapes

When a gate fails, the route returns 404 with no body. When a gate passes but something else goes wrong, the route returns structured JSON:

json
{
  "error": {
    "code": "clerk_secret_missing",
    "problem": "Cannot mint local dashboard auth URL.",
    "cause": "CLERK_SECRET_KEY is unset in .env.local.",
    "fix": "Add a Clerk test secret (sk_test_*) to .env.local and restart npm run dev.",
    "docs": "/docs/agents/local-auth"
  }
}

Every dev-mode error carries code, problem, cause, fix, and docs.

Playwright

Playwright doesn't need this route. The e2e suite uses @clerk/testing/playwright directly via tests-e2e/global.setup.ts, which calls clerk.signIn() with the same test user. Same identity, different transport.

Headless browser caveat (gstack /browse, raw Playwright)

The magic-link URL works in real browsers — Chrome, Safari, Arc, Firefox. Open it via npm run dev:auth -- --open and you land on /dashboard authenticated.

In headless Chromium on http://localhost (no HTTPS), the Clerk handshake sets the __session_* cookie with SameSite=None, which Chromium then rejects because the connection isn't Secure. The other handshake cookies (__client_uat, __clerk_db_jwt) set with explicit Domain=localhost and pass, but without __session the middleware redirects to /sign-in.

Real browsers don't hit this — Clerk's JS bundle runs to completion and re-sets the cookie with SameSite=Lax, which works on plain-HTTP localhost.

Workarounds for headless agents (when you can't open a real browser):

  1. Use the Playwright e2e suite directly — it uses @clerk/testing/playwright's clerk.signIn() which injects the session via window.Clerk instead of redirect handshake. The session sticks.
  2. Run next dev --experimental-https — with HTTPS on localhost, the SameSite=None; Secure cookie sets correctly even in headless.
  3. Hand the URL to a real browser via --open — best for ad-hoc QA.

This is a Clerk dev-instance + HTTP-localhost + headless-Chromium interaction, not a flaw in the route itself.

Edit this page on GitHub