From 01326941ffa5fbc8e8dd8e843cb634c61feef182 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 19 Oct 2026 17:14:05 -0400 Subject: [PATCH 01/17] feat(shared): add AGENTS.md writer for Clerk agent rules Co-Authored-By: Claude Opus 5.5 --- .changeset/shared-agent-rules.md | 5 + packages/shared/package.json | 20 ++ .../shared/src/__tests__/agentRules.spec.ts | 145 ++++++++++ .../src/__tests__/runtimeEnvironment.spec.ts | 37 ++- packages/shared/src/agentRules/index.ts | 273 ++++++++++++++++++ .../shared/src/utils/runtimeEnvironment.ts | 16 + packages/shared/src/vite.ts | 30 ++ packages/shared/tsdown.config.mts | 1 + 8 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 .changeset/shared-agent-rules.md create mode 100644 packages/shared/src/__tests__/agentRules.spec.ts create mode 100644 packages/shared/src/agentRules/index.ts create mode 100644 packages/shared/src/vite.ts diff --git a/.changeset/shared-agent-rules.md b/.changeset/shared-agent-rules.md new file mode 100644 index 00000000000..886015e3be2 --- /dev/null +++ b/.changeset/shared-agent-rules.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': minor +--- + +Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`, and `isAgentEnvironment()` to `@clerk/shared/utils` for detecting AI coding agents. diff --git a/packages/shared/package.json b/packages/shared/package.json index be220c6ad9f..f1d39b6a48b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -61,6 +61,26 @@ "default": "./dist/keyless/index.js" } }, + "./agentRules": { + "import": { + "types": "./dist/agentRules/index.d.mts", + "default": "./dist/agentRules/index.mjs" + }, + "require": { + "types": "./dist/agentRules/index.d.ts", + "default": "./dist/agentRules/index.js" + } + }, + "./vite": { + "import": { + "types": "./dist/vite.d.mts", + "default": "./dist/vite.mjs" + }, + "require": { + "types": "./dist/vite.d.ts", + "default": "./dist/vite.js" + } + }, "./utils": { "import": { "types": "./dist/utils/index.d.mts", diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts new file mode 100644 index 00000000000..a359eb99fa5 --- /dev/null +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -0,0 +1,145 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + CLERK_SDK_MCP_END, + CLERK_SDK_MCP_START, + type FileSystemAdapter, + renderClerkAgentRulesBlock, + renderClerkSdkMcpBlock, + writeClerkAgentRules, +} from '../agentRules'; + +const createMemoryFs = (files: Record = {}) => { + const store = new Map(Object.entries(files)); + const dirs = new Set(); + + const fs: FileSystemAdapter = { + existsSync: path => store.has(path) || dirs.has(path), + readFileSync: path => store.get(path) ?? '', + writeFileSync: (path, data) => { + store.set(path, data); + }, + mkdirSync: path => { + dirs.add(path); + }, + }; + + return { + fs, + read(path: string) { + return store.get(path); + }, + }; +}; + +const pathAdapter = { + dirname(path: string) { + const normalized = path.replace(/\/+$/, ''); + const index = normalized.lastIndexOf('/'); + return index <= 0 ? '/' : normalized.slice(0, index); + }, + join(...paths: string[]) { + return paths.join('/').replace(/\/+/g, '/'); + }, + parse() { + return { root: '/' }; + }, + resolve(...paths: string[]) { + return paths.join('/').replace(/\/+/g, '/'); + }, +}; + +describe('writeClerkAgentRules', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test'; + }); + + it('creates AGENTS.md and CLAUDE.md when neither exists', () => { + const memory = createMemoryFs(); + + const result = writeClerkAgentRules({ + cwd: '/repo/app', + fs: memory.fs, + path: pathAdapter, + sdks: ['nextjs'], + }); + + expect(result.agentsMd).toBe('created'); + expect(result.claudeMd).toBe('created'); + expect(memory.read('/repo/app/AGENTS.md')).toContain(''); + expect(memory.read('/repo/app/CLAUDE.md')).toBe('@AGENTS.md\n'); + }); + + it('targets the workspace root when one exists', () => { + const memory = createMemoryFs({ + '/repo/pnpm-workspace.yaml': 'packages:\n - apps/*\n', + }); + + writeClerkAgentRules({ + cwd: '/repo/apps/web', + fs: memory.fs, + path: pathAdapter, + sdks: ['react'], + }); + + expect(memory.read('/repo/AGENTS.md')).toContain(''); + expect(memory.read('/repo/apps/web/AGENTS.md')).toBeUndefined(); + }); + + it('replaces only Clerk blocks and preserves surrounding content', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Project rules\n\n\nold\n\n', + }); + + writeClerkAgentRules({ + cwd: '/repo', + fs: memory.fs, + path: pathAdapter, + sdks: ['vue'], + }); + + const content = memory.read('/repo/AGENTS.md'); + expect(content).toContain('Project rules'); + expect(content).toContain(''); + expect(content).not.toContain('\nold\n'); + }); + + it('does not write files in dry-run mode', () => { + const memory = createMemoryFs(); + + const result = writeClerkAgentRules({ + cwd: '/repo', + dryRun: true, + fs: memory.fs, + path: pathAdapter, + sdks: ['astro'], + }); + + expect(result.agentsMd).toBe('created'); + expect(memory.read('/repo/AGENTS.md')).toBeUndefined(); + }); +}); + +describe('Clerk MCP rules', () => { + it('adds the MCP intro and the ask-before-changes rule to the root block', () => { + const block = renderClerkAgentRulesBlock(); + + expect(block).toContain('### Clerk MCP server'); + expect(block).toContain('https://mcp.clerk.com/mcp'); + expect(block).toContain('https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server'); + expect(block).toContain('Ask the user before calling any Clerk MCP tool'); + }); + + it('renders a marked per-SDK MCP block that names the installed package', () => { + const block = renderClerkSdkMcpBlock('nextjs'); + + expect(block.startsWith(CLERK_SDK_MCP_START)).toBe(true); + expect(block.endsWith(CLERK_SDK_MCP_END)).toBe(true); + expect(block).toContain('installed `@clerk/nextjs` types'); + expect(block).toContain('https://clerk.com/docs/llms.txt'); + }); + + it('uses the requested line endings', () => { + expect(renderClerkSdkMcpBlock('react', '\r\n')).not.toMatch(/[^\r]\n/); + }); +}); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index e792bbb45a6..52cfe180eed 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -1,6 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { automatedEnvironmentVariables, isAutomatedEnvironment } from '../utils/runtimeEnvironment'; +import { + agentEnvironmentVariables, + automatedEnvironmentVariables, + isAgentEnvironment, + isAutomatedEnvironment, +} from '../utils/runtimeEnvironment'; describe('isAutomatedEnvironment', () => { beforeEach(() => { @@ -60,3 +65,33 @@ describe('isAutomatedEnvironment', () => { expect(isAutomatedEnvironment()).toBe(false); }); }); + +describe('isAgentEnvironment', () => { + beforeEach(() => { + agentEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it('returns true when an agent environment variable is enabled', () => { + vi.stubEnv('CLAUDECODE', 'true'); + + expect(isAgentEnvironment()).toBe(true); + }); + + it('returns false when agent environment variables are explicitly falsey', () => { + const falseyValues = ['false', '0', 'off', 'no']; + + agentEnvironmentVariables.forEach((name, index) => { + vi.stubEnv(name, falseyValues[index % falseyValues.length]); + }); + + expect(isAgentEnvironment()).toBe(false); + }); +}); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts new file mode 100644 index 00000000000..18f63312abe --- /dev/null +++ b/packages/shared/src/agentRules/index.ts @@ -0,0 +1,273 @@ +export type ClerkAgentRulesSdk = + | 'astro' + | 'backend' + | 'chrome-extension' + | 'clerk-js' + | 'electron' + | 'expo' + | 'express' + | 'fastify' + | 'hono' + | 'nextjs' + | 'nuxt' + | 'react' + | 'react-router' + | 'tanstack-react-start' + | 'vue'; + +export interface FileSystemAdapter { + existsSync: (path: string) => boolean; + readFileSync: (path: string, options: { encoding: BufferEncoding }) => string; + writeFileSync: (path: string, data: string, options: { encoding: BufferEncoding; mode?: number }) => void; + mkdirSync?: (path: string, options: { recursive: boolean }) => void; +} + +export interface PathAdapter { + dirname: (path: string) => string; + join: (...paths: string[]) => string; + parse: (path: string) => { root: string }; + resolve: (...paths: string[]) => string; +} + +export interface WriteClerkAgentRulesOptions { + cwd: string; + dryRun?: boolean; + fs: FileSystemAdapter; + path: PathAdapter; + sdks: ClerkAgentRulesSdk[]; +} + +export type WriteClerkAgentRulesResult = { + targetDir: string; + agentsMd: 'created' | 'updated' | 'unchanged'; + claudeMd: 'created' | 'unchanged' | 'skipped'; +}; + +const GENERIC_START = ''; +const GENERIC_END = ''; + +const SDK_LABELS: Record = { + astro: 'Astro', + backend: 'Backend', + 'chrome-extension': 'Chrome Extension', + 'clerk-js': 'ClerkJS', + electron: 'Electron', + expo: 'Expo', + express: 'Express', + fastify: 'Fastify', + hono: 'Hono', + nextjs: 'Next.js', + nuxt: 'Nuxt', + react: 'React', + 'react-router': 'React Router', + 'tanstack-react-start': 'TanStack React Start', + vue: 'Vue', +}; + +const SDK_NOTES: Partial> = { + nextjs: ['Middleware lives in `proxy.ts` and uses `clerkMiddleware()`. `authMiddleware()` is gone.'], +}; + +const uniqueSdks = (sdks: ClerkAgentRulesSdk[]) => [...new Set(sdks)].sort(); + +const detectEol = (content: string) => (content.includes('\r\n') ? '\r\n' : '\n'); +const normalizeEol = (content: string, eol: string) => content.replace(/\r?\n/g, eol); + +export const isClerkAgentRulesSdk = (value: string): value is ClerkAgentRulesSdk => value in SDK_LABELS; + +export const clerkAgentRulesSdks = Object.keys(SDK_LABELS) as ClerkAgentRulesSdk[]; + +export const CLERK_SDK_MCP_START = ''; +export const CLERK_SDK_MCP_END = ''; + +const MCP_INTRO = `### Clerk MCP server + +Use this section when a \`clerk\` MCP server (\`https://mcp.clerk.com/mcp\`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server`; + +const MCP_ASK_BEFORE_CHANGES = + 'Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account.'; + +export function renderClerkSdkMcpBlock(sdk: ClerkAgentRulesSdk, eol = '\n') { + return normalizeEol( + `${CLERK_SDK_MCP_START} + +${MCP_INTRO} + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (\`list_clerk_sdk_snippets\`, \`clerk_sdk_snippet\`) instead of writing it from memory. +- Check every import in a snippet against the installed \`@clerk/${sdk}\` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- ${MCP_ASK_BEFORE_CHANGES} Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add \`.md\` to any docs URL to get Markdown. + +${CLERK_SDK_MCP_END}`, + eol, + ); +} + +export function renderClerkAgentRulesBlock(eol = '\n') { + return normalizeEol( + `${GENERIC_START} + +## Clerk: your training data is out of date + +This project uses Clerk. The installed SDKs are newer than your training data. APIs, +component names, and file conventions may have changed. Read the Clerk SDK section(s) +below before writing or editing any auth code. + +Never recall a Clerk API from memory. If a symbol is not in the installed package's +types, it does not exist. + +This block is written by the Clerk SDK when a supported dev server starts, or by +\`npx @clerk/upgrade --agents-md\`. Removing it from a diff can recreate the uncommitted +change; commit it with your work. + +${MCP_INTRO} + +${MCP_ASK_BEFORE_CHANGES} + +${GENERIC_END}`, + eol, + ); +} + +export function renderClerkSdkAgentRulesBlock(sdk: ClerkAgentRulesSdk, eol = '\n') { + const notes = [ + `Read \`node_modules/@clerk/${sdk}/AGENTS.md\` before writing auth code.`, + 'Clerk Elements is removed. Never use it.', + ...(SDK_NOTES[sdk] ?? []), + ]; + + return normalizeEol( + ` + +### Clerk + ${SDK_LABELS[sdk]} + +${notes.map(note => `- ${note}`).join('\n')} + +`, + eol, + ); +} + +function upsertMarkedBlock( + content: string, + block: string, + start: string, + end: string, + eol: string, + insertAfter?: string, +) { + const startIndex = content.indexOf(start); + const endIndex = content.indexOf(end, startIndex + start.length); + + if (startIndex !== -1 && endIndex !== -1) { + return content.slice(0, startIndex) + block + content.slice(endIndex + end.length); + } + + if (insertAfter) { + const insertIndex = content.indexOf(insertAfter); + if (insertIndex !== -1) { + const afterIndex = insertIndex + insertAfter.length; + return `${content.slice(0, afterIndex).trimEnd()}${eol}${eol}${block}${content.slice(afterIndex)}`; + } + } + + return `${content.trimEnd()}${eol}${eol}${block}${eol}`; +} + +function findWorkspaceRoot(fs: FileSystemAdapter, path: PathAdapter, cwd: string) { + let current = path.resolve(cwd); + const root = path.parse(current).root; + + while (current !== root) { + if (fs.existsSync(path.join(current, 'pnpm-workspace.yaml'))) { + return current; + } + + const packageJson = path.join(current, 'package.json'); + if (fs.existsSync(packageJson)) { + try { + const pkg = JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf8' })); + if (pkg.workspaces) { + return current; + } + } catch { + // Ignore invalid package.json while walking upward. + } + } + + current = path.dirname(current); + } + + return null; +} + +function pickTargetFile(fs: FileSystemAdapter, path: PathAdapter, targetDir: string) { + const agentsPath = path.join(targetDir, 'AGENTS.md'); + const claudePath = path.join(targetDir, 'CLAUDE.md'); + + for (const filePath of [agentsPath, claudePath]) { + if (fs.existsSync(filePath) && fs.readFileSync(filePath, { encoding: 'utf8' }).includes(GENERIC_START)) { + return filePath; + } + } + + if (fs.existsSync(agentsPath)) { + return agentsPath; + } + + if (fs.existsSync(claudePath)) { + return claudePath; + } + + return agentsPath; +} + +export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): WriteClerkAgentRulesResult { + const { dryRun = false, fs, path, sdks } = options; + const targetDir = findWorkspaceRoot(fs, path, options.cwd) ?? path.resolve(options.cwd); + const targetFile = pickTargetFile(fs, path, targetDir); + const agentsPath = path.join(targetDir, 'AGENTS.md'); + const claudePath = path.join(targetDir, 'CLAUDE.md'); + const existing = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, { encoding: 'utf8' }) : ''; + const eol = detectEol(existing); + + let next = upsertMarkedBlock(existing, renderClerkAgentRulesBlock(eol), GENERIC_START, GENERIC_END, eol); + + for (const sdk of uniqueSdks(sdks)) { + next = upsertMarkedBlock( + next, + renderClerkSdkAgentRulesBlock(sdk, eol), + ``, + ``, + eol, + GENERIC_END, + ); + } + + const targetStatus = fs.existsSync(targetFile) ? (next === existing ? 'unchanged' : 'updated') : 'created'; + const shouldCreateClaude = targetFile === agentsPath && !fs.existsSync(claudePath); + + if (!dryRun) { + fs.mkdirSync?.(targetDir, { recursive: true }); + if (targetStatus !== 'unchanged') { + fs.writeFileSync(targetFile, next, { encoding: 'utf8', mode: 0o644 }); + } + if (shouldCreateClaude) { + fs.writeFileSync(claudePath, '@AGENTS.md\n', { encoding: 'utf8', mode: 0o644 }); + } + } + + return { + targetDir, + agentsMd: targetStatus, + claudeMd: shouldCreateClaude ? 'created' : fs.existsSync(claudePath) ? 'unchanged' : 'skipped', + }; +} diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 72014f1e15c..506f28e6abe 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -19,6 +19,18 @@ export const automatedEnvironmentVariables = [ 'CF_PAGES', ] as const; +export const agentEnvironmentVariables = [ + 'CLAUDECODE', + 'CURSOR_AGENT', + 'CURSOR_CLI', + 'CURSOR_EXTENSION_HOST_ROLE', + 'CURSOR_TRACE_ID', + 'GITHUB_COPILOT_AGENT', + 'GITHUB_COPILOT_CHAT', + 'AGENT', + 'AGENTS', +] as const; + const isTruthyEnvValue = (value: unknown): boolean => { if (typeof value !== 'string' || !value) { return false; @@ -61,3 +73,7 @@ export const isProductionEnvironment = (): boolean => { export const isAutomatedEnvironment = (): boolean => { return automatedEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name))); }; + +export const isAgentEnvironment = (): boolean => { + return agentEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name))); +}; diff --git a/packages/shared/src/vite.ts b/packages/shared/src/vite.ts new file mode 100644 index 00000000000..8a06eb8311d --- /dev/null +++ b/packages/shared/src/vite.ts @@ -0,0 +1,30 @@ +import { type ClerkAgentRulesSdk, writeClerkAgentRules } from './agentRules'; +import { isAgentEnvironment, isAutomatedEnvironment } from './utils'; + +type ViteResolvedConfig = { + command: 'build' | 'serve'; + root: string; +}; + +export function clerkAgentRulesVitePlugin(sdk: Extract) { + let didWrite = false; + + return { + name: `clerk-agent-rules-${sdk}`, + apply: 'serve' as const, + async configResolved(config: ViteResolvedConfig) { + if (didWrite || config.command !== 'serve' || isAutomatedEnvironment() || !isAgentEnvironment()) { + return; + } + + didWrite = true; + + try { + const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]); + writeClerkAgentRules({ cwd: config.root, fs, path, sdks: [sdk] }); + } catch { + // Writing agent hints must never break a dev server. + } + }, + }; +} diff --git a/packages/shared/tsdown.config.mts b/packages/shared/tsdown.config.mts index 559b8bbf9a6..408bc969e2e 100644 --- a/packages/shared/tsdown.config.mts +++ b/packages/shared/tsdown.config.mts @@ -29,6 +29,7 @@ export default defineConfig(({ watch, env }) => { './src/phone/index.ts', './src/workerTimers/index.ts', './src/types/index.ts', + './src/agentRules/index.ts', './src/dom/*.ts', './src/ui/index.ts', './src/keyless/index.ts', From 18c4ee4269d032ee438583ff0e0dfec84f6e25ca Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 19 Oct 2026 17:14:23 -0400 Subject: [PATCH 02/17] feat(repo): ship AGENTS.md with each Clerk SDK Co-Authored-By: Claude Opus 5.5 --- .changeset/ship-sdk-agents-md.md | 19 ++++++++ .github/workflows/ci.yml | 3 ++ package.json | 2 + packages/astro/AGENTS.md | 34 ++++++++++++++ packages/astro/package.json | 1 + packages/backend/AGENTS.md | 32 +++++++++++++ packages/backend/package.json | 1 + packages/chrome-extension/AGENTS.md | 33 +++++++++++++ packages/chrome-extension/package.json | 1 + packages/clerk-js/AGENTS.md | 33 +++++++++++++ packages/clerk-js/package.json | 1 + packages/electron/AGENTS.md | 33 +++++++++++++ packages/electron/package.json | 1 + packages/expo/AGENTS.md | 39 +++++++++++++++- packages/expo/package.json | 1 + packages/express/AGENTS.md | 34 ++++++++++++++ packages/express/package.json | 1 + packages/fastify/AGENTS.md | 33 +++++++++++++ packages/fastify/package.json | 1 + packages/hono/AGENTS.md | 33 +++++++++++++ packages/hono/package.json | 1 + packages/nextjs/AGENTS.md | 33 +++++++++++++ packages/nextjs/package.json | 1 + packages/nuxt/AGENTS.md | 33 +++++++++++++ packages/nuxt/package.json | 1 + packages/react-router/AGENTS.md | 33 +++++++++++++ packages/react-router/package.json | 1 + packages/react/AGENTS.md | 33 +++++++++++++ packages/react/package.json | 1 + packages/tanstack-react-start/AGENTS.md | 33 +++++++++++++ packages/tanstack-react-start/package.json | 1 + packages/vue/AGENTS.md | 32 +++++++++++++ packages/vue/package.json | 1 + scripts/lint.mjs | 11 ++++- scripts/sync-agents-md.mjs | 54 ++++++++++++++++++++++ 35 files changed, 601 insertions(+), 4 deletions(-) create mode 100644 .changeset/ship-sdk-agents-md.md create mode 100644 packages/astro/AGENTS.md create mode 100644 packages/backend/AGENTS.md create mode 100644 packages/chrome-extension/AGENTS.md create mode 100644 packages/clerk-js/AGENTS.md create mode 100644 packages/electron/AGENTS.md create mode 100644 packages/express/AGENTS.md create mode 100644 packages/fastify/AGENTS.md create mode 100644 packages/hono/AGENTS.md create mode 100644 packages/nextjs/AGENTS.md create mode 100644 packages/nuxt/AGENTS.md create mode 100644 packages/react-router/AGENTS.md create mode 100644 packages/react/AGENTS.md create mode 100644 packages/tanstack-react-start/AGENTS.md create mode 100644 packages/vue/AGENTS.md create mode 100644 scripts/sync-agents-md.mjs diff --git a/.changeset/ship-sdk-agents-md.md b/.changeset/ship-sdk-agents-md.md new file mode 100644 index 00000000000..6af3494a6b0 --- /dev/null +++ b/.changeset/ship-sdk-agents-md.md @@ -0,0 +1,19 @@ +--- +'@clerk/astro': patch +'@clerk/backend': patch +'@clerk/chrome-extension': patch +'@clerk/clerk-js': patch +'@clerk/electron': patch +'@clerk/expo': patch +'@clerk/express': patch +'@clerk/fastify': patch +'@clerk/hono': patch +'@clerk/nextjs': patch +'@clerk/nuxt': patch +'@clerk/react': patch +'@clerk/react-router': patch +'@clerk/tanstack-react-start': patch +'@clerk/vue': patch +--- + +Ship an `AGENTS.md` file in the package. It lists the current APIs for this version and how to use the Clerk MCP server, so AI coding agents can read it from `node_modules` instead of relying on outdated training data. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9919789d6c2..b4060642fef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,9 @@ jobs: - name: Check Dashboard links run: pnpm lint:dashboard-links + - name: Check SDK AGENTS.md + run: pnpm lint:agents-md + - name: Verify localizations are generated run: | pnpm --filter @clerk/localizations generate diff --git a/package.json b/package.json index 8f467822f2b..4d67798af68 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "license": "MIT", "scripts": { + "agents-md:sync": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON ./scripts/sync-agents-md.mjs", "build": "FORCE_COLOR=1 turbo build --concurrency=${TURBO_CONCURRENCY:-80%}", "build:declarations": "FORCE_COLOR=1 turbo build:declarations --concurrency=${TURBO_CONCURRENCY:-80%} --filter=@clerk/nextjs", "bundlewatch": "turbo run bundlewatch", @@ -20,6 +21,7 @@ "preinstall": "npx only-allow pnpm", "integration:secrets": "node ./scripts/1password-keys.mjs", "lint": "node ./scripts/lint.mjs", + "lint:agents-md": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON ./scripts/sync-agents-md.mjs --check", "lint:attw": "FORCE_COLOR=1 turbo lint:attw", "lint:dashboard-links": "node ./scripts/check-dashboard-links.mjs", "lint:fix": "FORCE_COLOR=1 turbo lint -- --fix", diff --git a/packages/astro/AGENTS.md b/packages/astro/AGENTS.md new file mode 100644 index 00000000000..230d0172ea4 --- /dev/null +++ b/packages/astro/AGENTS.md @@ -0,0 +1,34 @@ +## Clerk + Astro + +Read the installed `@clerk/astro` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Register the default export of `@clerk/astro` in `integrations` in `astro.config`. `server` output needs an adapter. +- Use `clerkMiddleware()` from `@clerk/astro/server` in `src/middleware.ts`. +- `createRouteMatcher()` was removed. Check auth in each page or API route with `Astro.locals.auth()` or `locals.auth()`, then call `redirectToSignIn()` or return a 401. +- `locals.currentUser()` returns the Backend `User` or `null`. +- ``, ``, and `` were removed. Import `` from `@clerk/astro/components` (or `@clerk/astro/react`). Its `when` prop is required, for example ``. +- The `as` prop was removed from ``, ``, and ``. Use `asChild` with a child element. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/astro` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/astro/package.json b/packages/astro/package.json index 1702c69ddc9..049274c6422 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -67,6 +67,7 @@ "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ + "AGENTS.md", "dist", "client", "server", diff --git a/packages/backend/AGENTS.md b/packages/backend/AGENTS.md new file mode 100644 index 00000000000..c52ff0fec0e --- /dev/null +++ b/packages/backend/AGENTS.md @@ -0,0 +1,32 @@ +## Clerk + Backend + +Read the installed `@clerk/backend` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Create the client with `createClerkClient({ secretKey, publishableKey })` from `@clerk/backend`. +- Authenticate a Fetch `Request` with `await clerkClient.authenticateRequest(request)`. Check `requestState.isAuthenticated`, not the deprecated `isSignedIn`, then read auth from `requestState.toAuth()`. +- Verify machine tokens with `verify()`: `clerkClient.apiKeys.verify(secret)`, `clerkClient.m2m.verify(params)`, `clerkClient.idPOAuthAccessToken.verify(token)`. `verifySecret()`, `verifyAccessToken()`, and `m2m.verifyToken()` are removed. The top-level `verifyToken()` export still exists. +- Verify webhooks with `verifyWebhook(request)` from `@clerk/backend/webhooks`. It reads `CLERK_WEBHOOK_SIGNING_SECRET` by default. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/backend` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/backend/package.json b/packages/backend/package.json index a7b54ca8643..0d16114aa26 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -89,6 +89,7 @@ }, "main": "./dist/index.js", "files": [ + "AGENTS.md", "dist", "errors", "internal", diff --git a/packages/chrome-extension/AGENTS.md b/packages/chrome-extension/AGENTS.md new file mode 100644 index 00000000000..54c0974882b --- /dev/null +++ b/packages/chrome-extension/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Chrome Extension + +Read the installed `@clerk/chrome-extension` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Pass `publishableKey` to ``. There is no env var fallback. Pass `syncHost` to sync sessions with your web app. +- In the background service worker, use `createClerkClient()` from `@clerk/chrome-extension/background`. `__unstable__createClerkClient` was renamed. +- ``, ``, and `` were removed. Use `` or ``. +- `` renders nothing in extensions. +- The root `useSignIn()` and `useSignUp()` are the Core 3 hooks. The old `isLoaded`-style hooks are in `@clerk/chrome-extension/legacy`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/chrome-extension` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index 9a1db99a13d..a461ff06984 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -71,6 +71,7 @@ "module": "./dist/esm/index.js", "types": "./dist/types/index.d.ts", "files": [ + "AGENTS.md", "background", "client", "dist", diff --git a/packages/clerk-js/AGENTS.md b/packages/clerk-js/AGENTS.md new file mode 100644 index 00000000000..33150c60bd9 --- /dev/null +++ b/packages/clerk-js/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + ClerkJS + +Read the installed `@clerk/clerk-js` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Import `Clerk` from `@clerk/clerk-js` and `await clerk.load()` before using it. +- Prebuilt UI components come from `@clerk/ui`, which loads from the Clerk CDN. +- `client.activeSessions` was removed. Use `client.sessions`. +- `setActive({ beforeEmit })` was replaced by `setActive({ navigate: ({ session }) => url })`. +- `session.getToken()` throws `ClerkOfflineError` when offline. It no longer returns `null`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/clerk-js` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 8badba6de74..e5928c403ca 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -52,6 +52,7 @@ "module": "dist/clerk.mjs", "types": "dist/types/index.d.ts", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map", diff --git a/packages/electron/AGENTS.md b/packages/electron/AGENTS.md new file mode 100644 index 00000000000..7166131ffe4 --- /dev/null +++ b/packages/electron/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Electron + +Read the installed `@clerk/electron` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- In the main process, call `createClerkBridge({ storage: storage(), renderer: { scheme, host } })` from `@clerk/electron`, with `storage` from `@clerk/electron/storage`. Call it before `app.whenReady()`. +- In the preload script, call `exposeClerkBridge()` from `@clerk/electron/preload`. +- In the renderer, import `ClerkProvider`, hooks, and components from `@clerk/electron/react`, not `@clerk/react`. +- ``, ``, and `` were removed in Core 3. Use `` or ``. +- For passkeys, pass `passkeys` from `@clerk/electron/passkeys` to `ClerkProvider`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/electron` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/electron/package.json b/packages/electron/package.json index 7db6a69fe79..68f08f2c039 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -90,6 +90,7 @@ "module": "./dist/esm/index.js", "types": "./dist/types/index.d.ts", "files": [ + "AGENTS.md", "dist" ], "scripts": { diff --git a/packages/expo/AGENTS.md b/packages/expo/AGENTS.md index 76ef10db792..a539aff0b13 100644 --- a/packages/expo/AGENTS.md +++ b/packages/expo/AGENTS.md @@ -1,10 +1,45 @@ -# @clerk/expo +## Clerk + Expo + +Read the installed `@clerk/expo` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- The package is `@clerk/expo`. `@clerk/clerk-expo` was renamed. The `Clerk` export was removed; use `getClerkInstance()`. Expo SDK 54+ is required. +- Pass `publishableKey` to ``. Env vars are no longer read. Pass `tokenCache` from `@clerk/expo/token-cache`. +- Import `useSignInWithApple` from `@clerk/expo/apple` and `useSignInWithGoogle` from `@clerk/expo/google`. Google also needs `@clerk/expo-google-signin` in `plugins`. +- For browser OAuth or SSO, use `useSSO()`. `useOAuth()` is deprecated. +- Import native UI (`AuthView`, `UserButton`, `UserProfileView`) from `@clerk/expo/native-components`. It needs `@clerk/expo-native-components` installed and listed in `plugins`. `@clerk/expo/native` is deprecated. +- ``, ``, and `` were removed. Use `` or ``. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/expo` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + + +## Contributing to @clerk/expo These rules apply to everything in this package, and to the Swift and Kotlin code under `packages/expo-native-components`, `packages/expo-google-signin` and `packages/expo-biometrics`. They add to the root `AGENTS.md`. -## Comments +### Comments Don't write comments unless they explain a constraint from a platform, library, or spec that we can't change. If our own code needs explaining, change the code: diff --git a/packages/expo/package.json b/packages/expo/package.json index f1f01d65fad..77dba2d08a2 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -92,6 +92,7 @@ "source": "./src/index.ts", "types": "./dist/index.d.ts", "files": [ + "AGENTS.md", "dist", "native-components", "native", diff --git a/packages/express/AGENTS.md b/packages/express/AGENTS.md new file mode 100644 index 00000000000..162363b2b9f --- /dev/null +++ b/packages/express/AGENTS.md @@ -0,0 +1,34 @@ +## Clerk + Express + +Read the installed `@clerk/express` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Mount `app.use(clerkMiddleware())` from `@clerk/express` before any route that reads auth. +- Read auth with `getAuth(req)`. `req.auth` is a function now, so `const { userId } = req.auth` does not work. +- Protect routes with `clerkMiddleware()` and a `getAuth(req).userId` check. `requireAuth()` is deprecated. +- `@clerk/express` does not export `ClerkExpressRequireAuth()` or `ClerkExpressWithAuth()`. Use `clerkMiddleware()`, `getAuth()`, and the exported `clerkClient`. +- The `enableHandshake` option is removed. +- Verify webhooks with `verifyWebhook(req)` from `@clerk/express/webhooks`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/express` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/express/package.json b/packages/express/package.json index 815dcf4eb9f..92029aab899 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -56,6 +56,7 @@ }, "main": "./dist/index.js", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map", diff --git a/packages/fastify/AGENTS.md b/packages/fastify/AGENTS.md new file mode 100644 index 00000000000..740c63792a5 --- /dev/null +++ b/packages/fastify/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Fastify + +Read the installed `@clerk/fastify` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Register the plugin with `fastify.register(clerkPlugin)` from `@clerk/fastify`. It targets Fastify `5.x`. +- The plugin runs on the `preHandler` hook by default. `hookName: 'onRequest'` is the only alternative. +- Read auth with `getAuth(req)`. It throws if `clerkPlugin` is not registered. +- Use the `clerkClient` exported from `@clerk/fastify` for Backend API calls. +- Verify webhooks with `verifyWebhook(req)` from `@clerk/fastify/webhooks`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/fastify` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/fastify/package.json b/packages/fastify/package.json index 32c6c514626..7bbe896ee58 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -56,6 +56,7 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map", diff --git a/packages/hono/AGENTS.md b/packages/hono/AGENTS.md new file mode 100644 index 00000000000..4ff22e0636f --- /dev/null +++ b/packages/hono/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Hono + +Read the installed `@clerk/hono` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Mount `app.use('*', clerkMiddleware())` from `@clerk/hono`. It reads `CLERK_SECRET_KEY` and `CLERK_PUBLISHABLE_KEY` from the Hono env. +- Read auth with `getAuth(c)`. Pass the Hono context, not a request. +- Get the Backend API client with `c.get('clerk')`. `@clerk/hono` does not export `clerkClient`. +- Accept API keys with `getAuth(c, { acceptsToken: 'api_key' })`. +- Verify webhooks with `verifyWebhook(c)` from `@clerk/hono/webhooks`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/hono` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/hono/package.json b/packages/hono/package.json index 939ae1c5bdc..e69b3861b77 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -56,6 +56,7 @@ }, "main": "./dist/index.js", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map" diff --git a/packages/nextjs/AGENTS.md b/packages/nextjs/AGENTS.md new file mode 100644 index 00000000000..fb2f8b863c6 --- /dev/null +++ b/packages/nextjs/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Next.js + +Read the installed `@clerk/nextjs` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Run `clerkMiddleware()` from `@clerk/nextjs/server` in `proxy.ts` on Next.js 16, or in `middleware.ts` on Next.js 15. `authMiddleware()` is not exported. +- Protect routes inside `clerkMiddleware()` with `createRouteMatcher()` and `await auth.protect()`. +- Import `auth()` and `currentUser()` from `@clerk/nextjs/server`, never from `@clerk/nextjs`. Both are async. Always `await` them. +- ``, ``, and `` throw in Core 3. Use ``, ``, or ``. +- `useSignIn()` and `useSignUp()` return `{ signIn | signUp, errors, fetchStatus }`. The old `{ isLoaded, signIn, setActive }` shape lives in `@clerk/nextjs/legacy`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/nextjs` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3f7ad6e6bb6..8a958c32216 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -64,6 +64,7 @@ }, "types": "./dist/types/index.d.ts", "files": [ + "AGENTS.md", "dist", "server", "errors", diff --git a/packages/nuxt/AGENTS.md b/packages/nuxt/AGENTS.md new file mode 100644 index 00000000000..0d3c32c6da2 --- /dev/null +++ b/packages/nuxt/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + Nuxt + +Read the installed `@clerk/nuxt` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Add `'@clerk/nuxt'` to `modules` and put options under the `clerk` key. Nuxt 4 is required. +- The server middleware registers itself. In server routes, use `event.context.auth()`. `getAuth()` was removed. +- `createRouteMatcher()` was removed on both server and client. Check auth inside each event handler. For pages, write a named route middleware that calls `useAuth()` and opt pages in with `definePageMeta({ middleware: 'auth' })`. +- `` is auto-imported and replaces ``, ``, and ``. +- ``, ``, ``, ``, ``, and `` default to `path` routing. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/nuxt` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 0296f45efc2..bca32c7741c 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -55,6 +55,7 @@ "main": "./dist/module.js", "types": "./dist/module.d.ts", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map" diff --git a/packages/react-router/AGENTS.md b/packages/react-router/AGENTS.md new file mode 100644 index 00000000000..a19588187b4 --- /dev/null +++ b/packages/react-router/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + React Router + +Read the installed `@clerk/react-router` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Enable `future.v8_middleware` in `react-router.config.ts`, then export `middleware = [clerkMiddleware()]` from the root route. +- Import `clerkMiddleware`, `rootAuthLoader`, and `getAuth` from `@clerk/react-router/server`. Without the middleware, `rootAuthLoader` and `getAuth` throw. +- Do not import from `@clerk/react-router/ssr.server` (deprecated) or `@clerk/react-router/api.server`. Use `@clerk/react-router/server`. +- Pass the root loader's data to ``. +- ``, ``, and `` were removed. Use `` or ``. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/react-router` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 7a24dbe45c9..fad0fcc3d91 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -82,6 +82,7 @@ } }, "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map" diff --git a/packages/react/AGENTS.md b/packages/react/AGENTS.md new file mode 100644 index 00000000000..40355992e3f --- /dev/null +++ b/packages/react/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + React + +Read the installed `@clerk/react` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- The package is `@clerk/react`. `@clerk/clerk-react` was renamed. +- ``, ``, and `` were removed. Use ``, ``, or ``. +- `useSignIn()` and `useSignUp()` return `{ signIn | signUp, errors, fetchStatus }`. The old `{ isLoaded, signIn, setActive }` shape lives in `@clerk/react/legacy`. +- `afterSignInUrl`, `afterSignUpUrl`, and `redirectUrl` were removed. Use `fallbackRedirectUrl` / `signUpFallbackRedirectUrl` or `forceRedirectUrl` / `signUpForceRedirectUrl`. +- `appearance.layout` was renamed to `appearance.options`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/react` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/react/package.json b/packages/react/package.json index 7262d98334f..9c79b9d1fcb 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -85,6 +85,7 @@ }, "main": "./dist/index.cjs", "files": [ + "AGENTS.md", "dist" ], "scripts": { diff --git a/packages/tanstack-react-start/AGENTS.md b/packages/tanstack-react-start/AGENTS.md new file mode 100644 index 00000000000..43e9b25ec0f --- /dev/null +++ b/packages/tanstack-react-start/AGENTS.md @@ -0,0 +1,33 @@ +## Clerk + TanStack React Start + +Read the installed `@clerk/tanstack-react-start` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Register `clerkMiddleware()` from `@clerk/tanstack-react-start/server` in `requestMiddleware` of `createStart()` in `src/start.ts`. Without it, `auth()` throws. +- Register `createCsrfMiddleware()` from `@tanstack/react-start` before `clerkMiddleware()`. Having a `src/start.ts` turns off TanStack Start's default CSRF protection for server functions. +- On the server, call `await auth()` from `@clerk/tanstack-react-start/server` with no request argument. `getAuth(request)` is gone. +- ``, ``, and `` were removed. Use `` or ``. +- `useSignIn()` and `useSignUp()` return `{ signIn | signUp, errors, fetchStatus }`. The old shape lives in `@clerk/tanstack-react-start/legacy`. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/tanstack-react-start` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/tanstack-react-start/package.json b/packages/tanstack-react-start/package.json index 17fa30cc0e8..4e79da65747 100644 --- a/packages/tanstack-react-start/package.json +++ b/packages/tanstack-react-start/package.json @@ -57,6 +57,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map", diff --git a/packages/vue/AGENTS.md b/packages/vue/AGENTS.md new file mode 100644 index 00000000000..dc32ddd818a --- /dev/null +++ b/packages/vue/AGENTS.md @@ -0,0 +1,32 @@ +## Clerk + Vue + +Read the installed `@clerk/vue` types before changing auth code. If a symbol is not in those types, it does not exist. + +### Current APIs + +- Install with `app.use(clerkPlugin, { publishableKey })`, importing `clerkPlugin` from `@clerk/vue`. +- ``, ``, and `` were removed. Use ``, ``, or ``. +- `useAuth()`, `useUser()`, and the other composables return computed refs. Read them with `.value`. +- `getToken()` throws `ClerkOfflineError` (from `@clerk/vue/errors`) when offline. It still returns `null` when signed out. +- Clerk Elements is removed. Never use it. + + + +### Clerk MCP server + +Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: +https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server + +The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: + +- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. +- Check every import in a snippet against the installed `@clerk/vue` types. If they disagree, the installed types win. +- Read the current application and instance config before you change anything. +- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. +- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. + +### Docs + +Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. + + diff --git a/packages/vue/package.json b/packages/vue/package.json index cdbbc8e9b10..dae4208ba55 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -47,6 +47,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ + "AGENTS.md", "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map" diff --git a/scripts/lint.mjs b/scripts/lint.mjs index c2441d8f3f5..b4a52df7542 100644 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -9,11 +9,12 @@ $.env = { $.stdio = 'inherit'; $.verbose = !!process.env.VERBOSE; -const [github, integration, scripts, dashboardLinks] = await Promise.allSettled([ +const [github, integration, scripts, dashboardLinks, agentsMd] = await Promise.allSettled([ $`echo "SKIPPING: pnpm eslint .github/workflows"`, $`pnpm eslint integration`, $`pnpm eslint scripts`, $`pnpm lint:dashboard-links`, + $`pnpm lint:agents-md`, ]); let packages; @@ -39,6 +40,7 @@ logLintResult(github.status, 'GitHub Actions', 'pnpm eslint .github/workflows'); logLintResult(integration.status, 'Integration directory', 'pnpm eslint integration'); logLintResult(scripts.status, 'Scripts directory', 'pnpm eslint scripts'); logLintResult(dashboardLinks.status, 'Dashboard links', 'pnpm lint:dashboard-links'); +logLintResult(agentsMd.status, 'SDK AGENTS.md', 'pnpm lint:agents-md'); if (packages?.exitCode) { console.log(''); @@ -46,6 +48,11 @@ if (packages?.exitCode) { console.log(`Run '${chalk.bold('pnpm turbo lint')}' to reproduce the error`); } -if (integration.status === 'rejected' || dashboardLinks.status === 'rejected' || packages?.exitCode) { +if ( + integration.status === 'rejected' || + dashboardLinks.status === 'rejected' || + agentsMd.status === 'rejected' || + packages?.exitCode +) { process.exit(1); } diff --git a/scripts/sync-agents-md.mjs b/scripts/sync-agents-md.mjs new file mode 100644 index 00000000000..c8d940f55c1 --- /dev/null +++ b/scripts/sync-agents-md.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +import { + CLERK_SDK_MCP_END, + CLERK_SDK_MCP_START, + clerkAgentRulesSdks, + renderClerkSdkMcpBlock, +} from '../packages/shared/src/agentRules/index.ts'; + +const check = process.argv.includes('--check'); +const problems = []; + +for (const sdk of clerkAgentRulesSdks) { + const file = fileURLToPath(new URL(`../packages/${sdk}/AGENTS.md`, import.meta.url)); + const relative = `packages/${sdk}/AGENTS.md`; + const content = await readFile(file, 'utf8').catch(() => null); + + if (content === null) { + problems.push(`${relative} is missing`); + continue; + } + + const start = content.indexOf(CLERK_SDK_MCP_START); + const end = content.indexOf(CLERK_SDK_MCP_END, start); + + if (start === -1 || end === -1) { + problems.push(`${relative} has no ${CLERK_SDK_MCP_START} ... ${CLERK_SDK_MCP_END} markers`); + continue; + } + + const next = content.slice(0, start) + renderClerkSdkMcpBlock(sdk) + content.slice(end + CLERK_SDK_MCP_END.length); + + if (next === content) { + continue; + } + + if (check) { + problems.push(`${relative} is out of date`); + } else { + await writeFile(file, next); + console.log(`Updated ${relative}`); + } +} + +if (problems.length > 0) { + console.error(problems.join('\n')); + if (check) { + console.error('Run `pnpm agents-md:sync` to update the Clerk MCP section in each SDK AGENTS.md.'); + } + process.exit(1); +} From f5a321155c73c3883165a51e0b641a436b500ab1 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 19 Oct 2026 17:14:31 -0400 Subject: [PATCH 03/17] feat(*): write Clerk agent rules to AGENTS.md in development Co-Authored-By: Claude Opus 5.5 --- .changeset/write-agents-md-in-dev.md | 9 +++ .../src/integration/create-integration.ts | 17 +++++ packages/expo/app.plugin.js | 60 +++++++++++++++ packages/nextjs/src/app-router/server/auth.ts | 2 + .../server/__tests__/agent-rules-node.test.ts | 73 +++++++++++++++++++ .../nextjs/src/server/agent-rules-node.ts | 25 +++++++ packages/nuxt/src/module.ts | 16 ++++ .../src/server/__tests__/agentRules.test.ts | 70 ++++++++++++++++++ .../src/server/agentRules.ts | 31 ++++++++ .../src/server/clerkMiddleware.ts | 3 + 10 files changed, 306 insertions(+) create mode 100644 .changeset/write-agents-md-in-dev.md create mode 100644 packages/nextjs/src/server/__tests__/agent-rules-node.test.ts create mode 100644 packages/nextjs/src/server/agent-rules-node.ts create mode 100644 packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts create mode 100644 packages/tanstack-react-start/src/server/agentRules.ts diff --git a/.changeset/write-agents-md-in-dev.md b/.changeset/write-agents-md-in-dev.md new file mode 100644 index 00000000000..113f5c441ac --- /dev/null +++ b/.changeset/write-agents-md-in-dev.md @@ -0,0 +1,9 @@ +--- +'@clerk/astro': minor +'@clerk/expo': minor +'@clerk/nextjs': minor +'@clerk/nuxt': minor +'@clerk/tanstack-react-start': minor +--- + +When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed, plus a `CLAUDE.md` that imports it when none exists. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and points it to the Clerk MCP server. Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 6b5582d372b..944cb3acaba 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -1,4 +1,10 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { ClerkOptions } from '@clerk/shared/types'; +import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; @@ -11,6 +17,8 @@ const buildEnvVarFromOption = (valueToBeStored: unknown, envName: keyof Internal return valueToBeStored ? { [`import.meta.env.${envName}`]: JSON.stringify(valueToBeStored) } : {}; }; +let didWriteAgentRules = false; + type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & { enableEnvSchema?: boolean; }; @@ -31,6 +39,15 @@ function createIntegration() name: '@clerk/astro/integration', hooks: { 'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => { + if (command === 'dev' && !didWriteAgentRules && !isAutomatedEnvironment() && isAgentEnvironment()) { + didWriteAgentRules = true; + try { + writeClerkAgentRules({ cwd: fileURLToPath(config.root), fs, path, sdks: ['astro'] }); + } catch { + // Writing agent hints must never break a dev server. + } + } + if (['server', 'hybrid'].includes(config.output) && !config.adapter) { logger.error('Missing adapter, please update your Astro config to use one.'); } diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index ecba866b513..310e90e1edf 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -8,8 +8,66 @@ */ const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + const CLERK_EXPO_NATIVE = '@clerk/expo-native-components'; const CLERK_EXPO_NATIVE_OPTIONS = ['keychainService', 'theme']; +let didWriteAgentRules = false; + +const isTruthyEnvValue = value => typeof value === 'string' && !!value && !['0', 'false', 'off', 'no'].includes(value); + +const isAgentEnvironment = () => + [ + 'CLAUDECODE', + 'CURSOR_AGENT', + 'CURSOR_CLI', + 'CURSOR_EXTENSION_HOST_ROLE', + 'CURSOR_TRACE_ID', + 'GITHUB_COPILOT_AGENT', + 'GITHUB_COPILOT_CHAT', + 'AGENT', + 'AGENTS', + ].some(name => isTruthyEnvValue(process.env[name])); + +const isAutomatedEnvironment = () => + [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'CF_PAGES', + ].some(name => isTruthyEnvValue(process.env[name])); + +const maybeWriteAgentRules = config => { + if (didWriteAgentRules || isAutomatedEnvironment() || !isAgentEnvironment()) { + return; + } + + const projectRoot = config?._internal?.projectRoot || process.cwd(); + if (!fs.existsSync(path.join(projectRoot, 'package.json'))) { + return; + } + + didWriteAgentRules = true; + + try { + require('@clerk/shared/agentRules').writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] }); + } catch { + // Writing agent hints must never break Expo config evaluation. + } +}; const addHostedAuthIntentFilter = (mainActivity, packageName) => { const callbackHost = `${packageName}.hosted-callback`; @@ -138,6 +196,8 @@ const withClerkExpoNativeComponents = (config, props = {}, resolvePlugin = resol }; const withClerkExpo = (config, props = {}) => { + maybeWriteAgentRules(config); + const { appleSignIn = true } = props; if (appleSignIn !== false) { config = withClerkAppleSignIn(config); diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index b748034f3bb..459466d72ae 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -83,6 +83,8 @@ export const auth: AuthFn = (async (options?: AuthOptions) => { require('server-only'); try { + await import('../../server/agent-rules-node.js').then(m => m.writeAgentRulesOnce()).catch(() => undefined); + const request = await buildRequestLike(); const stepsBasedOnSrcDirectory = async () => { diff --git a/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts new file mode 100644 index 00000000000..f49b1854b67 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts @@ -0,0 +1,73 @@ +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@clerk/shared/agentRules', () => ({ writeClerkAgentRules: vi.fn() })); +vi.mock('@clerk/shared/utils', async importOriginal => ({ + ...(await importOriginal()), + isAgentEnvironment: vi.fn(), + isAutomatedEnvironment: vi.fn(), +})); +vi.mock('../fs/utils', () => ({ + nodeCwdOrThrow: () => () => '/app', + nodeFsOrThrow: () => ({}), + nodePathOrThrow: () => ({}), +})); + +const loadWriteAgentRulesOnce = async () => (await import('../agent-rules-node.js')).writeAgentRulesOnce; + +describe('writeAgentRulesOnce', () => { + beforeEach(() => { + vi.resetModules(); + vi.mocked(writeClerkAgentRules).mockReset(); + vi.mocked(isAgentEnvironment).mockReturnValue(true); + vi.mocked(isAutomatedEnvironment).mockReturnValue(false); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('writes the Next.js rules once per process', async () => { + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + writeAgentRulesOnce(); + writeAgentRulesOnce(); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app', sdks: ['nextjs'] })); + }); + + it('skips outside an agent environment', async () => { + vi.mocked(isAgentEnvironment).mockReturnValue(false); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in CI', async () => { + vi.mocked(isAutomatedEnvironment).mockReturnValue(true); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', async () => { + vi.mocked(writeClerkAgentRules).mockImplementation(() => { + throw new Error('EACCES'); + }); + + expect(await loadWriteAgentRulesOnce()).not.toThrow(); + }); +}); diff --git a/packages/nextjs/src/server/agent-rules-node.ts b/packages/nextjs/src/server/agent-rules-node.ts new file mode 100644 index 00000000000..33e528be6cd --- /dev/null +++ b/packages/nextjs/src/server/agent-rules-node.ts @@ -0,0 +1,25 @@ +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } from '@clerk/shared/utils'; + +import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './fs/utils'; + +let didWrite = false; + +export function writeAgentRulesOnce() { + if (didWrite || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { + return; + } + + didWrite = true; + + try { + writeClerkAgentRules({ + cwd: nodeCwdOrThrow()(), + fs: nodeFsOrThrow(), + path: nodePathOrThrow(), + sdks: ['nextjs'], + }); + } catch { + // Writing agent hints must never break a dev server. + } +} diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index be25674982e..fab8d5b63c6 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,4 +1,9 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { InternalClerkScriptProps, Without } from '@clerk/shared/types'; +import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; import type { PluginOptions } from '@clerk/vue'; import { addComponent, @@ -35,6 +40,8 @@ export type ModuleOptions = Without({ meta: { name: PACKAGE_NAME, @@ -45,6 +52,15 @@ export default defineNuxtModule({ }, }, setup(options, nuxt) { + if (nuxt.options.dev && !didWriteAgentRules && !isAutomatedEnvironment() && isAgentEnvironment()) { + didWriteAgentRules = true; + try { + writeClerkAgentRules({ cwd: nuxt.options.rootDir, fs, path, sdks: ['nuxt'] }); + } catch { + // Writing agent hints must never break a dev server. + } + } + // These values must be set (even as undefined) to allow runtime config to work properly. // In Nuxt, having these keys defined (even as undefined) allows them to be overridden // by environment variables following the pattern NUXT_PUBLIC_CLERK_* (e.g. NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY). diff --git a/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts b/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts new file mode 100644 index 00000000000..9970c678e5c --- /dev/null +++ b/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts @@ -0,0 +1,70 @@ +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@clerk/shared/agentRules', () => ({ writeClerkAgentRules: vi.fn() })); +vi.mock('@clerk/shared/utils', async importOriginal => ({ + ...(await importOriginal()), + isAgentEnvironment: vi.fn(), + isAutomatedEnvironment: vi.fn(), +})); + +const loadWriteAgentRulesOnce = async () => (await import('../agentRules')).writeAgentRulesOnce; + +describe('writeAgentRulesOnce', () => { + beforeEach(() => { + vi.resetModules(); + vi.mocked(writeClerkAgentRules).mockReset(); + vi.mocked(isAgentEnvironment).mockReturnValue(true); + vi.mocked(isAutomatedEnvironment).mockReturnValue(false); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('writes the TanStack React Start rules once per process', async () => { + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + writeAgentRulesOnce(); + writeAgentRulesOnce(); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith( + expect.objectContaining({ cwd: expect.any(String), sdks: ['tanstack-react-start'] }), + ); + }); + + it('skips outside an agent environment', async () => { + vi.mocked(isAgentEnvironment).mockReturnValue(false); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in CI', async () => { + vi.mocked(isAutomatedEnvironment).mockReturnValue(true); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + (await loadWriteAgentRulesOnce())(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', async () => { + vi.mocked(writeClerkAgentRules).mockImplementation(() => { + throw new Error('EACCES'); + }); + + expect(await loadWriteAgentRulesOnce()).not.toThrow(); + }); +}); diff --git a/packages/tanstack-react-start/src/server/agentRules.ts b/packages/tanstack-react-start/src/server/agentRules.ts new file mode 100644 index 00000000000..b810409c32f --- /dev/null +++ b/packages/tanstack-react-start/src/server/agentRules.ts @@ -0,0 +1,31 @@ +import type { FileSystemAdapter, PathAdapter } from '@clerk/shared/agentRules'; +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } from '@clerk/shared/utils'; + +type NodeProcess = { + cwd: () => string; + getBuiltinModule?: (id: string) => unknown; +}; + +let didWrite = false; + +export function writeAgentRulesOnce() { + if (didWrite || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { + return; + } + + didWrite = true; + + const nodeProcess = (globalThis as { process?: NodeProcess }).process; + const fs = nodeProcess?.getBuiltinModule?.('node:fs') as FileSystemAdapter | undefined; + const path = nodeProcess?.getBuiltinModule?.('node:path') as PathAdapter | undefined; + if (!nodeProcess || !fs || !path) { + return; + } + + try { + writeClerkAgentRules({ cwd: nodeProcess.cwd(), fs, path, sdks: ['tanstack-react-start'] }); + } catch { + return; + } +} diff --git a/packages/tanstack-react-start/src/server/clerkMiddleware.ts b/packages/tanstack-react-start/src/server/clerkMiddleware.ts index dfac9f37e7f..b0f121e2230 100644 --- a/packages/tanstack-react-start/src/server/clerkMiddleware.ts +++ b/packages/tanstack-react-start/src/server/clerkMiddleware.ts @@ -6,6 +6,7 @@ import type { PendingSessionOptions } from '@clerk/shared/types'; import type { AnyRequestMiddleware } from '@tanstack/react-start'; import { createMiddleware } from '@tanstack/react-start'; +import { writeAgentRulesOnce } from './agentRules'; import { clerkClient } from './clerkClient'; import { loadOptions } from './loadOptions'; import type { ClerkMiddlewareOptions, ClerkMiddlewareOptionsCallback } from './types'; @@ -15,6 +16,8 @@ export const clerkMiddleware = ( options?: ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback, ): AnyRequestMiddleware => { return createMiddleware().server(async ({ request, next }) => { + writeAgentRulesOnce(); + const clerkRequest = createClerkRequest(patchRequest(request)); // Resolve options: if function, call it with context object; otherwise use as-is From f8b581fca87644373dbb397947262a858aa03470 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 19 Oct 2026 17:14:32 -0400 Subject: [PATCH 04/17] feat(react,vue): add Vite plugins that write Clerk agent rules Co-Authored-By: Claude Opus 5.5 --- .changeset/react-vue-agent-rules-vite.md | 15 +++++++++++++++ packages/react/package.json | 10 ++++++++++ packages/react/src/vite.ts | 3 +++ packages/react/tsdown.config.mts | 1 + packages/vue/package.json | 6 +++++- packages/vue/src/vite.ts | 3 +++ packages/vue/tsdown.config.ts | 9 ++++++++- 7 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 .changeset/react-vue-agent-rules-vite.md create mode 100644 packages/react/src/vite.ts create mode 100644 packages/vue/src/vite.ts diff --git a/.changeset/react-vue-agent-rules-vite.md b/.changeset/react-vue-agent-rules-vite.md new file mode 100644 index 00000000000..a153d58b062 --- /dev/null +++ b/.changeset/react-vue-agent-rules-vite.md @@ -0,0 +1,15 @@ +--- +'@clerk/react': minor +'@clerk/vue': minor +--- + +Add Vite plugins that write a Clerk section to the project's `AGENTS.md` when the Vite dev server runs under an AI coding agent. Nothing is written during builds or in CI. + +```ts +import { clerkReactAgentRules } from '@clerk/react/vite'; +// or: import { clerkVueAgentRules } from '@clerk/vue/vite'; + +export default defineConfig({ + plugins: [react(), clerkReactAgentRules()], +}); +``` diff --git a/packages/react/package.json b/packages/react/package.json index 9c79b9d1fcb..f75a01fafa7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -81,6 +81,16 @@ "types": "./dist/types.d.cts" } }, + "./vite": { + "import": { + "types": "./dist/vite.d.mts", + "default": "./dist/vite.mjs" + }, + "require": { + "types": "./dist/vite.d.cts", + "default": "./dist/vite.cjs" + } + }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", diff --git a/packages/react/src/vite.ts b/packages/react/src/vite.ts new file mode 100644 index 00000000000..d2b276b3e23 --- /dev/null +++ b/packages/react/src/vite.ts @@ -0,0 +1,3 @@ +import { clerkAgentRulesVitePlugin } from '@clerk/shared/vite'; + +export const clerkReactAgentRules = () => clerkAgentRulesVitePlugin('react'); diff --git a/packages/react/tsdown.config.mts b/packages/react/tsdown.config.mts index 3b2d3521d6c..5c60e7a5334 100644 --- a/packages/react/tsdown.config.mts +++ b/packages/react/tsdown.config.mts @@ -65,6 +65,7 @@ export default defineConfig((overrideOptions: Options) => { experimental: 'src/experimental.ts', legacy: 'src/legacy.ts', types: 'src/types/index.ts', + vite: 'src/vite.ts', }, dts: true, onSuccess: shouldPublish ? 'pkglab pub --ping' : undefined, diff --git a/packages/vue/package.json b/packages/vue/package.json index dae4208ba55..e2f51e44479 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -42,7 +42,11 @@ "types": "./dist/errors.d.ts", "default": "./dist/errors.js" }, - "./types": "./dist/types/index.d.ts" + "./types": "./dist/types/index.d.ts", + "./vite": { + "types": "./dist/vite.d.ts", + "default": "./dist/vite.js" + } }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/vue/src/vite.ts b/packages/vue/src/vite.ts new file mode 100644 index 00000000000..6b3ed019fa9 --- /dev/null +++ b/packages/vue/src/vite.ts @@ -0,0 +1,3 @@ +import { clerkAgentRulesVitePlugin } from '@clerk/shared/vite'; + +export const clerkVueAgentRules = () => clerkAgentRulesVitePlugin('vue'); diff --git a/packages/vue/tsdown.config.ts b/packages/vue/tsdown.config.ts index 9ea0f8d0fc8..fe55454852f 100644 --- a/packages/vue/tsdown.config.ts +++ b/packages/vue/tsdown.config.ts @@ -9,7 +9,14 @@ export default defineConfig(overrideOptions => { return { clean: true, - entry: ['./src/index.ts', './src/experimental.ts', './src/internal.ts', './src/errors.ts', './src/types/index.ts'], + entry: [ + './src/index.ts', + './src/experimental.ts', + './src/internal.ts', + './src/errors.ts', + './src/types/index.ts', + './src/vite.ts', + ], format: ['esm'], fixedExtension: false, sourcemap: true, From 853d3334dab6601b0be35b6c04446f2e54ffa536 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 19 Oct 2026 17:14:33 -0400 Subject: [PATCH 05/17] feat(upgrade): add --agents-md flag Co-Authored-By: Claude Opus 5.5 --- .changeset/upgrade-agents-md.md | 5 +++ packages/upgrade/package.json | 1 + .../src/__tests__/integration/cli.test.js | 33 +++++++++++++++++++ packages/upgrade/src/cli.js | 33 +++++++++++++++++++ packages/upgrade/src/util/agent-rules.js | 27 +++++++++++++++ pnpm-lock.yaml | 3 ++ 6 files changed, 102 insertions(+) create mode 100644 .changeset/upgrade-agents-md.md create mode 100644 packages/upgrade/src/util/agent-rules.js diff --git a/.changeset/upgrade-agents-md.md b/.changeset/upgrade-agents-md.md new file mode 100644 index 00000000000..ead62e02cca --- /dev/null +++ b/.changeset/upgrade-agents-md.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add an `--agents-md` flag that writes Clerk sections to the project's `AGENTS.md` for each installed Clerk SDK, then exits. Interactive upgrades also offer to write these sections. diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index 5632067a24a..e612c832f39 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -33,6 +33,7 @@ ] }, "dependencies": { + "@clerk/shared": "workspace:^", "chalk": "^5.6.2", "ejs": "3.1.10", "gray-matter": "^4.0.3", diff --git a/packages/upgrade/src/__tests__/integration/cli.test.js b/packages/upgrade/src/__tests__/integration/cli.test.js index 1a6f1a4f9ad..98b96e8be5a 100644 --- a/packages/upgrade/src/__tests__/integration/cli.test.js +++ b/packages/upgrade/src/__tests__/integration/cli.test.js @@ -175,6 +175,39 @@ describe('CLI Integration', () => { }); }); + describe('--agents-md flag', () => { + let fixture; + + beforeEach(() => { + fixture = createTempFixture('nextjs-v7'); + }); + + afterEach(() => { + fixture?.cleanup(); + }); + + it('writes AGENTS.md and CLAUDE.md without running SDK upgrade detection', async () => { + const fs = await import('node:fs'); + const result = await runCli(['--dir', fixture.path, '--agents-md'], { timeout: 15000 }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Wrote Clerk AGENTS.md sections'); + expect(fs.readFileSync(path.join(fixture.path, 'AGENTS.md'), 'utf8')).toContain( + '', + ); + expect(fs.readFileSync(path.join(fixture.path, 'CLAUDE.md'), 'utf8')).toBe('@AGENTS.md\n'); + }); + + it('does not write files in dry-run mode', async () => { + const fs = await import('node:fs'); + const result = await runCli(['--dir', fixture.path, '--agents-md', '--dry-run'], { timeout: 15000 }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('[dry run] Would write'); + expect(fs.existsSync(path.join(fixture.path, 'AGENTS.md'))).toBe(false); + }); + }); + describe('Version Display', () => { it('shows current version in output', async () => { const dir = getFixturePath('nextjs-v6'); diff --git a/packages/upgrade/src/cli.js b/packages/upgrade/src/cli.js index 7901129ed60..57b089fc8de 100644 --- a/packages/upgrade/src/cli.js +++ b/packages/upgrade/src/cli.js @@ -17,6 +17,7 @@ import { renderWarning, } from './render.js'; import { runCodemods, runScans } from './runner.js'; +import { detectClerkAgentRuleSdks, writeClerkAgentRules } from './util/agent-rules.js'; import { detectSdk, findWorkspaceRoot, @@ -50,6 +51,7 @@ const cli = meow( --release Name of the release you're upgrading to (e.g. core-3) --canary Upgrade to the latest canary version instead of the stable release --dry-run Show what would be done without making changes + --agents-md Write Clerk AGENTS.md sections and exit Examples $ npx @clerk/upgrade @@ -70,6 +72,7 @@ const cli = meow( flags: { dir: { type: 'string', default: process.cwd() }, dryRun: { type: 'boolean', default: false }, + agentsMd: { type: 'boolean', default: false }, glob: { type: 'string', default: '**/*.(js|jsx|ts|tsx|mjs|cjs)' }, ignore: { type: 'string', isMultiple: true }, release: { type: 'string' }, @@ -93,6 +96,7 @@ async function main() { release: cli.flags.release, skipCodemods: cli.flags.skipCodemods, skipUpgrade: cli.flags.skipUpgrade, + agentsMd: cli.flags.agentsMd, }; if (options.dryRun) { @@ -100,6 +104,11 @@ async function main() { renderNewline(); } + if (options.agentsMd) { + writeAgentsMd(options); + return; + } + // Step 1: Detect or prompt for SDK let sdk = normalizeSdkName(cli.flags.sdk); @@ -237,6 +246,13 @@ async function main() { console.log(''); + if (isInteractive) { + const sdks = detectClerkAgentRuleSdks(options.dir); + if (sdks.length > 0 && (await promptConfirm('Write Clerk AGENTS.md sections?', true))) { + writeAgentsMd(options, sdks); + } + } + // Step 6: Handle upgrade status if (options.skipUpgrade) { renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow'); @@ -271,6 +287,23 @@ async function main() { renderComplete(sdk, config.docsUrl); } +function writeAgentsMd(options, detectedSdks) { + const sdks = detectedSdks || detectClerkAgentRuleSdks(options.dir); + + if (sdks.length === 0) { + renderWarning('No Clerk SDK packages found for AGENTS.md'); + return; + } + + const result = writeClerkAgentRules({ dir: options.dir, dryRun: options.dryRun, sdks }); + const prefix = options.dryRun ? '[dry run] Would write' : 'Wrote'; + + renderText(`${prefix} Clerk AGENTS.md sections for ${sdks.map(s => `@clerk/${s}`).join(', ')}`, 'blue'); + renderText(`Target: ${result.targetDir}`); + renderText(`AGENTS.md: ${result.agentsMd}; CLAUDE.md: ${result.claudeMd}`); + renderNewline(); +} + async function performUpgrade(sdk, packageManager, config, options) { const targetPackage = getTargetPackageName(sdk); const oldPackage = getOldPackageName(sdk); diff --git a/packages/upgrade/src/util/agent-rules.js b/packages/upgrade/src/util/agent-rules.js new file mode 100644 index 00000000000..35f96739000 --- /dev/null +++ b/packages/upgrade/src/util/agent-rules.js @@ -0,0 +1,27 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { isClerkAgentRulesSdk, writeClerkAgentRules as writeRules } from '@clerk/shared/agentRules'; + +import { findWorkspaceRoot, getInstalledClerkPackages, getWorkspacePackageDirs } from './detect-sdk.js'; + +export function detectClerkAgentRuleSdks(dir) { + const workspaceRoot = findWorkspaceRoot(dir); + const packageDirs = workspaceRoot ? getWorkspacePackageDirs(workspaceRoot) : [dir]; + const sdks = new Set(); + + for (const packageDir of packageDirs) { + for (const packageName of Object.keys(getInstalledClerkPackages(packageDir))) { + const sdk = packageName.replace(/^@clerk\//, ''); + if (isClerkAgentRulesSdk(sdk)) { + sdks.add(sdk); + } + } + } + + return [...sdks].sort(); +} + +export function writeClerkAgentRules({ dir, dryRun = false, sdks = detectClerkAgentRuleSdks(dir) }) { + return { ...writeRules({ cwd: dir, dryRun, fs, path, sdks }), sdks }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 032116ad7a8..cc1136468f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1370,6 +1370,9 @@ importers: packages/upgrade: dependencies: + '@clerk/shared': + specifier: workspace:^ + version: link:../shared chalk: specifier: ^5.6.2 version: 5.6.2 From 5dc481bdaa363b349c660ee55282556f4f7ec80f Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Tue, 20 Oct 2026 16:31:21 -0400 Subject: [PATCH 06/17] refactor(shared): recommend the Clerk plugin in AGENTS.md Co-Authored-By: Claude Opus 5.5 --- .changeset/ship-sdk-agents-md.md | 2 +- .changeset/write-agents-md-in-dev.md | 2 +- packages/astro/AGENTS.md | 22 +++++------ packages/backend/AGENTS.md | 22 +++++------ packages/chrome-extension/AGENTS.md | 22 +++++------ packages/clerk-js/AGENTS.md | 22 +++++------ packages/electron/AGENTS.md | 22 +++++------ packages/expo/AGENTS.md | 22 +++++------ packages/express/AGENTS.md | 22 +++++------ packages/fastify/AGENTS.md | 22 +++++------ packages/hono/AGENTS.md | 22 +++++------ packages/nextjs/AGENTS.md | 22 +++++------ packages/nuxt/AGENTS.md | 22 +++++------ packages/react-router/AGENTS.md | 22 +++++------ packages/react/AGENTS.md | 22 +++++------ .../shared/src/__tests__/agentRules.spec.ts | 32 ++++++++-------- packages/shared/src/agentRules/index.ts | 37 ++++++++----------- packages/tanstack-react-start/AGENTS.md | 22 +++++------ packages/vue/AGENTS.md | 22 +++++------ scripts/sync-agents-md.mjs | 16 ++++---- 20 files changed, 208 insertions(+), 211 deletions(-) diff --git a/.changeset/ship-sdk-agents-md.md b/.changeset/ship-sdk-agents-md.md index 6af3494a6b0..6ea5963d3e8 100644 --- a/.changeset/ship-sdk-agents-md.md +++ b/.changeset/ship-sdk-agents-md.md @@ -16,4 +16,4 @@ '@clerk/vue': patch --- -Ship an `AGENTS.md` file in the package. It lists the current APIs for this version and how to use the Clerk MCP server, so AI coding agents can read it from `node_modules` instead of relying on outdated training data. +Ship an `AGENTS.md` file in the package. It lists the current APIs for this version and recommends the Clerk plugin, so AI coding agents can read it from `node_modules` instead of relying on outdated training data. diff --git a/.changeset/write-agents-md-in-dev.md b/.changeset/write-agents-md-in-dev.md index 113f5c441ac..eebff89a268 100644 --- a/.changeset/write-agents-md-in-dev.md +++ b/.changeset/write-agents-md-in-dev.md @@ -6,4 +6,4 @@ '@clerk/tanstack-react-start': minor --- -When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed, plus a `CLAUDE.md` that imports it when none exists. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and points it to the Clerk MCP server. Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. +When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed, plus a `CLAUDE.md` that imports it when none exists. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. diff --git a/packages/astro/AGENTS.md b/packages/astro/AGENTS.md index 230d0172ea4..d654f8099b0 100644 --- a/packages/astro/AGENTS.md +++ b/packages/astro/AGENTS.md @@ -12,23 +12,23 @@ Read the installed `@clerk/astro` types before changing auth code. If a symbol i - The `as` prop was removed from ``, ``, and ``. Use `asChild` with a child element. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/astro` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/backend/AGENTS.md b/packages/backend/AGENTS.md index c52ff0fec0e..05772f875be 100644 --- a/packages/backend/AGENTS.md +++ b/packages/backend/AGENTS.md @@ -10,23 +10,23 @@ Read the installed `@clerk/backend` types before changing auth code. If a symbol - Verify webhooks with `verifyWebhook(request)` from `@clerk/backend/webhooks`. It reads `CLERK_WEBHOOK_SIGNING_SECRET` by default. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/backend` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/chrome-extension/AGENTS.md b/packages/chrome-extension/AGENTS.md index 54c0974882b..ceca314d5a7 100644 --- a/packages/chrome-extension/AGENTS.md +++ b/packages/chrome-extension/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/chrome-extension` types before changing auth code. If - The root `useSignIn()` and `useSignUp()` are the Core 3 hooks. The old `isLoaded`-style hooks are in `@clerk/chrome-extension/legacy`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/chrome-extension` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/clerk-js/AGENTS.md b/packages/clerk-js/AGENTS.md index 33150c60bd9..6317895508f 100644 --- a/packages/clerk-js/AGENTS.md +++ b/packages/clerk-js/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/clerk-js` types before changing auth code. If a symbo - `session.getToken()` throws `ClerkOfflineError` when offline. It no longer returns `null`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/clerk-js` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/electron/AGENTS.md b/packages/electron/AGENTS.md index 7166131ffe4..1bf50ab1ab0 100644 --- a/packages/electron/AGENTS.md +++ b/packages/electron/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/electron` types before changing auth code. If a symbo - For passkeys, pass `passkeys` from `@clerk/electron/passkeys` to `ClerkProvider`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/electron` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/expo/AGENTS.md b/packages/expo/AGENTS.md index a539aff0b13..c6d24aa3dd0 100644 --- a/packages/expo/AGENTS.md +++ b/packages/expo/AGENTS.md @@ -12,26 +12,26 @@ Read the installed `@clerk/expo` types before changing auth code. If a symbol is - ``, ``, and `` were removed. Use `` or ``. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/expo` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + ## Contributing to @clerk/expo diff --git a/packages/express/AGENTS.md b/packages/express/AGENTS.md index 162363b2b9f..ac1dd89d1a3 100644 --- a/packages/express/AGENTS.md +++ b/packages/express/AGENTS.md @@ -12,23 +12,23 @@ Read the installed `@clerk/express` types before changing auth code. If a symbol - Verify webhooks with `verifyWebhook(req)` from `@clerk/express/webhooks`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/express` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/fastify/AGENTS.md b/packages/fastify/AGENTS.md index 740c63792a5..b2319e9d588 100644 --- a/packages/fastify/AGENTS.md +++ b/packages/fastify/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/fastify` types before changing auth code. If a symbol - Verify webhooks with `verifyWebhook(req)` from `@clerk/fastify/webhooks`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/fastify` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/hono/AGENTS.md b/packages/hono/AGENTS.md index 4ff22e0636f..65e6df00e7e 100644 --- a/packages/hono/AGENTS.md +++ b/packages/hono/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/hono` types before changing auth code. If a symbol is - Verify webhooks with `verifyWebhook(c)` from `@clerk/hono/webhooks`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/hono` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/nextjs/AGENTS.md b/packages/nextjs/AGENTS.md index fb2f8b863c6..1cf65db099b 100644 --- a/packages/nextjs/AGENTS.md +++ b/packages/nextjs/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/nextjs` types before changing auth code. If a symbol - `useSignIn()` and `useSignUp()` return `{ signIn | signUp, errors, fetchStatus }`. The old `{ isLoaded, signIn, setActive }` shape lives in `@clerk/nextjs/legacy`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/nextjs` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/nuxt/AGENTS.md b/packages/nuxt/AGENTS.md index 0d3c32c6da2..c59cc5a0ae5 100644 --- a/packages/nuxt/AGENTS.md +++ b/packages/nuxt/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/nuxt` types before changing auth code. If a symbol is - ``, ``, ``, ``, ``, and `` default to `path` routing. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/nuxt` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/react-router/AGENTS.md b/packages/react-router/AGENTS.md index a19588187b4..5f1b2c99e67 100644 --- a/packages/react-router/AGENTS.md +++ b/packages/react-router/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/react-router` types before changing auth code. If a s - ``, ``, and `` were removed. Use `` or ``. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/react-router` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/react/AGENTS.md b/packages/react/AGENTS.md index 40355992e3f..4a1e864693e 100644 --- a/packages/react/AGENTS.md +++ b/packages/react/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/react` types before changing auth code. If a symbol i - `appearance.layout` was renamed to `appearance.options`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/react` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index a359eb99fa5..5d3c3ae944f 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -1,11 +1,11 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { - CLERK_SDK_MCP_END, - CLERK_SDK_MCP_START, + CLERK_SDK_PLUGIN_END, + CLERK_SDK_PLUGIN_START, type FileSystemAdapter, renderClerkAgentRulesBlock, - renderClerkSdkMcpBlock, + renderClerkSdkPluginBlock, writeClerkAgentRules, } from '../agentRules'; @@ -120,26 +120,28 @@ describe('writeClerkAgentRules', () => { }); }); -describe('Clerk MCP rules', () => { - it('adds the MCP intro and the ask-before-changes rule to the root block', () => { +describe('Clerk plugin rules', () => { + it('recommends the Clerk plugin and keeps the ask-before-changes rule in the root block', () => { const block = renderClerkAgentRulesBlock(); - expect(block).toContain('### Clerk MCP server'); - expect(block).toContain('https://mcp.clerk.com/mcp'); - expect(block).toContain('https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server'); - expect(block).toContain('Ask the user before calling any Clerk MCP tool'); + expect(block).toContain('### Clerk plugin'); + expect(block).toContain('npx plugins add clerk/skills'); + expect(block).toContain('https://clerk.com/docs/guides/ai/overview'); + expect(block).toContain('https://github.com/clerk/skills'); + expect(block).toContain('Ask the user before calling any Clerk tool'); + expect(block).not.toContain('mcp.clerk.com'); }); - it('renders a marked per-SDK MCP block that names the installed package', () => { - const block = renderClerkSdkMcpBlock('nextjs'); + it('renders a marked per-SDK plugin block with the docs entry point', () => { + const block = renderClerkSdkPluginBlock(); - expect(block.startsWith(CLERK_SDK_MCP_START)).toBe(true); - expect(block.endsWith(CLERK_SDK_MCP_END)).toBe(true); - expect(block).toContain('installed `@clerk/nextjs` types'); + expect(block.startsWith(CLERK_SDK_PLUGIN_START)).toBe(true); + expect(block.endsWith(CLERK_SDK_PLUGIN_END)).toBe(true); + expect(block).toContain('npx plugins add clerk/skills'); expect(block).toContain('https://clerk.com/docs/llms.txt'); }); it('uses the requested line endings', () => { - expect(renderClerkSdkMcpBlock('react', '\r\n')).not.toMatch(/[^\r]\n/); + expect(renderClerkSdkPluginBlock('\r\n')).not.toMatch(/[^\r]\n/); }); }); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 18f63312abe..6a5abc19b13 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -77,36 +77,33 @@ export const isClerkAgentRulesSdk = (value: string): value is ClerkAgentRulesSdk export const clerkAgentRulesSdks = Object.keys(SDK_LABELS) as ClerkAgentRulesSdk[]; -export const CLERK_SDK_MCP_START = ''; -export const CLERK_SDK_MCP_END = ''; +export const CLERK_SDK_PLUGIN_START = ''; +export const CLERK_SDK_PLUGIN_END = ''; -const MCP_INTRO = `### Clerk MCP server +const PLUGIN_SECTION = `### Clerk plugin -Use this section when a \`clerk\` MCP server (\`https://mcp.clerk.com/mcp\`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server`; +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -const MCP_ASK_BEFORE_CHANGES = - 'Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account.'; +\`\`\`sh +npx plugins add clerk/skills +\`\`\` -export function renderClerkSdkMcpBlock(sdk: ClerkAgentRulesSdk, eol = '\n') { - return normalizeEol( - `${CLERK_SDK_MCP_START} +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills -${MCP_INTRO} +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account.`; -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +export function renderClerkSdkPluginBlock(eol = '\n') { + return normalizeEol( + `${CLERK_SDK_PLUGIN_START} -- Get Clerk code from the SDK snippet tools (\`list_clerk_sdk_snippets\`, \`clerk_sdk_snippet\`) instead of writing it from memory. -- Check every import in a snippet against the installed \`@clerk/${sdk}\` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- ${MCP_ASK_BEFORE_CHANGES} Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +${PLUGIN_SECTION} ### Docs Start at https://clerk.com/docs/llms.txt. Add \`.md\` to any docs URL to get Markdown. -${CLERK_SDK_MCP_END}`, +${CLERK_SDK_PLUGIN_END}`, eol, ); } @@ -128,9 +125,7 @@ This block is written by the Clerk SDK when a supported dev server starts, or by \`npx @clerk/upgrade --agents-md\`. Removing it from a diff can recreate the uncommitted change; commit it with your work. -${MCP_INTRO} - -${MCP_ASK_BEFORE_CHANGES} +${PLUGIN_SECTION} ${GENERIC_END}`, eol, diff --git a/packages/tanstack-react-start/AGENTS.md b/packages/tanstack-react-start/AGENTS.md index 43e9b25ec0f..2a8a640b07a 100644 --- a/packages/tanstack-react-start/AGENTS.md +++ b/packages/tanstack-react-start/AGENTS.md @@ -11,23 +11,23 @@ Read the installed `@clerk/tanstack-react-start` types before changing auth code - `useSignIn()` and `useSignUp()` return `{ signIn | signUp, errors, fetchStatus }`. The old shape lives in `@clerk/tanstack-react-start/legacy`. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/tanstack-react-start` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/packages/vue/AGENTS.md b/packages/vue/AGENTS.md index dc32ddd818a..19ac8c70c32 100644 --- a/packages/vue/AGENTS.md +++ b/packages/vue/AGENTS.md @@ -10,23 +10,23 @@ Read the installed `@clerk/vue` types before changing auth code. If a symbol is - `getToken()` throws `ClerkOfflineError` (from `@clerk/vue/errors`) when offline. It still returns `null` when signed out. - Clerk Elements is removed. Never use it. - + -### Clerk MCP server +### Clerk plugin -Use this section when a `clerk` MCP server (`https://mcp.clerk.com/mcp`) is connected. If it is not connected, tell the user they can add it: -https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server +The Clerk plugin gives agents Clerk's skills and MCP server. If it is not installed, tell the user they can install it. Do not run the command yourself: -The server lists its own tools and describes what each one does. Follow these rules on top of those descriptions: +```sh +npx plugins add clerk/skills +``` -- Get Clerk code from the SDK snippet tools (`list_clerk_sdk_snippets`, `clerk_sdk_snippet`) instead of writing it from memory. -- Check every import in a snippet against the installed `@clerk/vue` types. If they disagree, the installed types win. -- Read the current application and instance config before you change anything. -- Ask the user before calling any Clerk MCP tool that creates or changes something in their Clerk account. Run config changes as a dry run first and show the user the result. -- The server never returns secret keys. Do not ask the user to paste one into chat. Point them to https://dashboard.clerk.com/~/api-keys. +Install steps for each tool: https://clerk.com/docs/guides/ai/overview +Source: https://github.com/clerk/skills + +Ask the user before calling any Clerk tool that creates or changes something in their Clerk account. ### Docs Start at https://clerk.com/docs/llms.txt. Add `.md` to any docs URL to get Markdown. - + diff --git a/scripts/sync-agents-md.mjs b/scripts/sync-agents-md.mjs index c8d940f55c1..2e04cd19798 100644 --- a/scripts/sync-agents-md.mjs +++ b/scripts/sync-agents-md.mjs @@ -4,10 +4,10 @@ import { readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { - CLERK_SDK_MCP_END, - CLERK_SDK_MCP_START, + CLERK_SDK_PLUGIN_END, + CLERK_SDK_PLUGIN_START, clerkAgentRulesSdks, - renderClerkSdkMcpBlock, + renderClerkSdkPluginBlock, } from '../packages/shared/src/agentRules/index.ts'; const check = process.argv.includes('--check'); @@ -23,15 +23,15 @@ for (const sdk of clerkAgentRulesSdks) { continue; } - const start = content.indexOf(CLERK_SDK_MCP_START); - const end = content.indexOf(CLERK_SDK_MCP_END, start); + const start = content.indexOf(CLERK_SDK_PLUGIN_START); + const end = content.indexOf(CLERK_SDK_PLUGIN_END, start); if (start === -1 || end === -1) { - problems.push(`${relative} has no ${CLERK_SDK_MCP_START} ... ${CLERK_SDK_MCP_END} markers`); + problems.push(`${relative} has no ${CLERK_SDK_PLUGIN_START} ... ${CLERK_SDK_PLUGIN_END} markers`); continue; } - const next = content.slice(0, start) + renderClerkSdkMcpBlock(sdk) + content.slice(end + CLERK_SDK_MCP_END.length); + const next = content.slice(0, start) + renderClerkSdkPluginBlock() + content.slice(end + CLERK_SDK_PLUGIN_END.length); if (next === content) { continue; @@ -48,7 +48,7 @@ for (const sdk of clerkAgentRulesSdks) { if (problems.length > 0) { console.error(problems.join('\n')); if (check) { - console.error('Run `pnpm agents-md:sync` to update the Clerk MCP section in each SDK AGENTS.md.'); + console.error('Run `pnpm agents-md:sync` to update the Clerk plugin section in each SDK AGENTS.md.'); } process.exit(1); } From 0e4331e3162844253101a3712e33fc4f5190305d Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Tue, 20 Oct 2026 17:09:48 -0400 Subject: [PATCH 07/17] fix(shared): import AGENTS.md from an existing CLAUDE.md Co-Authored-By: Claude Opus 5.5 --- .changeset/write-agents-md-in-dev.md | 2 +- .../shared/src/__tests__/agentRules.spec.ts | 93 +++++++++++++++++++ packages/shared/src/agentRules/index.ts | 30 +++++- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/.changeset/write-agents-md-in-dev.md b/.changeset/write-agents-md-in-dev.md index eebff89a268..52672b11693 100644 --- a/.changeset/write-agents-md-in-dev.md +++ b/.changeset/write-agents-md-in-dev.md @@ -6,4 +6,4 @@ '@clerk/tanstack-react-start': minor --- -When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed, plus a `CLAUDE.md` that imports it when none exists. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. +When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed. It also creates a `CLAUDE.md` that imports `AGENTS.md`, or adds an `@AGENTS.md` import to an existing `CLAUDE.md` that doesn't load it. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 5d3c3ae944f..91fd6393d67 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -120,6 +120,99 @@ describe('writeClerkAgentRules', () => { }); }); +describe('writeClerkAgentRules with an existing CLAUDE.md', () => { + it('adds an @AGENTS.md import when CLAUDE.md does not load AGENTS.md', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Agent rules\n', + '/repo/CLAUDE.md': 'Claude rules\n', + }); + + const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(result.claudeMd).toBe('updated'); + expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n\n@AGENTS.md\n'); + expect(memory.read('/repo/AGENTS.md')).toContain(''); + }); + + it('adds the import only once across runs', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Agent rules\n', + '/repo/CLAUDE.md': 'Claude rules\n', + }); + + writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(result.claudeMd).toBe('unchanged'); + expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n\n@AGENTS.md\n'); + }); + + it.each(['@AGENTS.md', '@./AGENTS.md'])('leaves CLAUDE.md alone when it already imports %s', importLine => { + const claude = `Claude rules\n\n${importLine}\n`; + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Agent rules\n', + '/repo/CLAUDE.md': claude, + }); + + const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(result.claudeMd).toBe('unchanged'); + expect(memory.read('/repo/CLAUDE.md')).toBe(claude); + }); + + it('leaves CLAUDE.md alone when it mirrors AGENTS.md, as a symlink does', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Shared rules\n', + '/repo/CLAUDE.md': 'Shared rules\n', + }); + + const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(result.claudeMd).toBe('unchanged'); + expect(memory.read('/repo/CLAUDE.md')).toBe('Shared rules\n'); + }); + + it('writes the Clerk block into CLAUDE.md and skips the import when only CLAUDE.md exists', () => { + const memory = createMemoryFs({ '/repo/CLAUDE.md': 'Claude rules\n' }); + + const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(result.agentsMd).toBe('updated'); + expect(memory.read('/repo/AGENTS.md')).toBeUndefined(); + expect(memory.read('/repo/CLAUDE.md')).toContain(''); + expect(memory.read('/repo/CLAUDE.md')).not.toContain('@AGENTS.md'); + }); + + it('keeps CRLF line endings in CLAUDE.md', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Agent rules\r\n', + '/repo/CLAUDE.md': 'Claude rules\r\n', + }); + + writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\r\n\r\n@AGENTS.md\r\n'); + }); + + it('reports the import but does not write it in dry-run mode', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Agent rules\n', + '/repo/CLAUDE.md': 'Claude rules\n', + }); + + const result = writeClerkAgentRules({ + cwd: '/repo', + dryRun: true, + fs: memory.fs, + path: pathAdapter, + sdks: ['nextjs'], + }); + + expect(result.claudeMd).toBe('updated'); + expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n'); + }); +}); + describe('Clerk plugin rules', () => { it('recommends the Clerk plugin and keeps the ask-before-changes rule in the root block', () => { const block = renderClerkAgentRulesBlock(); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 6a5abc19b13..84bdd06b5f5 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -40,7 +40,7 @@ export interface WriteClerkAgentRulesOptions { export type WriteClerkAgentRulesResult = { targetDir: string; agentsMd: 'created' | 'updated' | 'unchanged'; - claudeMd: 'created' | 'unchanged' | 'skipped'; + claudeMd: 'created' | 'updated' | 'unchanged' | 'skipped'; }; const GENERIC_START = ''; @@ -73,6 +73,13 @@ const uniqueSdks = (sdks: ClerkAgentRulesSdk[]) => [...new Set(sdks)].sort(); const detectEol = (content: string) => (content.includes('\r\n') ? '\r\n' : '\n'); const normalizeEol = (content: string, eol: string) => content.replace(/\r?\n/g, eol); +const CLAUDE_AGENTS_IMPORT = /^\s*@(?:\.\/)?AGENTS\.md\s*$/m; + +const appendAgentsImport = (content: string) => { + const eol = detectEol(content); + return content.trim() ? `${content.trimEnd()}${eol}${eol}@AGENTS.md${eol}` : `@AGENTS.md${eol}`; +}; + export const isClerkAgentRulesSdk = (value: string): value is ClerkAgentRulesSdk => value in SDK_LABELS; export const clerkAgentRulesSdks = Object.keys(SDK_LABELS) as ClerkAgentRulesSdk[]; @@ -248,7 +255,15 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ } const targetStatus = fs.existsSync(targetFile) ? (next === existing ? 'unchanged' : 'updated') : 'created'; - const shouldCreateClaude = targetFile === agentsPath && !fs.existsSync(claudePath); + const claudeExists = fs.existsSync(claudePath); + const shouldCreateClaude = targetFile === agentsPath && !claudeExists; + const claudeContent = + targetFile === agentsPath && claudeExists ? fs.readFileSync(claudePath, { encoding: 'utf8' }) : null; + const shouldImportAgentsInClaude = + claudeContent !== null && + claudeContent !== existing && + !claudeContent.includes(GENERIC_START) && + !CLAUDE_AGENTS_IMPORT.test(claudeContent); if (!dryRun) { fs.mkdirSync?.(targetDir, { recursive: true }); @@ -258,11 +273,20 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ if (shouldCreateClaude) { fs.writeFileSync(claudePath, '@AGENTS.md\n', { encoding: 'utf8', mode: 0o644 }); } + if (shouldImportAgentsInClaude && claudeContent !== null) { + fs.writeFileSync(claudePath, appendAgentsImport(claudeContent), { encoding: 'utf8', mode: 0o644 }); + } } return { targetDir, agentsMd: targetStatus, - claudeMd: shouldCreateClaude ? 'created' : fs.existsSync(claudePath) ? 'unchanged' : 'skipped', + claudeMd: shouldCreateClaude + ? 'created' + : shouldImportAgentsInClaude + ? 'updated' + : claudeExists + ? 'unchanged' + : 'skipped', }; } From b01bbba7ebdc69f262219ea0aad330667f02ffdb Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Tue, 20 Oct 2026 17:29:11 -0400 Subject: [PATCH 08/17] refactor(shared): shorten the AGENTS.md intro and link the Markdown docs Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/__tests__/agentRules.spec.ts | 4 ++++ packages/shared/src/agentRules/index.ts | 11 ++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 91fd6393d67..8f8df657580 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -237,4 +237,8 @@ describe('Clerk plugin rules', () => { it('uses the requested line endings', () => { expect(renderClerkSdkPluginBlock('\r\n')).not.toMatch(/[^\r]\n/); }); + + it('links agents to the Markdown Clerk docs in the root block', () => { + expect(renderClerkAgentRulesBlock()).toContain('Clerk docs: https://clerk.com/docs.md'); + }); }); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 84bdd06b5f5..72383863458 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -121,16 +121,9 @@ export function renderClerkAgentRulesBlock(eol = '\n') { ## Clerk: your training data is out of date -This project uses Clerk. The installed SDKs are newer than your training data. APIs, -component names, and file conventions may have changed. Read the Clerk SDK section(s) -below before writing or editing any auth code. +Read the Clerk sections below before touching auth code. Only use Clerk APIs that exist in the installed package's types. Clerk docs: https://clerk.com/docs.md -Never recall a Clerk API from memory. If a symbol is not in the installed package's -types, it does not exist. - -This block is written by the Clerk SDK when a supported dev server starts, or by -\`npx @clerk/upgrade --agents-md\`. Removing it from a diff can recreate the uncommitted -change; commit it with your work. +The Clerk SDK writes this block. Commit it with your work. ${PLUGIN_SECTION} From d667c3a26fa5f74977894982580d514e5538c0ee Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Tue, 20 Oct 2026 17:50:04 -0400 Subject: [PATCH 09/17] fix(*): address review feedback on Clerk agent rules Co-Authored-By: Claude Opus 5.5 --- packages/expo/app.plugin.js | 39 +--------- .../__tests__/appPlugin.agentRules.test.js | 74 +++++++++++++++++++ .../shared/src/__tests__/agentRules.spec.ts | 30 ++++++++ .../src/__tests__/runtimeEnvironment.spec.ts | 32 ++++++++ packages/shared/src/agentRules/index.ts | 4 + .../shared/src/utils/runtimeEnvironment.ts | 5 +- .../__tests__/integration/agent-rules.test.js | 38 ++++++++++ .../src/__tests__/integration/cli.test.js | 21 ++++++ packages/upgrade/src/cli.js | 15 ++-- packages/upgrade/src/util/agent-rules.js | 2 +- 10 files changed, 215 insertions(+), 45 deletions(-) create mode 100644 packages/expo/src/__tests__/appPlugin.agentRules.test.js create mode 100644 packages/upgrade/src/__tests__/integration/agent-rules.test.js diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index 310e90e1edf..80cffc3dc56 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -7,6 +7,7 @@ * 3. If @clerk/expo-native-components is installed, its config plugin is applied for the Clerk native SDKs */ const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins'); +const { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } = require('@clerk/shared/utils'); const fs = require('fs'); const path = require('path'); @@ -15,43 +16,8 @@ const CLERK_EXPO_NATIVE = '@clerk/expo-native-components'; const CLERK_EXPO_NATIVE_OPTIONS = ['keychainService', 'theme']; let didWriteAgentRules = false; -const isTruthyEnvValue = value => typeof value === 'string' && !!value && !['0', 'false', 'off', 'no'].includes(value); - -const isAgentEnvironment = () => - [ - 'CLAUDECODE', - 'CURSOR_AGENT', - 'CURSOR_CLI', - 'CURSOR_EXTENSION_HOST_ROLE', - 'CURSOR_TRACE_ID', - 'GITHUB_COPILOT_AGENT', - 'GITHUB_COPILOT_CHAT', - 'AGENT', - 'AGENTS', - ].some(name => isTruthyEnvValue(process.env[name])); - -const isAutomatedEnvironment = () => - [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'CF_PAGES', - ].some(name => isTruthyEnvValue(process.env[name])); - const maybeWriteAgentRules = config => { - if (didWriteAgentRules || isAutomatedEnvironment() || !isAgentEnvironment()) { + if (didWriteAgentRules || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { return; } @@ -211,6 +177,7 @@ const withClerkExpo = (config, props = {}) => { module.exports = withClerkExpo; module.exports._testing = { addHostedAuthIntentFilter, + maybeWriteAgentRules, withClerkExpoNativeComponents, withClerkFaceIDPermission, }; diff --git a/packages/expo/src/__tests__/appPlugin.agentRules.test.js b/packages/expo/src/__tests__/appPlugin.agentRules.test.js new file mode 100644 index 00000000000..29f4a2af242 --- /dev/null +++ b/packages/expo/src/__tests__/appPlugin.agentRules.test.js @@ -0,0 +1,74 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; + +import { agentEnvironmentVariables, automatedEnvironmentVariables } from '@clerk/shared/utils'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +const nodeRequire = createRequire(import.meta.url); +const pluginPath = nodeRequire.resolve('../../app.plugin.js'); + +const loadMaybeWriteAgentRules = () => { + delete nodeRequire.cache[pluginPath]; + return nodeRequire(pluginPath)._testing.maybeWriteAgentRules; +}; + +describe('maybeWriteAgentRules', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-expo-agent-rules-')); + fs.writeFileSync(path.join(projectRoot, 'package.json'), '{}'); + for (const name of [...agentEnvironmentVariables, ...automatedEnvironmentVariables]) { + vi.stubEnv(name, ''); + } + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + const agentsMdPath = () => path.join(projectRoot, 'AGENTS.md'); + const config = () => ({ _internal: { projectRoot } }); + + test('writes the Expo rules once per process under an AI agent', () => { + vi.stubEnv('CLAUDECODE', '1'); + const maybeWriteAgentRules = loadMaybeWriteAgentRules(); + + maybeWriteAgentRules(config()); + expect(fs.readFileSync(agentsMdPath(), 'utf8')).toContain(''); + + fs.rmSync(agentsMdPath()); + maybeWriteAgentRules(config()); + expect(fs.existsSync(agentsMdPath())).toBe(false); + }); + + test('skips when the agent variable is set to a false value', () => { + vi.stubEnv('CLAUDECODE', 'False'); + + loadMaybeWriteAgentRules()(config()); + + expect(fs.existsSync(agentsMdPath())).toBe(false); + }); + + test('skips in production', () => { + vi.stubEnv('CLAUDECODE', '1'); + vi.stubEnv('NODE_ENV', 'production'); + + loadMaybeWriteAgentRules()(config()); + + expect(fs.existsSync(agentsMdPath())).toBe(false); + }); + + test('skips in CI', () => { + vi.stubEnv('CLAUDECODE', '1'); + vi.stubEnv('CI', 'true'); + + loadMaybeWriteAgentRules()(config()); + + expect(fs.existsSync(agentsMdPath())).toBe(false); + }); +}); diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 8f8df657580..53239de8d19 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -120,6 +120,36 @@ describe('writeClerkAgentRules', () => { }); }); +describe('writeClerkAgentRules with an unmatched BEGIN marker', () => { + it('keeps text after an orphan shared BEGIN marker across runs', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': 'Team rules\n\n\nold\n\n## My notes\nKeep this.\n', + }); + + for (let run = 0; run < 2; run++) { + writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + const content = memory.read('/repo/AGENTS.md'); + expect(content).toContain('Keep this.'); + expect(content?.match(//g)).toHaveLength(1); + } + }); + + it('keeps text after an orphan SDK BEGIN marker across runs', () => { + const memory = createMemoryFs({ + '/repo/AGENTS.md': '\nold\n\n## My notes\nKeep this.\n', + }); + + for (let run = 0; run < 2; run++) { + writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] }); + + const content = memory.read('/repo/AGENTS.md'); + expect(content).toContain('Keep this.'); + expect(content?.match(//g)).toHaveLength(1); + } + }); +}); + describe('writeClerkAgentRules with an existing CLAUDE.md', () => { it('adds an @AGENTS.md import when CLAUDE.md does not load AGENTS.md', () => { const memory = createMemoryFs({ diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 52cfe180eed..ee171ca8ec5 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -95,3 +95,35 @@ describe('isAgentEnvironment', () => { expect(isAgentEnvironment()).toBe(false); }); }); + +describe('isAgentEnvironment with Cursor', () => { + beforeEach(() => { + agentEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it('returns true when CURSOR_EXTENSION_HOST_ROLE is agent-exec', () => { + vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'agent-exec'); + + expect(isAgentEnvironment()).toBe(true); + }); + + it('returns false for any other CURSOR_EXTENSION_HOST_ROLE value', () => { + vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'user'); + + expect(isAgentEnvironment()).toBe(false); + }); + + it('returns true when CURSOR_TRACE_ID is set', () => { + vi.stubEnv('CURSOR_TRACE_ID', 'abc123'); + + expect(isAgentEnvironment()).toBe(true); + }); +}); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 72383863458..babbad83dbd 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -166,6 +166,10 @@ function upsertMarkedBlock( return content.slice(0, startIndex) + block + content.slice(endIndex + end.length); } + if (startIndex !== -1) { + return content; + } + if (insertAfter) { const insertIndex = content.indexOf(insertAfter); if (insertIndex !== -1) { diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 506f28e6abe..846a37dc5ec 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -75,5 +75,8 @@ export const isAutomatedEnvironment = (): boolean => { }; export const isAgentEnvironment = (): boolean => { - return agentEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name))); + return agentEnvironmentVariables.some(name => { + const value = getEnvVariable(name); + return name === 'CURSOR_EXTENSION_HOST_ROLE' ? value === 'agent-exec' : isTruthyEnvValue(value); + }); }; diff --git a/packages/upgrade/src/__tests__/integration/agent-rules.test.js b/packages/upgrade/src/__tests__/integration/agent-rules.test.js new file mode 100644 index 00000000000..fb91dc9e927 --- /dev/null +++ b/packages/upgrade/src/__tests__/integration/agent-rules.test.js @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { detectClerkAgentRuleSdks } from '../../util/agent-rules.js'; + +describe('detectClerkAgentRuleSdks', () => { + let root; + + const writePackage = (dir, pkg) => { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg)); + }; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-upgrade-agent-rules-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it('includes SDKs declared in the workspace root package.json', () => { + writePackage(root, { name: 'app', workspaces: ['packages/*'], dependencies: { '@clerk/nextjs': '^7.0.0' } }); + writePackage(path.join(root, 'packages', 'ui'), { name: 'ui', dependencies: { react: '^19.0.0' } }); + + expect(detectClerkAgentRuleSdks(root)).toEqual(['nextjs']); + }); + + it('combines SDKs from the workspace root and its packages', () => { + writePackage(root, { name: 'app', workspaces: ['packages/*'], dependencies: { '@clerk/nextjs': '^7.0.0' } }); + writePackage(path.join(root, 'packages', 'api'), { name: 'api', dependencies: { '@clerk/express': '^2.0.0' } }); + + expect(detectClerkAgentRuleSdks(path.join(root, 'packages', 'api'))).toEqual(['express', 'nextjs']); + }); +}); diff --git a/packages/upgrade/src/__tests__/integration/cli.test.js b/packages/upgrade/src/__tests__/integration/cli.test.js index 98b96e8be5a..11c714def5a 100644 --- a/packages/upgrade/src/__tests__/integration/cli.test.js +++ b/packages/upgrade/src/__tests__/integration/cli.test.js @@ -208,6 +208,27 @@ describe('CLI Integration', () => { }); }); + describe('--agents-md flag without a Clerk SDK', () => { + let fixture; + + beforeEach(() => { + fixture = createTempFixture('no-clerk'); + }); + + afterEach(() => { + fixture?.cleanup(); + }); + + it('exits with a failure status and writes nothing', async () => { + const fs = await import('node:fs'); + const result = await runCli(['--dir', fixture.path, '--agents-md'], { timeout: 15000 }); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain('No Clerk SDK packages found for AGENTS.md'); + expect(fs.existsSync(path.join(fixture.path, 'AGENTS.md'))).toBe(false); + }); + }); + describe('Version Display', () => { it('shows current version in output', async () => { const dir = getFixturePath('nextjs-v6'); diff --git a/packages/upgrade/src/cli.js b/packages/upgrade/src/cli.js index 57b089fc8de..00d76fea1c3 100644 --- a/packages/upgrade/src/cli.js +++ b/packages/upgrade/src/cli.js @@ -246,13 +246,6 @@ async function main() { console.log(''); - if (isInteractive) { - const sdks = detectClerkAgentRuleSdks(options.dir); - if (sdks.length > 0 && (await promptConfirm('Write Clerk AGENTS.md sections?', true))) { - writeAgentsMd(options, sdks); - } - } - // Step 6: Handle upgrade status if (options.skipUpgrade) { renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow'); @@ -268,6 +261,13 @@ async function main() { await performPackageReplacements(packageManager, config, options); } + if (isInteractive) { + const sdks = detectClerkAgentRuleSdks(options.dir); + if (sdks.length > 0 && (await promptConfirm('Write Clerk AGENTS.md sections?', true))) { + writeAgentsMd(options, sdks); + } + } + // Step 7: Run codemods if (config.codemods?.length > 0) { renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue'); @@ -292,6 +292,7 @@ function writeAgentsMd(options, detectedSdks) { if (sdks.length === 0) { renderWarning('No Clerk SDK packages found for AGENTS.md'); + process.exitCode = 1; return; } diff --git a/packages/upgrade/src/util/agent-rules.js b/packages/upgrade/src/util/agent-rules.js index 35f96739000..f515080530b 100644 --- a/packages/upgrade/src/util/agent-rules.js +++ b/packages/upgrade/src/util/agent-rules.js @@ -7,7 +7,7 @@ import { findWorkspaceRoot, getInstalledClerkPackages, getWorkspacePackageDirs } export function detectClerkAgentRuleSdks(dir) { const workspaceRoot = findWorkspaceRoot(dir); - const packageDirs = workspaceRoot ? getWorkspacePackageDirs(workspaceRoot) : [dir]; + const packageDirs = workspaceRoot ? [workspaceRoot, ...getWorkspacePackageDirs(workspaceRoot)] : [dir]; const sdks = new Set(); for (const packageDir of packageDirs) { From 3bacc8ea2cad1dfd48bc0136ad12cb13501f741f Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 14:59:06 -0400 Subject: [PATCH 10/17] feat(*): write Clerk agent rules on every dev run, not only under an agent Co-Authored-By: Claude Opus 5.5 --- .changeset/react-vue-agent-rules-vite.md | 2 +- .changeset/shared-agent-rules.md | 2 +- .changeset/write-agents-md-in-dev.md | 2 +- .../src/integration/create-integration.ts | 4 +- packages/expo/app.plugin.js | 7 +-- .../__tests__/appPlugin.agentRules.test.js | 14 +++--- .../server/__tests__/agent-rules-node.test.ts | 30 +++++------ .../nextjs/src/server/agent-rules-node.ts | 4 +- packages/nuxt/src/module.ts | 4 +- .../src/__tests__/runtimeEnvironment.spec.ts | 43 ++++++++++++++++ packages/shared/src/__tests__/vite.spec.ts | 50 +++++++++++++++++++ .../shared/src/utils/runtimeEnvironment.ts | 5 ++ packages/shared/src/vite.ts | 4 +- .../src/server/__tests__/agentRules.test.ts | 30 +++++------ .../src/server/agentRules.ts | 4 +- 15 files changed, 151 insertions(+), 54 deletions(-) create mode 100644 packages/shared/src/__tests__/vite.spec.ts diff --git a/.changeset/react-vue-agent-rules-vite.md b/.changeset/react-vue-agent-rules-vite.md index a153d58b062..a85f7531d12 100644 --- a/.changeset/react-vue-agent-rules-vite.md +++ b/.changeset/react-vue-agent-rules-vite.md @@ -3,7 +3,7 @@ '@clerk/vue': minor --- -Add Vite plugins that write a Clerk section to the project's `AGENTS.md` when the Vite dev server runs under an AI coding agent. Nothing is written during builds or in CI. +Add Vite plugins that write a Clerk section to the project's `AGENTS.md` when the Vite dev server starts. Nothing is written during builds or in CI, and setting `CLERK_AGENTS_MD=false` turns it off. ```ts import { clerkReactAgentRules } from '@clerk/react/vite'; diff --git a/.changeset/shared-agent-rules.md b/.changeset/shared-agent-rules.md index 886015e3be2..9821115b54b 100644 --- a/.changeset/shared-agent-rules.md +++ b/.changeset/shared-agent-rules.md @@ -2,4 +2,4 @@ '@clerk/shared': minor --- -Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`, and `isAgentEnvironment()` to `@clerk/shared/utils` for detecting AI coding agents. +Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`. Add `isClerkAgentRulesWriteAllowed()` to `@clerk/shared/utils`, which returns `false` in production, in CI, or when `CLERK_AGENTS_MD=false`, and `isAgentEnvironment()` for detecting AI coding agents. diff --git a/.changeset/write-agents-md-in-dev.md b/.changeset/write-agents-md-in-dev.md index 52672b11693..e67f99f100f 100644 --- a/.changeset/write-agents-md-in-dev.md +++ b/.changeset/write-agents-md-in-dev.md @@ -6,4 +6,4 @@ '@clerk/tanstack-react-start': minor --- -When your app runs in development under an AI coding agent (such as Claude Code, Cursor, or GitHub Copilot), Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed. It also creates a `CLAUDE.md` that imports `AGENTS.md`, or adds an `@AGENTS.md` import to an existing `CLAUDE.md` that doesn't load it. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. +When your app runs in development, Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed. It also creates a `CLAUDE.md` that imports `AGENTS.md`, or adds an `@AGENTS.md` import to an existing `CLAUDE.md` that doesn't load it. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI, and setting `CLERK_AGENTS_MD=false` turns it off. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 944cb3acaba..aaaee763693 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { ClerkOptions } from '@clerk/shared/types'; -import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; @@ -39,7 +39,7 @@ function createIntegration() name: '@clerk/astro/integration', hooks: { 'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => { - if (command === 'dev' && !didWriteAgentRules && !isAutomatedEnvironment() && isAgentEnvironment()) { + if (command === 'dev' && !didWriteAgentRules && isClerkAgentRulesWriteAllowed()) { didWriteAgentRules = true; try { writeClerkAgentRules({ cwd: fileURLToPath(config.root), fs, path, sdks: ['astro'] }); diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index 80cffc3dc56..de29d46eaa2 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -7,7 +7,8 @@ * 3. If @clerk/expo-native-components is installed, its config plugin is applied for the Clerk native SDKs */ const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins'); -const { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } = require('@clerk/shared/utils'); +const { writeClerkAgentRules } = require('@clerk/shared/agentRules'); +const { isClerkAgentRulesWriteAllowed } = require('@clerk/shared/utils'); const fs = require('fs'); const path = require('path'); @@ -17,7 +18,7 @@ const CLERK_EXPO_NATIVE_OPTIONS = ['keychainService', 'theme']; let didWriteAgentRules = false; const maybeWriteAgentRules = config => { - if (didWriteAgentRules || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { + if (didWriteAgentRules || !isClerkAgentRulesWriteAllowed()) { return; } @@ -29,7 +30,7 @@ const maybeWriteAgentRules = config => { didWriteAgentRules = true; try { - require('@clerk/shared/agentRules').writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] }); + writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] }); } catch { // Writing agent hints must never break Expo config evaluation. } diff --git a/packages/expo/src/__tests__/appPlugin.agentRules.test.js b/packages/expo/src/__tests__/appPlugin.agentRules.test.js index 29f4a2af242..90ee8c0cfaa 100644 --- a/packages/expo/src/__tests__/appPlugin.agentRules.test.js +++ b/packages/expo/src/__tests__/appPlugin.agentRules.test.js @@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; -import { agentEnvironmentVariables, automatedEnvironmentVariables } from '@clerk/shared/utils'; +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; const nodeRequire = createRequire(import.meta.url); @@ -20,9 +20,10 @@ describe('maybeWriteAgentRules', () => { beforeEach(() => { projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-expo-agent-rules-')); fs.writeFileSync(path.join(projectRoot, 'package.json'), '{}'); - for (const name of [...agentEnvironmentVariables, ...automatedEnvironmentVariables]) { + for (const name of automatedEnvironmentVariables) { vi.stubEnv(name, ''); } + vi.stubEnv('CLERK_AGENTS_MD', ''); vi.stubEnv('NODE_ENV', 'development'); }); @@ -34,8 +35,7 @@ describe('maybeWriteAgentRules', () => { const agentsMdPath = () => path.join(projectRoot, 'AGENTS.md'); const config = () => ({ _internal: { projectRoot } }); - test('writes the Expo rules once per process under an AI agent', () => { - vi.stubEnv('CLAUDECODE', '1'); + test('writes the Expo rules once per process in development, with or without an AI agent', () => { const maybeWriteAgentRules = loadMaybeWriteAgentRules(); maybeWriteAgentRules(config()); @@ -46,8 +46,8 @@ describe('maybeWriteAgentRules', () => { expect(fs.existsSync(agentsMdPath())).toBe(false); }); - test('skips when the agent variable is set to a false value', () => { - vi.stubEnv('CLAUDECODE', 'False'); + test('skips when CLERK_AGENTS_MD is false', () => { + vi.stubEnv('CLERK_AGENTS_MD', 'False'); loadMaybeWriteAgentRules()(config()); @@ -55,7 +55,6 @@ describe('maybeWriteAgentRules', () => { }); test('skips in production', () => { - vi.stubEnv('CLAUDECODE', '1'); vi.stubEnv('NODE_ENV', 'production'); loadMaybeWriteAgentRules()(config()); @@ -64,7 +63,6 @@ describe('maybeWriteAgentRules', () => { }); test('skips in CI', () => { - vi.stubEnv('CLAUDECODE', '1'); vi.stubEnv('CI', 'true'); loadMaybeWriteAgentRules()(config()); diff --git a/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts index f49b1854b67..ead72cbbe4a 100644 --- a/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts +++ b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts @@ -1,12 +1,10 @@ import { writeClerkAgentRules } from '@clerk/shared/agentRules'; -import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@clerk/shared/agentRules', () => ({ writeClerkAgentRules: vi.fn() })); -vi.mock('@clerk/shared/utils', async importOriginal => ({ - ...(await importOriginal()), - isAgentEnvironment: vi.fn(), - isAutomatedEnvironment: vi.fn(), +vi.mock('@clerk/shared/agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), })); vi.mock('../fs/utils', () => ({ nodeCwdOrThrow: () => () => '/app', @@ -20,8 +18,10 @@ describe('writeAgentRulesOnce', () => { beforeEach(() => { vi.resetModules(); vi.mocked(writeClerkAgentRules).mockReset(); - vi.mocked(isAgentEnvironment).mockReturnValue(true); - vi.mocked(isAutomatedEnvironment).mockReturnValue(false); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); vi.stubEnv('NODE_ENV', 'development'); }); @@ -29,7 +29,7 @@ describe('writeAgentRulesOnce', () => { vi.unstubAllEnvs(); }); - it('writes the Next.js rules once per process', async () => { + it('writes the rules once per process in development, with or without an AI agent', async () => { const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); writeAgentRulesOnce(); @@ -39,24 +39,24 @@ describe('writeAgentRulesOnce', () => { expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app', sdks: ['nextjs'] })); }); - it('skips outside an agent environment', async () => { - vi.mocked(isAgentEnvironment).mockReturnValue(false); + it('skips in CI', async () => { + vi.stubEnv('CI', 'true'); (await loadWriteAgentRulesOnce())(); expect(writeClerkAgentRules).not.toHaveBeenCalled(); }); - it('skips in CI', async () => { - vi.mocked(isAutomatedEnvironment).mockReturnValue(true); + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); (await loadWriteAgentRulesOnce())(); expect(writeClerkAgentRules).not.toHaveBeenCalled(); }); - it('skips in production', async () => { - vi.stubEnv('NODE_ENV', 'production'); + it('skips when CLERK_AGENTS_MD is false', async () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); (await loadWriteAgentRulesOnce())(); diff --git a/packages/nextjs/src/server/agent-rules-node.ts b/packages/nextjs/src/server/agent-rules-node.ts index 33e528be6cd..fdbf0a94407 100644 --- a/packages/nextjs/src/server/agent-rules-node.ts +++ b/packages/nextjs/src/server/agent-rules-node.ts @@ -1,12 +1,12 @@ import { writeClerkAgentRules } from '@clerk/shared/agentRules'; -import { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } from '@clerk/shared/utils'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './fs/utils'; let didWrite = false; export function writeAgentRulesOnce() { - if (didWrite || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { + if (didWrite || !isClerkAgentRulesWriteAllowed()) { return; } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index fab8d5b63c6..b0a50e18564 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { InternalClerkScriptProps, Without } from '@clerk/shared/types'; -import { isAgentEnvironment, isAutomatedEnvironment } from '@clerk/shared/utils'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import type { PluginOptions } from '@clerk/vue'; import { addComponent, @@ -52,7 +52,7 @@ export default defineNuxtModule({ }, }, setup(options, nuxt) { - if (nuxt.options.dev && !didWriteAgentRules && !isAutomatedEnvironment() && isAgentEnvironment()) { + if (nuxt.options.dev && !didWriteAgentRules && isClerkAgentRulesWriteAllowed()) { didWriteAgentRules = true; try { writeClerkAgentRules({ cwd: nuxt.options.rootDir, fs, path, sdks: ['nuxt'] }); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index ee171ca8ec5..29953924557 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -5,6 +5,7 @@ import { automatedEnvironmentVariables, isAgentEnvironment, isAutomatedEnvironment, + isClerkAgentRulesWriteAllowed, } from '../utils/runtimeEnvironment'; describe('isAutomatedEnvironment', () => { @@ -127,3 +128,45 @@ describe('isAgentEnvironment with Cursor', () => { expect(isAgentEnvironment()).toBe(true); }); }); + +describe('isClerkAgentRulesWriteAllowed', () => { + beforeEach(() => { + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('allows writes in development without an AI agent', () => { + expect(isClerkAgentRulesWriteAllowed()).toBe(true); + }); + + it('blocks writes in production', () => { + vi.stubEnv('NODE_ENV', 'production'); + + expect(isClerkAgentRulesWriteAllowed()).toBe(false); + }); + + it('blocks writes in CI', () => { + vi.stubEnv('CI', 'true'); + + expect(isClerkAgentRulesWriteAllowed()).toBe(false); + }); + + it.each(['false', 'FALSE', '0', 'off', 'no'])('blocks writes when CLERK_AGENTS_MD is %s', value => { + vi.stubEnv('CLERK_AGENTS_MD', value); + + expect(isClerkAgentRulesWriteAllowed()).toBe(false); + }); + + it('allows writes when CLERK_AGENTS_MD is true', () => { + vi.stubEnv('CLERK_AGENTS_MD', 'true'); + + expect(isClerkAgentRulesWriteAllowed()).toBe(true); + }); +}); diff --git a/packages/shared/src/__tests__/vite.spec.ts b/packages/shared/src/__tests__/vite.spec.ts new file mode 100644 index 00000000000..396650506e2 --- /dev/null +++ b/packages/shared/src/__tests__/vite.spec.ts @@ -0,0 +1,50 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type * as AgentRules from '../agentRules'; +import { writeClerkAgentRules } from '../agentRules'; +import { automatedEnvironmentVariables } from '../utils/runtimeEnvironment'; +import { clerkAgentRulesVitePlugin } from '../vite'; + +vi.mock('../agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), +})); + +describe('clerkAgentRulesVitePlugin', () => { + beforeEach(() => { + vi.mocked(writeClerkAgentRules).mockReset(); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('writes the rules once when the dev server starts', async () => { + const plugin = clerkAgentRulesVitePlugin('react'); + + await plugin.configResolved({ command: 'serve', root: '/app' }); + await plugin.configResolved({ command: 'serve', root: '/app' }); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app', sdks: ['react'] })); + }); + + it('skips builds', async () => { + await clerkAgentRulesVitePlugin('vue').configResolved({ command: 'build', root: '/app' }); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips when CLERK_AGENTS_MD is false', async () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); + + await clerkAgentRulesVitePlugin('react').configResolved({ command: 'serve', root: '/app' }); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 846a37dc5ec..9ecc700744a 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -80,3 +80,8 @@ export const isAgentEnvironment = (): boolean => { return name === 'CURSOR_EXTENSION_HOST_ROLE' ? value === 'agent-exec' : isTruthyEnvValue(value); }); }; + +export const isClerkAgentRulesWriteAllowed = (): boolean => { + const optOut = getEnvVariable('CLERK_AGENTS_MD').toLowerCase(); + return !['0', 'false', 'off', 'no'].includes(optOut) && !isProductionEnvironment() && !isAutomatedEnvironment(); +}; diff --git a/packages/shared/src/vite.ts b/packages/shared/src/vite.ts index 8a06eb8311d..cd040762314 100644 --- a/packages/shared/src/vite.ts +++ b/packages/shared/src/vite.ts @@ -1,5 +1,5 @@ import { type ClerkAgentRulesSdk, writeClerkAgentRules } from './agentRules'; -import { isAgentEnvironment, isAutomatedEnvironment } from './utils'; +import { isClerkAgentRulesWriteAllowed } from './utils'; type ViteResolvedConfig = { command: 'build' | 'serve'; @@ -13,7 +13,7 @@ export function clerkAgentRulesVitePlugin(sdk: Extract ({ writeClerkAgentRules: vi.fn() })); -vi.mock('@clerk/shared/utils', async importOriginal => ({ - ...(await importOriginal()), - isAgentEnvironment: vi.fn(), - isAutomatedEnvironment: vi.fn(), +vi.mock('@clerk/shared/agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), })); const loadWriteAgentRulesOnce = async () => (await import('../agentRules')).writeAgentRulesOnce; @@ -15,8 +13,10 @@ describe('writeAgentRulesOnce', () => { beforeEach(() => { vi.resetModules(); vi.mocked(writeClerkAgentRules).mockReset(); - vi.mocked(isAgentEnvironment).mockReturnValue(true); - vi.mocked(isAutomatedEnvironment).mockReturnValue(false); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); vi.stubEnv('NODE_ENV', 'development'); }); @@ -24,7 +24,7 @@ describe('writeAgentRulesOnce', () => { vi.unstubAllEnvs(); }); - it('writes the TanStack React Start rules once per process', async () => { + it('writes the rules once per process in development, with or without an AI agent', async () => { const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); writeAgentRulesOnce(); @@ -36,24 +36,24 @@ describe('writeAgentRulesOnce', () => { ); }); - it('skips outside an agent environment', async () => { - vi.mocked(isAgentEnvironment).mockReturnValue(false); + it('skips in CI', async () => { + vi.stubEnv('CI', 'true'); (await loadWriteAgentRulesOnce())(); expect(writeClerkAgentRules).not.toHaveBeenCalled(); }); - it('skips in CI', async () => { - vi.mocked(isAutomatedEnvironment).mockReturnValue(true); + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); (await loadWriteAgentRulesOnce())(); expect(writeClerkAgentRules).not.toHaveBeenCalled(); }); - it('skips in production', async () => { - vi.stubEnv('NODE_ENV', 'production'); + it('skips when CLERK_AGENTS_MD is false', async () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); (await loadWriteAgentRulesOnce())(); diff --git a/packages/tanstack-react-start/src/server/agentRules.ts b/packages/tanstack-react-start/src/server/agentRules.ts index b810409c32f..ecec28cf00c 100644 --- a/packages/tanstack-react-start/src/server/agentRules.ts +++ b/packages/tanstack-react-start/src/server/agentRules.ts @@ -1,6 +1,6 @@ import type { FileSystemAdapter, PathAdapter } from '@clerk/shared/agentRules'; import { writeClerkAgentRules } from '@clerk/shared/agentRules'; -import { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } from '@clerk/shared/utils'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; type NodeProcess = { cwd: () => string; @@ -10,7 +10,7 @@ type NodeProcess = { let didWrite = false; export function writeAgentRulesOnce() { - if (didWrite || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) { + if (didWrite || !isClerkAgentRulesWriteAllowed()) { return; } From 4117b1d47744f2c4283af7e3b4d543eaaf505ece Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 15:02:34 -0400 Subject: [PATCH 11/17] refactor(*): drop isAgentEnvironment and test the Astro and Nuxt agent rules triggers Co-Authored-By: Claude Opus 5.5 --- .changeset/shared-agent-rules.md | 2 +- .../integration/__tests__/agent-rules.test.ts | 71 +++++++++++++++++++ packages/astro/src/integration/agent-rules.ts | 22 ++++++ .../src/integration/create-integration.ts | 18 +---- .../nuxt/src/__tests__/agent-rules.test.ts | 71 +++++++++++++++++++ packages/nuxt/src/agent-rules.ts | 21 ++++++ packages/nuxt/src/module.ts | 18 ++--- .../src/__tests__/runtimeEnvironment.spec.ts | 64 ----------------- .../shared/src/utils/runtimeEnvironment.ts | 19 ----- 9 files changed, 193 insertions(+), 113 deletions(-) create mode 100644 packages/astro/src/integration/__tests__/agent-rules.test.ts create mode 100644 packages/astro/src/integration/agent-rules.ts create mode 100644 packages/nuxt/src/__tests__/agent-rules.test.ts create mode 100644 packages/nuxt/src/agent-rules.ts diff --git a/.changeset/shared-agent-rules.md b/.changeset/shared-agent-rules.md index 9821115b54b..1f0b9e144a4 100644 --- a/.changeset/shared-agent-rules.md +++ b/.changeset/shared-agent-rules.md @@ -2,4 +2,4 @@ '@clerk/shared': minor --- -Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`. Add `isClerkAgentRulesWriteAllowed()` to `@clerk/shared/utils`, which returns `false` in production, in CI, or when `CLERK_AGENTS_MD=false`, and `isAgentEnvironment()` for detecting AI coding agents. +Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`. Add `isClerkAgentRulesWriteAllowed()` to `@clerk/shared/utils`, which returns `false` in production, in CI, or when `CLERK_AGENTS_MD=false`. diff --git a/packages/astro/src/integration/__tests__/agent-rules.test.ts b/packages/astro/src/integration/__tests__/agent-rules.test.ts new file mode 100644 index 00000000000..0ff0d61427c --- /dev/null +++ b/packages/astro/src/integration/__tests__/agent-rules.test.ts @@ -0,0 +1,71 @@ +import type * as AgentRules from '@clerk/shared/agentRules'; +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@clerk/shared/agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), +})); + +const loadWriteAgentRulesOnce = async () => (await import('../agent-rules')).writeAgentRulesOnce; + +describe('writeAgentRulesOnce', () => { + beforeEach(() => { + vi.resetModules(); + vi.mocked(writeClerkAgentRules).mockReset(); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('writes the Astro rules once per process', async () => { + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + writeAgentRulesOnce(new URL('file:///app/')); + writeAgentRulesOnce(new URL('file:///app/')); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app/', sdks: ['astro'] })); + }); + + it('skips in CI', async () => { + vi.stubEnv('CI', 'true'); + + (await loadWriteAgentRulesOnce())(new URL('file:///app/')); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + (await loadWriteAgentRulesOnce())(new URL('file:///app/')); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips when CLERK_AGENTS_MD is false', async () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); + + (await loadWriteAgentRulesOnce())(new URL('file:///app/')); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', async () => { + vi.mocked(writeClerkAgentRules).mockImplementation(() => { + throw new Error('EACCES'); + }); + + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + expect(() => writeAgentRulesOnce(new URL('file:///app/'))).not.toThrow(); + }); +}); diff --git a/packages/astro/src/integration/agent-rules.ts b/packages/astro/src/integration/agent-rules.ts new file mode 100644 index 00000000000..252caef47c5 --- /dev/null +++ b/packages/astro/src/integration/agent-rules.ts @@ -0,0 +1,22 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; + +let didWrite = false; + +export function writeAgentRulesOnce(root: URL) { + if (didWrite || !isClerkAgentRulesWriteAllowed()) { + return; + } + + didWrite = true; + + try { + writeClerkAgentRules({ cwd: fileURLToPath(root), fs, path, sdks: ['astro'] }); + } catch { + // Writing agent hints must never break a dev server. + } +} diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index aaaee763693..48fd5f07078 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -1,15 +1,10 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { ClerkOptions } from '@clerk/shared/types'; -import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; import { name as packageName, version as packageVersion } from '../../package.json'; import type { AstroClerkIntegrationParams } from '../types'; +import { writeAgentRulesOnce } from './agent-rules'; import { buildBeforeHydrationSnippet, buildPageLoadSnippet } from './snippets'; import { vitePluginAstroConfig } from './vite-plugin-astro-config'; @@ -17,8 +12,6 @@ const buildEnvVarFromOption = (valueToBeStored: unknown, envName: keyof Internal return valueToBeStored ? { [`import.meta.env.${envName}`]: JSON.stringify(valueToBeStored) } : {}; }; -let didWriteAgentRules = false; - type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & { enableEnvSchema?: boolean; }; @@ -39,13 +32,8 @@ function createIntegration() name: '@clerk/astro/integration', hooks: { 'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => { - if (command === 'dev' && !didWriteAgentRules && isClerkAgentRulesWriteAllowed()) { - didWriteAgentRules = true; - try { - writeClerkAgentRules({ cwd: fileURLToPath(config.root), fs, path, sdks: ['astro'] }); - } catch { - // Writing agent hints must never break a dev server. - } + if (command === 'dev') { + writeAgentRulesOnce(config.root); } if (['server', 'hybrid'].includes(config.output) && !config.adapter) { diff --git a/packages/nuxt/src/__tests__/agent-rules.test.ts b/packages/nuxt/src/__tests__/agent-rules.test.ts new file mode 100644 index 00000000000..90c11508dab --- /dev/null +++ b/packages/nuxt/src/__tests__/agent-rules.test.ts @@ -0,0 +1,71 @@ +import type * as AgentRules from '@clerk/shared/agentRules'; +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@clerk/shared/agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), +})); + +const loadWriteAgentRulesOnce = async () => (await import('../agent-rules')).writeAgentRulesOnce; + +describe('writeAgentRulesOnce', () => { + beforeEach(() => { + vi.resetModules(); + vi.mocked(writeClerkAgentRules).mockReset(); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('writes the Nuxt rules once per process', async () => { + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + writeAgentRulesOnce('/app'); + writeAgentRulesOnce('/app'); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app', sdks: ['nuxt'] })); + }); + + it('skips in CI', async () => { + vi.stubEnv('CI', 'true'); + + (await loadWriteAgentRulesOnce())('/app'); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in production', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + (await loadWriteAgentRulesOnce())('/app'); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips when CLERK_AGENTS_MD is false', async () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); + + (await loadWriteAgentRulesOnce())('/app'); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', async () => { + vi.mocked(writeClerkAgentRules).mockImplementation(() => { + throw new Error('EACCES'); + }); + + const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); + + expect(() => writeAgentRulesOnce('/app')).not.toThrow(); + }); +}); diff --git a/packages/nuxt/src/agent-rules.ts b/packages/nuxt/src/agent-rules.ts new file mode 100644 index 00000000000..4eb6a9c92f2 --- /dev/null +++ b/packages/nuxt/src/agent-rules.ts @@ -0,0 +1,21 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; + +let didWrite = false; + +export function writeAgentRulesOnce(rootDir: string) { + if (didWrite || !isClerkAgentRulesWriteAllowed()) { + return; + } + + didWrite = true; + + try { + writeClerkAgentRules({ cwd: rootDir, fs, path, sdks: ['nuxt'] }); + } catch { + // Writing agent hints must never break a dev server. + } +} diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index b0a50e18564..b0809209bea 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,9 +1,4 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; import type { InternalClerkScriptProps, Without } from '@clerk/shared/types'; -import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import type { PluginOptions } from '@clerk/vue'; import { addComponent, @@ -17,6 +12,8 @@ import { updateRuntimeConfig, } from '@nuxt/kit'; +import { writeAgentRulesOnce } from './agent-rules'; + export type ModuleOptions = Without & InternalClerkScriptProps & { publishableKey?: string; @@ -40,8 +37,6 @@ export type ModuleOptions = Without({ meta: { name: PACKAGE_NAME, @@ -52,13 +47,8 @@ export default defineNuxtModule({ }, }, setup(options, nuxt) { - if (nuxt.options.dev && !didWriteAgentRules && isClerkAgentRulesWriteAllowed()) { - didWriteAgentRules = true; - try { - writeClerkAgentRules({ cwd: nuxt.options.rootDir, fs, path, sdks: ['nuxt'] }); - } catch { - // Writing agent hints must never break a dev server. - } + if (nuxt.options.dev) { + writeAgentRulesOnce(nuxt.options.rootDir); } // These values must be set (even as undefined) to allow runtime config to work properly. diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 29953924557..750787a5ca7 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -1,9 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { - agentEnvironmentVariables, automatedEnvironmentVariables, - isAgentEnvironment, isAutomatedEnvironment, isClerkAgentRulesWriteAllowed, } from '../utils/runtimeEnvironment'; @@ -67,68 +65,6 @@ describe('isAutomatedEnvironment', () => { }); }); -describe('isAgentEnvironment', () => { - beforeEach(() => { - agentEnvironmentVariables.forEach(name => { - vi.stubEnv(name, undefined); - vi.stubGlobal(name, undefined); - }); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - vi.unstubAllGlobals(); - }); - - it('returns true when an agent environment variable is enabled', () => { - vi.stubEnv('CLAUDECODE', 'true'); - - expect(isAgentEnvironment()).toBe(true); - }); - - it('returns false when agent environment variables are explicitly falsey', () => { - const falseyValues = ['false', '0', 'off', 'no']; - - agentEnvironmentVariables.forEach((name, index) => { - vi.stubEnv(name, falseyValues[index % falseyValues.length]); - }); - - expect(isAgentEnvironment()).toBe(false); - }); -}); - -describe('isAgentEnvironment with Cursor', () => { - beforeEach(() => { - agentEnvironmentVariables.forEach(name => { - vi.stubEnv(name, undefined); - vi.stubGlobal(name, undefined); - }); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - vi.unstubAllGlobals(); - }); - - it('returns true when CURSOR_EXTENSION_HOST_ROLE is agent-exec', () => { - vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'agent-exec'); - - expect(isAgentEnvironment()).toBe(true); - }); - - it('returns false for any other CURSOR_EXTENSION_HOST_ROLE value', () => { - vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'user'); - - expect(isAgentEnvironment()).toBe(false); - }); - - it('returns true when CURSOR_TRACE_ID is set', () => { - vi.stubEnv('CURSOR_TRACE_ID', 'abc123'); - - expect(isAgentEnvironment()).toBe(true); - }); -}); - describe('isClerkAgentRulesWriteAllowed', () => { beforeEach(() => { for (const name of automatedEnvironmentVariables) { diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 9ecc700744a..b949c86e202 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -19,18 +19,6 @@ export const automatedEnvironmentVariables = [ 'CF_PAGES', ] as const; -export const agentEnvironmentVariables = [ - 'CLAUDECODE', - 'CURSOR_AGENT', - 'CURSOR_CLI', - 'CURSOR_EXTENSION_HOST_ROLE', - 'CURSOR_TRACE_ID', - 'GITHUB_COPILOT_AGENT', - 'GITHUB_COPILOT_CHAT', - 'AGENT', - 'AGENTS', -] as const; - const isTruthyEnvValue = (value: unknown): boolean => { if (typeof value !== 'string' || !value) { return false; @@ -74,13 +62,6 @@ export const isAutomatedEnvironment = (): boolean => { return automatedEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name))); }; -export const isAgentEnvironment = (): boolean => { - return agentEnvironmentVariables.some(name => { - const value = getEnvVariable(name); - return name === 'CURSOR_EXTENSION_HOST_ROLE' ? value === 'agent-exec' : isTruthyEnvValue(value); - }); -}; - export const isClerkAgentRulesWriteAllowed = (): boolean => { const optOut = getEnvVariable('CLERK_AGENTS_MD').toLowerCase(); return !['0', 'false', 'off', 'no'].includes(optOut) && !isProductionEnvironment() && !isAutomatedEnvironment(); From 7127b70d41877b97e8ade5a31d52b231ec2e3836 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 15:33:59 -0400 Subject: [PATCH 12/17] feat(*): write Clerk agent rules from server middleware, Electron, and Next.js clerkMiddleware Co-Authored-By: Claude Opus 5.5 --- .changeset/react-vue-agent-rules-vite.md | 2 +- .changeset/shared-agent-rules.md | 2 +- .changeset/write-agents-md-in-dev.md | 7 +- .../src/main/__tests__/agent-rules.test.ts | 49 +++++++++++ packages/electron/src/main/agent-rules.ts | 16 ++++ .../electron/src/main/create-clerk-bridge.ts | 3 + .../express/src/__tests__/agentRules.test.ts | 26 ++++++ packages/express/src/clerkMiddleware.ts | 5 ++ .../fastify/src/__tests__/agentRules.test.ts | 30 +++++++ packages/fastify/src/clerkPlugin.ts | 5 ++ .../hono/src/__tests__/agentRules.test.ts | 26 ++++++ packages/hono/src/clerkMiddleware.ts | 5 ++ .../clerkMiddleware.agentRules.test.ts | 39 +++++++++ packages/nextjs/src/server/clerkMiddleware.ts | 2 + .../src/server/__tests__/agentRules.test.ts | 26 ++++++ .../src/server/clerkMiddleware.ts | 5 ++ packages/shared/package.json | 10 +++ .../src/__tests__/agentRulesWriter.spec.ts | 85 +++++++++++++++++++ .../src/__tests__/runtimeEnvironment.spec.ts | 12 +++ packages/shared/src/agentRulesWriter.ts | 32 +++++++ .../shared/src/utils/runtimeEnvironment.ts | 9 +- .../src/server/__tests__/agentRules.test.ts | 77 +++-------------- .../src/server/agentRules.ts | 32 +------ 23 files changed, 406 insertions(+), 99 deletions(-) create mode 100644 packages/electron/src/main/__tests__/agent-rules.test.ts create mode 100644 packages/electron/src/main/agent-rules.ts create mode 100644 packages/express/src/__tests__/agentRules.test.ts create mode 100644 packages/fastify/src/__tests__/agentRules.test.ts create mode 100644 packages/hono/src/__tests__/agentRules.test.ts create mode 100644 packages/nextjs/src/server/__tests__/clerkMiddleware.agentRules.test.ts create mode 100644 packages/react-router/src/server/__tests__/agentRules.test.ts create mode 100644 packages/shared/src/__tests__/agentRulesWriter.spec.ts create mode 100644 packages/shared/src/agentRulesWriter.ts diff --git a/.changeset/react-vue-agent-rules-vite.md b/.changeset/react-vue-agent-rules-vite.md index a85f7531d12..fe94fdb6157 100644 --- a/.changeset/react-vue-agent-rules-vite.md +++ b/.changeset/react-vue-agent-rules-vite.md @@ -3,7 +3,7 @@ '@clerk/vue': minor --- -Add Vite plugins that write a Clerk section to the project's `AGENTS.md` when the Vite dev server starts. Nothing is written during builds or in CI, and setting `CLERK_AGENTS_MD=false` turns it off. +Add Vite plugins that write a Clerk section to the project's `AGENTS.md` when the Vite dev server starts. Nothing is written during builds, tests, or CI, and setting `CLERK_AGENTS_MD=false` turns it off. ```ts import { clerkReactAgentRules } from '@clerk/react/vite'; diff --git a/.changeset/shared-agent-rules.md b/.changeset/shared-agent-rules.md index 1f0b9e144a4..9e5e5db0e69 100644 --- a/.changeset/shared-agent-rules.md +++ b/.changeset/shared-agent-rules.md @@ -2,4 +2,4 @@ '@clerk/shared': minor --- -Add `@clerk/shared/agentRules` and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`. Add `isClerkAgentRulesWriteAllowed()` to `@clerk/shared/utils`, which returns `false` in production, in CI, or when `CLERK_AGENTS_MD=false`. +Add `@clerk/shared/agentRules`, `@clerk/shared/agentRulesWriter`, and `@clerk/shared/vite` for writing Clerk sections to a project's `AGENTS.md`. Add `isClerkAgentRulesWriteAllowed()` to `@clerk/shared/utils`, which returns `false` in production, in tests, in CI, or when `CLERK_AGENTS_MD=false`. diff --git a/.changeset/write-agents-md-in-dev.md b/.changeset/write-agents-md-in-dev.md index e67f99f100f..dda80b4218d 100644 --- a/.changeset/write-agents-md-in-dev.md +++ b/.changeset/write-agents-md-in-dev.md @@ -1,9 +1,14 @@ --- '@clerk/astro': minor +'@clerk/electron': minor '@clerk/expo': minor +'@clerk/express': minor +'@clerk/fastify': minor +'@clerk/hono': minor '@clerk/nextjs': minor '@clerk/nuxt': minor +'@clerk/react-router': minor '@clerk/tanstack-react-start': minor --- -When your app runs in development, Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed. It also creates a `CLAUDE.md` that imports `AGENTS.md`, or adds an `@AGENTS.md` import to an existing `CLAUDE.md` that doesn't load it. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production or CI, and setting `CLERK_AGENTS_MD=false` turns it off. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call in Next.js, and TanStack React Start's `clerkMiddleware()`. +When your app runs in development, Clerk now adds a Clerk section to the project's root `AGENTS.md`, creating the file if needed. It also creates a `CLAUDE.md` that imports `AGENTS.md`, or adds an `@AGENTS.md` import to an existing `CLAUDE.md` that doesn't load it. The section tells the agent to read `node_modules/@clerk//AGENTS.md` before writing auth code and recommends the Clerk plugin (`npx plugins add clerk/skills`). Nothing is written in production, tests, or CI, and setting `CLERK_AGENTS_MD=false` turns it off. The section is written by the Astro and Nuxt dev servers, the Expo config plugin, the first `auth()` call or `clerkMiddleware()` request in Next.js, TanStack React Start's `clerkMiddleware()`, `clerkMiddleware()` in Express, Hono, and React Router, Fastify's `clerkPlugin`, and `createClerkBridge()` in an unpackaged Electron app. diff --git a/packages/electron/src/main/__tests__/agent-rules.test.ts b/packages/electron/src/main/__tests__/agent-rules.test.ts new file mode 100644 index 00000000000..e4e36c2f280 --- /dev/null +++ b/packages/electron/src/main/__tests__/agent-rules.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { writeAgentRulesOnce } from '../agent-rules'; + +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); + +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); + +const electronApp = vi.hoisted(() => ({ isPackaged: false, getAppPath: () => '/projects/my-app' })); + +vi.mock('electron', () => ({ app: electronApp })); + +describe('writeAgentRulesOnce', () => { + beforeEach(() => { + writeAgentRules.mockReset(); + electronApp.isPackaged = false; + }); + + it('creates a writer for the electron SDK', () => { + expect(createdFor).toEqual(['electron']); + }); + + it('writes to the app path when the app is not packaged', () => { + writeAgentRulesOnce(); + + expect(writeAgentRules).toHaveBeenCalledWith('/projects/my-app'); + }); + + it('skips packaged apps', () => { + electronApp.isPackaged = true; + + writeAgentRulesOnce(); + + expect(writeAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', () => { + writeAgentRules.mockImplementation(() => { + throw new Error('EACCES'); + }); + + expect(() => writeAgentRulesOnce()).not.toThrow(); + }); +}); diff --git a/packages/electron/src/main/agent-rules.ts b/packages/electron/src/main/agent-rules.ts new file mode 100644 index 00000000000..75cb2c02e1b --- /dev/null +++ b/packages/electron/src/main/agent-rules.ts @@ -0,0 +1,16 @@ +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; +import { app } from 'electron'; + +const writeAgentRules = createClerkAgentRulesWriter('electron'); + +export function writeAgentRulesOnce() { + if (app.isPackaged) { + return; + } + + try { + writeAgentRules(app.getAppPath()); + } catch { + return; + } +} diff --git a/packages/electron/src/main/create-clerk-bridge.ts b/packages/electron/src/main/create-clerk-bridge.ts index e65a5ae860b..15b444ad6ff 100644 --- a/packages/electron/src/main/create-clerk-bridge.ts +++ b/packages/electron/src/main/create-clerk-bridge.ts @@ -1,6 +1,7 @@ import { app, protocol } from 'electron'; import type { ClerkBridge, CreateClerkBridgeOptions } from '../shared/types'; +import { writeAgentRulesOnce } from './agent-rules'; import { setupTokenCacheIpcHandlers } from './ipc-handlers'; import { setupMissingRendererOAuthTransportIpcHandlers, setupOAuthTransportIpcHandlers } from './oauth-transport'; import { setupPasskeysMain } from './passkey-handlers'; @@ -48,6 +49,8 @@ export function createClerkBridge(options: CreateClerkBridgeOptions): ClerkBridg ); } + writeAgentRulesOnce(); + let ownsSingleInstanceLock = false; if (options.renderer) { diff --git a/packages/express/src/__tests__/agentRules.test.ts b/packages/express/src/__tests__/agentRules.test.ts new file mode 100644 index 00000000000..7839d1df615 --- /dev/null +++ b/packages/express/src/__tests__/agentRules.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { clerkMiddleware } from '../clerkMiddleware'; + +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); + +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); + +describe('Clerk agent rules', () => { + it('creates a writer for the express SDK', () => { + expect(createdFor).toEqual(['express']); + }); + + it('writes the agent rules when the app sets up Clerk', () => { + writeAgentRules.mockClear(); + + clerkMiddleware(); + + expect(writeAgentRules).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/express/src/clerkMiddleware.ts b/packages/express/src/clerkMiddleware.ts index 43a86437461..b16835d1da0 100644 --- a/packages/express/src/clerkMiddleware.ts +++ b/packages/express/src/clerkMiddleware.ts @@ -1,8 +1,11 @@ +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; import type { RequestHandler } from 'express'; import { authenticateAndDecorateRequest } from './authenticateRequest'; import type { ClerkMiddlewareOptions, ClerkMiddlewareOptionsCallback } from './types'; +const writeAgentRulesOnce = createClerkAgentRulesWriter('express'); + /** * Middleware that integrates Clerk authentication into your Express application. * It checks the request's cookies and headers for a session JWT and, if found, @@ -31,6 +34,8 @@ import type { ClerkMiddlewareOptions, ClerkMiddlewareOptionsCallback } from './t export const clerkMiddleware = ( options: ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback = {}, ): RequestHandler => { + writeAgentRulesOnce(); + if (typeof options !== 'function') { const authMiddleware = authenticateAndDecorateRequest({ ...options, diff --git a/packages/fastify/src/__tests__/agentRules.test.ts b/packages/fastify/src/__tests__/agentRules.test.ts new file mode 100644 index 00000000000..59afc4bb215 --- /dev/null +++ b/packages/fastify/src/__tests__/agentRules.test.ts @@ -0,0 +1,30 @@ +import Fastify from 'fastify'; +import { describe, expect, it, vi } from 'vitest'; + +import { clerkPlugin } from '../clerkPlugin'; + +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); + +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); + +describe('Clerk agent rules', () => { + it('creates a writer for the fastify SDK', () => { + expect(createdFor).toEqual(['fastify']); + }); + + it('writes the agent rules when the plugin is registered', async () => { + writeAgentRules.mockClear(); + const fastify = Fastify(); + + await fastify.register(clerkPlugin, { publishableKey: 'pk_test_xxx', secretKey: 'sk_test_xxx' }); + await fastify.ready(); + + expect(writeAgentRules).toHaveBeenCalledTimes(1); + await fastify.close(); + }); +}); diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index d6e95f6bb20..d5237857087 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -1,3 +1,4 @@ +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; import type { FastifyInstance, FastifyPluginCallback } from 'fastify'; import fp from 'fastify-plugin'; @@ -5,11 +6,15 @@ import type { ClerkFastifyOptions } from './types'; import { ALLOWED_HOOKS } from './types'; import { withClerkMiddleware } from './withClerkMiddleware'; +const writeAgentRulesOnce = createClerkAgentRulesWriter('fastify'); + const plugin: FastifyPluginCallback = ( instance: FastifyInstance, opts: ClerkFastifyOptions, done, ) => { + writeAgentRulesOnce(); + instance.decorateRequest('auth', null); instance.decorateRequest('clerk', null as any); diff --git a/packages/hono/src/__tests__/agentRules.test.ts b/packages/hono/src/__tests__/agentRules.test.ts new file mode 100644 index 00000000000..7e448c65754 --- /dev/null +++ b/packages/hono/src/__tests__/agentRules.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { clerkMiddleware } from '../clerkMiddleware'; + +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); + +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); + +describe('Clerk agent rules', () => { + it('creates a writer for the hono SDK', () => { + expect(createdFor).toEqual(['hono']); + }); + + it('writes the agent rules when the app sets up Clerk', () => { + writeAgentRules.mockClear(); + + clerkMiddleware(); + + expect(writeAgentRules).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/hono/src/clerkMiddleware.ts b/packages/hono/src/clerkMiddleware.ts index 67bf53686c5..73a1930c203 100644 --- a/packages/hono/src/clerkMiddleware.ts +++ b/packages/hono/src/clerkMiddleware.ts @@ -3,11 +3,14 @@ import { createClerkClient } from '@clerk/backend'; import type { AuthenticateRequestOptions, AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal'; import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; import { clerkFrontendApiProxy, DEFAULT_PROXY_PATH, matchProxyPath, stripTrailingSlashes } from '@clerk/backend/proxy'; +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; import type { MiddlewareHandler } from 'hono'; import { env } from 'hono/adapter'; import type { FrontendApiProxyOptions } from './types'; +const writeAgentRulesOnce = createClerkAgentRulesWriter('hono'); + type ClerkEnv = { CLERK_SECRET_KEY: string; CLERK_PUBLISHABLE_KEY: string; @@ -39,6 +42,8 @@ export type ClerkMiddlewareOptions = Omit { + writeAgentRulesOnce(); + return async (c, next) => { const clerkEnv = env(c); const { diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.agentRules.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.agentRules.test.ts new file mode 100644 index 00000000000..c7ac2d75045 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.agentRules.test.ts @@ -0,0 +1,39 @@ +import { TokenType } from '@clerk/backend/internal'; +import type { NextFetchEvent } from 'next/server'; +import { NextRequest } from 'next/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { writeAgentRulesOnce } from '../agent-rules-node'; +import { clerkClient } from '../clerkClient'; +import { clerkMiddleware } from '../clerkMiddleware'; + +vi.mock('../clerkClient'); +vi.mock('../agent-rules-node', () => ({ writeAgentRulesOnce: vi.fn() })); +vi.mock(import('../constants.js'), async importOriginal => ({ + ...(await importOriginal()), + PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', +})); + +describe('clerkMiddleware agent rules', () => { + beforeEach(() => { + vi.mocked(writeAgentRulesOnce).mockClear(); + vi.mocked(clerkClient).mockResolvedValue({ + authenticateRequest: vi.fn().mockResolvedValue({ + toAuth: () => ({ tokenType: TokenType.SessionToken, debug: (d: unknown) => d }), + headers: new Headers(), + publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + }), + // @ts-expect-error - mock + telemetry: { record: vi.fn() }, + }); + }); + + it('writes the agent rules on a request, even when the app never calls auth()', async () => { + const request = new NextRequest('https://www.clerk.com/protected'); + + await clerkMiddleware()(request, {} as NextFetchEvent); + + expect(writeAgentRulesOnce).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index a0e0fb9ceeb..3089d2d7f8b 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -147,6 +147,8 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl const [handler, params] = parseHandlerAndOptions(args); const baseNextMiddleware: NextMiddleware = withLogger('clerkMiddleware', logger => async (request, event) => { + await import('./agent-rules-node.js').then(m => m.writeAgentRulesOnce()).catch(() => undefined); + // Handles the case where `options` is a callback function to dynamically access `NextRequest` const resolvedParams = typeof params === 'function' ? await params(request) : params; diff --git a/packages/react-router/src/server/__tests__/agentRules.test.ts b/packages/react-router/src/server/__tests__/agentRules.test.ts new file mode 100644 index 00000000000..6ef4ab6bf33 --- /dev/null +++ b/packages/react-router/src/server/__tests__/agentRules.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { clerkMiddleware } from '../clerkMiddleware'; + +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); + +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); + +describe('Clerk agent rules', () => { + it('creates a writer for the react-router SDK', () => { + expect(createdFor).toEqual(['react-router']); + }); + + it('writes the agent rules when the app sets up Clerk', () => { + writeAgentRules.mockClear(); + + clerkMiddleware(); + + expect(writeAgentRules).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/react-router/src/server/clerkMiddleware.ts b/packages/react-router/src/server/clerkMiddleware.ts index 7b6e9a81413..a14333e0f8d 100644 --- a/packages/react-router/src/server/clerkMiddleware.ts +++ b/packages/react-router/src/server/clerkMiddleware.ts @@ -1,6 +1,7 @@ import type { AuthObject } from '@clerk/backend'; import type { RequestState } from '@clerk/backend/internal'; import { AuthStatus, constants, createClerkRequest } from '@clerk/backend/internal'; +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; import { patchRequest } from '@clerk/shared/patchRequest'; import type { PendingSessionOptions } from '@clerk/shared/types'; @@ -31,7 +32,11 @@ export const requestStateContext = createContext => { + writeAgentRulesOnce(); + return async (args, next) => { const clerkRequest = createClerkRequest(patchRequest(args.request)); const loadedOptions = loadOptions(args, options); diff --git a/packages/shared/package.json b/packages/shared/package.json index f1d39b6a48b..928ced9da7d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -71,6 +71,16 @@ "default": "./dist/agentRules/index.js" } }, + "./agentRulesWriter": { + "import": { + "types": "./dist/agentRulesWriter.d.mts", + "default": "./dist/agentRulesWriter.mjs" + }, + "require": { + "types": "./dist/agentRulesWriter.d.ts", + "default": "./dist/agentRulesWriter.js" + } + }, "./vite": { "import": { "types": "./dist/vite.d.mts", diff --git a/packages/shared/src/__tests__/agentRulesWriter.spec.ts b/packages/shared/src/__tests__/agentRulesWriter.spec.ts new file mode 100644 index 00000000000..2e4146653f7 --- /dev/null +++ b/packages/shared/src/__tests__/agentRulesWriter.spec.ts @@ -0,0 +1,85 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type * as AgentRules from '../agentRules'; +import { writeClerkAgentRules } from '../agentRules'; +import { createClerkAgentRulesWriter } from '../agentRulesWriter'; +import { automatedEnvironmentVariables } from '../utils/runtimeEnvironment'; + +vi.mock('../agentRules', async importOriginal => ({ + ...(await importOriginal()), + writeClerkAgentRules: vi.fn(), +})); + +describe('createClerkAgentRulesWriter', () => { + beforeEach(() => { + vi.mocked(writeClerkAgentRules).mockReset(); + for (const name of automatedEnvironmentVariables) { + vi.stubEnv(name, ''); + } + vi.stubEnv('CLERK_AGENTS_MD', ''); + vi.stubEnv('NODE_ENV', 'development'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it('writes once per writer from the process working directory', () => { + const write = createClerkAgentRulesWriter('express'); + + write(); + write(); + + expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); + expect(writeClerkAgentRules).toHaveBeenCalledWith( + expect.objectContaining({ cwd: process.cwd(), sdks: ['express'] }), + ); + }); + + it('writes to the directory it is given', () => { + createClerkAgentRulesWriter('electron')('/app'); + + expect(writeClerkAgentRules).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/app', sdks: ['electron'] })); + }); + + it('skips in production', () => { + vi.stubEnv('NODE_ENV', 'production'); + + createClerkAgentRulesWriter('hono')(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips in CI', () => { + vi.stubEnv('CI', 'true'); + + createClerkAgentRulesWriter('hono')(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips when CLERK_AGENTS_MD is false', () => { + vi.stubEnv('CLERK_AGENTS_MD', 'false'); + + createClerkAgentRulesWriter('fastify')(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('skips when the runtime has no Node file system', () => { + vi.spyOn(process, 'getBuiltinModule').mockReturnValue(undefined as never); + + createClerkAgentRulesWriter('hono')(); + + expect(writeClerkAgentRules).not.toHaveBeenCalled(); + }); + + it('does not throw when writing fails', () => { + vi.mocked(writeClerkAgentRules).mockImplementation(() => { + throw new Error('EACCES'); + }); + + expect(() => createClerkAgentRulesWriter('react-router')()).not.toThrow(); + }); +}); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 750787a5ca7..9d9dc5b03c0 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -94,12 +94,24 @@ describe('isClerkAgentRulesWriteAllowed', () => { expect(isClerkAgentRulesWriteAllowed()).toBe(false); }); + it('blocks writes in tests', () => { + vi.stubEnv('NODE_ENV', 'test'); + + expect(isClerkAgentRulesWriteAllowed()).toBe(false); + }); + it.each(['false', 'FALSE', '0', 'off', 'no'])('blocks writes when CLERK_AGENTS_MD is %s', value => { vi.stubEnv('CLERK_AGENTS_MD', value); expect(isClerkAgentRulesWriteAllowed()).toBe(false); }); + it('allows writes when CLERK_AGENTS_MD is unset', () => { + vi.stubEnv('CLERK_AGENTS_MD', undefined); + + expect(isClerkAgentRulesWriteAllowed()).toBe(true); + }); + it('allows writes when CLERK_AGENTS_MD is true', () => { vi.stubEnv('CLERK_AGENTS_MD', 'true'); diff --git a/packages/shared/src/agentRulesWriter.ts b/packages/shared/src/agentRulesWriter.ts new file mode 100644 index 00000000000..2fb32e28aeb --- /dev/null +++ b/packages/shared/src/agentRulesWriter.ts @@ -0,0 +1,32 @@ +import { type ClerkAgentRulesSdk, type FileSystemAdapter, type PathAdapter, writeClerkAgentRules } from './agentRules'; +import { isClerkAgentRulesWriteAllowed } from './utils'; + +type NodeProcess = { + cwd: () => string; + getBuiltinModule?: (id: string) => unknown; +}; + +export function createClerkAgentRulesWriter(sdk: ClerkAgentRulesSdk) { + let didWrite = false; + + return (cwd?: string) => { + if (didWrite || !isClerkAgentRulesWriteAllowed()) { + return; + } + + didWrite = true; + + const nodeProcess = (globalThis as { process?: NodeProcess }).process; + const fs = nodeProcess?.getBuiltinModule?.('node:fs') as FileSystemAdapter | undefined; + const path = nodeProcess?.getBuiltinModule?.('node:path') as PathAdapter | undefined; + if (!nodeProcess || !fs || !path) { + return; + } + + try { + writeClerkAgentRules({ cwd: cwd ?? nodeProcess.cwd(), fs, path, sdks: [sdk] }); + } catch { + return; + } + }; +} diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index b949c86e202..cd96dd16dae 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -63,6 +63,11 @@ export const isAutomatedEnvironment = (): boolean => { }; export const isClerkAgentRulesWriteAllowed = (): boolean => { - const optOut = getEnvVariable('CLERK_AGENTS_MD').toLowerCase(); - return !['0', 'false', 'off', 'no'].includes(optOut) && !isProductionEnvironment() && !isAutomatedEnvironment(); + const optOut = (getEnvVariable('CLERK_AGENTS_MD') || '').toLowerCase(); + return ( + !['0', 'false', 'off', 'no'].includes(optOut) && + !isProductionEnvironment() && + !isTestEnvironment() && + !isAutomatedEnvironment() + ); }; diff --git a/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts b/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts index 220e40fdb3d..67df6289af7 100644 --- a/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts +++ b/packages/tanstack-react-start/src/server/__tests__/agentRules.test.ts @@ -1,70 +1,19 @@ -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; -import { automatedEnvironmentVariables } from '@clerk/shared/utils'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; -vi.mock('@clerk/shared/agentRules', async importOriginal => ({ - ...(await importOriginal()), - writeClerkAgentRules: vi.fn(), -})); - -const loadWriteAgentRulesOnce = async () => (await import('../agentRules')).writeAgentRulesOnce; - -describe('writeAgentRulesOnce', () => { - beforeEach(() => { - vi.resetModules(); - vi.mocked(writeClerkAgentRules).mockReset(); - for (const name of automatedEnvironmentVariables) { - vi.stubEnv(name, ''); - } - vi.stubEnv('CLERK_AGENTS_MD', ''); - vi.stubEnv('NODE_ENV', 'development'); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('writes the rules once per process in development, with or without an AI agent', async () => { - const writeAgentRulesOnce = await loadWriteAgentRulesOnce(); - - writeAgentRulesOnce(); - writeAgentRulesOnce(); - - expect(writeClerkAgentRules).toHaveBeenCalledTimes(1); - expect(writeClerkAgentRules).toHaveBeenCalledWith( - expect.objectContaining({ cwd: expect.any(String), sdks: ['tanstack-react-start'] }), - ); - }); - - it('skips in CI', async () => { - vi.stubEnv('CI', 'true'); +import { writeAgentRulesOnce } from '../agentRules'; - (await loadWriteAgentRulesOnce())(); +const { createdFor, writeAgentRules } = vi.hoisted(() => ({ createdFor: [] as string[], writeAgentRules: vi.fn() })); - expect(writeClerkAgentRules).not.toHaveBeenCalled(); - }); - - it('skips in production', async () => { - vi.stubEnv('NODE_ENV', 'production'); - - (await loadWriteAgentRulesOnce())(); - - expect(writeClerkAgentRules).not.toHaveBeenCalled(); - }); - - it('skips when CLERK_AGENTS_MD is false', async () => { - vi.stubEnv('CLERK_AGENTS_MD', 'false'); - - (await loadWriteAgentRulesOnce())(); - - expect(writeClerkAgentRules).not.toHaveBeenCalled(); - }); - - it('does not throw when writing fails', async () => { - vi.mocked(writeClerkAgentRules).mockImplementation(() => { - throw new Error('EACCES'); - }); +vi.mock('@clerk/shared/agentRulesWriter', () => ({ + createClerkAgentRulesWriter: (sdk: string) => { + createdFor.push(sdk); + return writeAgentRules; + }, +})); - expect(await loadWriteAgentRulesOnce()).not.toThrow(); +describe('writeAgentRulesOnce', () => { + it('is the shared writer for the tanstack-react-start SDK', () => { + expect(createdFor).toEqual(['tanstack-react-start']); + expect(writeAgentRulesOnce).toBe(writeAgentRules); }); }); diff --git a/packages/tanstack-react-start/src/server/agentRules.ts b/packages/tanstack-react-start/src/server/agentRules.ts index ecec28cf00c..d02da24465f 100644 --- a/packages/tanstack-react-start/src/server/agentRules.ts +++ b/packages/tanstack-react-start/src/server/agentRules.ts @@ -1,31 +1,3 @@ -import type { FileSystemAdapter, PathAdapter } from '@clerk/shared/agentRules'; -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; -import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; +import { createClerkAgentRulesWriter } from '@clerk/shared/agentRulesWriter'; -type NodeProcess = { - cwd: () => string; - getBuiltinModule?: (id: string) => unknown; -}; - -let didWrite = false; - -export function writeAgentRulesOnce() { - if (didWrite || !isClerkAgentRulesWriteAllowed()) { - return; - } - - didWrite = true; - - const nodeProcess = (globalThis as { process?: NodeProcess }).process; - const fs = nodeProcess?.getBuiltinModule?.('node:fs') as FileSystemAdapter | undefined; - const path = nodeProcess?.getBuiltinModule?.('node:path') as PathAdapter | undefined; - if (!nodeProcess || !fs || !path) { - return; - } - - try { - writeClerkAgentRules({ cwd: nodeProcess.cwd(), fs, path, sdks: ['tanstack-react-start'] }); - } catch { - return; - } -} +export const writeAgentRulesOnce = createClerkAgentRulesWriter('tanstack-react-start'); From 627310a8d45501ba81ce187e6b1c4fb9b545e135 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 16:26:35 -0400 Subject: [PATCH 13/17] feat(chrome-extension): add Vite plugin that writes Clerk agent rules Co-Authored-By: Claude Opus 5.5 --- .changeset/chrome-extension-agent-rules-vite.md | 14 ++++++++++++++ packages/chrome-extension/package.json | 13 ++++++++++++- packages/chrome-extension/src/vite.ts | 3 +++ packages/chrome-extension/tsup.config.ts | 1 + packages/chrome-extension/vite/package.json | 5 +++++ packages/shared/src/vite.ts | 2 +- 6 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .changeset/chrome-extension-agent-rules-vite.md create mode 100644 packages/chrome-extension/src/vite.ts create mode 100644 packages/chrome-extension/vite/package.json diff --git a/.changeset/chrome-extension-agent-rules-vite.md b/.changeset/chrome-extension-agent-rules-vite.md new file mode 100644 index 00000000000..c0519885406 --- /dev/null +++ b/.changeset/chrome-extension-agent-rules-vite.md @@ -0,0 +1,14 @@ +--- +'@clerk/chrome-extension': minor +--- + +Add a Vite plugin that writes a Clerk section to the project's `AGENTS.md` when the Vite dev server starts, for extensions built with WXT, CRXJS, or a custom Vite setup. Nothing is written during builds, tests, or CI, and setting `CLERK_AGENTS_MD=false` turns it off. + +```ts +// wxt.config.ts +import { clerkChromeExtensionAgentRules } from '@clerk/chrome-extension/vite'; + +export default defineConfig({ + vite: () => ({ plugins: [clerkChromeExtensionAgentRules()] }), +}); +``` diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index a461ff06984..3bcfaa6cea1 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -65,6 +65,16 @@ "./types": { "types": "./dist/types/types/index.d.ts" }, + "./vite": { + "import": { + "types": "./dist/types/vite.d.ts", + "default": "./dist/esm/vite.js" + }, + "require": { + "types": "./dist/types/vite.d.ts", + "default": "./dist/cjs/vite.js" + } + }, "./package.json": "./package.json" }, "main": "./dist/cjs/index.js", @@ -78,7 +88,8 @@ "internal", "legacy", "react", - "types" + "types", + "vite" ], "scripts": { "build": "tsup", diff --git a/packages/chrome-extension/src/vite.ts b/packages/chrome-extension/src/vite.ts new file mode 100644 index 00000000000..1b2a21ebfe2 --- /dev/null +++ b/packages/chrome-extension/src/vite.ts @@ -0,0 +1,3 @@ +import { clerkAgentRulesVitePlugin } from '@clerk/shared/vite'; + +export const clerkChromeExtensionAgentRules = () => clerkAgentRulesVitePlugin('chrome-extension'); diff --git a/packages/chrome-extension/tsup.config.ts b/packages/chrome-extension/tsup.config.ts index d6fee15bbee..57ee7e29509 100644 --- a/packages/chrome-extension/tsup.config.ts +++ b/packages/chrome-extension/tsup.config.ts @@ -16,6 +16,7 @@ export default defineConfig(overrideOptions => { './src/legacy.ts', './src/react/index.ts', './src/types/index.ts', + './src/vite.ts', ], bundle: true, clean: true, diff --git a/packages/chrome-extension/vite/package.json b/packages/chrome-extension/vite/package.json new file mode 100644 index 00000000000..4bcd8d3656f --- /dev/null +++ b/packages/chrome-extension/vite/package.json @@ -0,0 +1,5 @@ +{ + "main": "../dist/cjs/vite.js", + "module": "../dist/esm/vite.js", + "types": "../dist/types/vite.d.ts" +} diff --git a/packages/shared/src/vite.ts b/packages/shared/src/vite.ts index cd040762314..1d26a7f1fd2 100644 --- a/packages/shared/src/vite.ts +++ b/packages/shared/src/vite.ts @@ -6,7 +6,7 @@ type ViteResolvedConfig = { root: string; }; -export function clerkAgentRulesVitePlugin(sdk: Extract) { +export function clerkAgentRulesVitePlugin(sdk: Extract) { let didWrite = false; return { From 1ae1fb5314c471eca962403c97b5926671cfd5e6 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 16:45:40 -0400 Subject: [PATCH 14/17] fix(shared): write Clerk agent rules to the nearest package.json folder The dev-time writers started from process.cwd(), so a server started outside its project wrote AGENTS.md and CLAUDE.md into whatever folder the shell was in, including the home folder. The writer now targets the workspace root, or the nearest folder at or above the start with a package.json, and skips the write when there is neither. Co-Authored-By: Claude Opus 5.5 --- packages/expo/app.plugin.js | 6 +---- .../shared/src/__tests__/agentRules.spec.ts | 25 ++++++++++++++++--- packages/shared/src/agentRules/index.ts | 14 ++++++++--- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index de29d46eaa2..8d848e6de70 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -22,14 +22,10 @@ const maybeWriteAgentRules = config => { return; } - const projectRoot = config?._internal?.projectRoot || process.cwd(); - if (!fs.existsSync(path.join(projectRoot, 'package.json'))) { - return; - } - didWriteAgentRules = true; try { + const projectRoot = config?._internal?.projectRoot || process.cwd(); writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] }); } catch { // Writing agent hints must never break Expo config evaluation. diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 53239de8d19..165c09d2f7d 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -9,8 +9,8 @@ import { writeClerkAgentRules, } from '../agentRules'; -const createMemoryFs = (files: Record = {}) => { - const store = new Map(Object.entries(files)); +const createMemoryFs = (files: Record = {}, { withRepoPackageJson = true } = {}) => { + const store = new Map(Object.entries(withRepoPackageJson ? { '/repo/package.json': '{}', ...files } : files)); const dirs = new Set(); const fs: FileSystemAdapter = { @@ -55,7 +55,7 @@ describe('writeClerkAgentRules', () => { }); it('creates AGENTS.md and CLAUDE.md when neither exists', () => { - const memory = createMemoryFs(); + const memory = createMemoryFs({ '/repo/app/package.json': '{}' }); const result = writeClerkAgentRules({ cwd: '/repo/app', @@ -70,6 +70,25 @@ describe('writeClerkAgentRules', () => { expect(memory.read('/repo/app/CLAUDE.md')).toBe('@AGENTS.md\n'); }); + it('writes nothing when no package.json is at or above the starting folder', () => { + const memory = createMemoryFs({}, { withRepoPackageJson: false }); + + const result = writeClerkAgentRules({ cwd: '/home/user', fs: memory.fs, path: pathAdapter, sdks: ['express'] }); + + expect(result).toEqual({ targetDir: '/home/user', agentsMd: 'skipped', claudeMd: 'skipped' }); + expect(memory.read('/home/user/AGENTS.md')).toBeUndefined(); + expect(memory.read('/home/user/CLAUDE.md')).toBeUndefined(); + }); + + it('targets the nearest folder with a package.json when started in a subfolder', () => { + const memory = createMemoryFs(); + + writeClerkAgentRules({ cwd: '/repo/src', fs: memory.fs, path: pathAdapter, sdks: ['react'] }); + + expect(memory.read('/repo/AGENTS.md')).toContain(''); + expect(memory.read('/repo/src/AGENTS.md')).toBeUndefined(); + }); + it('targets the workspace root when one exists', () => { const memory = createMemoryFs({ '/repo/pnpm-workspace.yaml': 'packages:\n - apps/*\n', diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index babbad83dbd..3c5bc8d2bb7 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -39,7 +39,7 @@ export interface WriteClerkAgentRulesOptions { export type WriteClerkAgentRulesResult = { targetDir: string; - agentsMd: 'created' | 'updated' | 'unchanged'; + agentsMd: 'created' | 'updated' | 'unchanged' | 'skipped'; claudeMd: 'created' | 'updated' | 'unchanged' | 'skipped'; }; @@ -181,9 +181,10 @@ function upsertMarkedBlock( return `${content.trimEnd()}${eol}${eol}${block}${eol}`; } -function findWorkspaceRoot(fs: FileSystemAdapter, path: PathAdapter, cwd: string) { +function findTargetDir(fs: FileSystemAdapter, path: PathAdapter, cwd: string) { let current = path.resolve(cwd); const root = path.parse(current).root; + let nearestPackageDir: string | null = null; while (current !== root) { if (fs.existsSync(path.join(current, 'pnpm-workspace.yaml'))) { @@ -192,6 +193,7 @@ function findWorkspaceRoot(fs: FileSystemAdapter, path: PathAdapter, cwd: string const packageJson = path.join(current, 'package.json'); if (fs.existsSync(packageJson)) { + nearestPackageDir ??= current; try { const pkg = JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf8' })); if (pkg.workspaces) { @@ -205,7 +207,7 @@ function findWorkspaceRoot(fs: FileSystemAdapter, path: PathAdapter, cwd: string current = path.dirname(current); } - return null; + return nearestPackageDir; } function pickTargetFile(fs: FileSystemAdapter, path: PathAdapter, targetDir: string) { @@ -231,7 +233,11 @@ function pickTargetFile(fs: FileSystemAdapter, path: PathAdapter, targetDir: str export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): WriteClerkAgentRulesResult { const { dryRun = false, fs, path, sdks } = options; - const targetDir = findWorkspaceRoot(fs, path, options.cwd) ?? path.resolve(options.cwd); + const targetDir = findTargetDir(fs, path, options.cwd); + if (!targetDir) { + return { targetDir: path.resolve(options.cwd), agentsMd: 'skipped', claudeMd: 'skipped' }; + } + const targetFile = pickTargetFile(fs, path, targetDir); const agentsPath = path.join(targetDir, 'AGENTS.md'); const claudePath = path.join(targetDir, 'CLAUDE.md'); From f4868315695706da0b3d2b2a4cfaf35397659ef2 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Wed, 21 Oct 2026 16:45:53 -0400 Subject: [PATCH 15/17] feat(*): log when Clerk writes agent rules in development Each dev-time trigger now prints the files it created or updated and how to turn the write off, so a write to an unexpected folder is visible on the first run. Nothing is logged when the files are already current. Co-Authored-By: Claude Opus 5.5 --- packages/astro/src/integration/agent-rules.ts | 4 +-- packages/expo/app.plugin.js | 4 +-- .../nextjs/src/server/agent-rules-node.ts | 16 +++++---- packages/nuxt/src/agent-rules.ts | 4 +-- .../shared/src/__tests__/agentRules.spec.ts | 33 +++++++++++++++++-- .../src/__tests__/agentRulesWriter.spec.ts | 4 ++- packages/shared/src/__tests__/vite.spec.ts | 4 ++- packages/shared/src/agentRules/index.ts | 15 ++++++++- packages/shared/src/agentRulesWriter.ts | 10 ++++-- packages/shared/src/vite.ts | 4 +-- 10 files changed, 76 insertions(+), 22 deletions(-) diff --git a/packages/astro/src/integration/agent-rules.ts b/packages/astro/src/integration/agent-rules.ts index 252caef47c5..96e18c49fa6 100644 --- a/packages/astro/src/integration/agent-rules.ts +++ b/packages/astro/src/integration/agent-rules.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { logClerkAgentRulesResult, writeClerkAgentRules } from '@clerk/shared/agentRules'; import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; let didWrite = false; @@ -15,7 +15,7 @@ export function writeAgentRulesOnce(root: URL) { didWrite = true; try { - writeClerkAgentRules({ cwd: fileURLToPath(root), fs, path, sdks: ['astro'] }); + logClerkAgentRulesResult(writeClerkAgentRules({ cwd: fileURLToPath(root), fs, path, sdks: ['astro'] })); } catch { // Writing agent hints must never break a dev server. } diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index 8d848e6de70..c5afcb4178c 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -7,7 +7,7 @@ * 3. If @clerk/expo-native-components is installed, its config plugin is applied for the Clerk native SDKs */ const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins'); -const { writeClerkAgentRules } = require('@clerk/shared/agentRules'); +const { logClerkAgentRulesResult, writeClerkAgentRules } = require('@clerk/shared/agentRules'); const { isClerkAgentRulesWriteAllowed } = require('@clerk/shared/utils'); const fs = require('fs'); @@ -26,7 +26,7 @@ const maybeWriteAgentRules = config => { try { const projectRoot = config?._internal?.projectRoot || process.cwd(); - writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] }); + logClerkAgentRulesResult(writeClerkAgentRules({ cwd: projectRoot, fs, path, sdks: ['expo'] })); } catch { // Writing agent hints must never break Expo config evaluation. } diff --git a/packages/nextjs/src/server/agent-rules-node.ts b/packages/nextjs/src/server/agent-rules-node.ts index fdbf0a94407..2e4aa748326 100644 --- a/packages/nextjs/src/server/agent-rules-node.ts +++ b/packages/nextjs/src/server/agent-rules-node.ts @@ -1,4 +1,4 @@ -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { logClerkAgentRulesResult, writeClerkAgentRules } from '@clerk/shared/agentRules'; import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './fs/utils'; @@ -13,12 +13,14 @@ export function writeAgentRulesOnce() { didWrite = true; try { - writeClerkAgentRules({ - cwd: nodeCwdOrThrow()(), - fs: nodeFsOrThrow(), - path: nodePathOrThrow(), - sdks: ['nextjs'], - }); + logClerkAgentRulesResult( + writeClerkAgentRules({ + cwd: nodeCwdOrThrow()(), + fs: nodeFsOrThrow(), + path: nodePathOrThrow(), + sdks: ['nextjs'], + }), + ); } catch { // Writing agent hints must never break a dev server. } diff --git a/packages/nuxt/src/agent-rules.ts b/packages/nuxt/src/agent-rules.ts index 4eb6a9c92f2..19d0ae5b1b9 100644 --- a/packages/nuxt/src/agent-rules.ts +++ b/packages/nuxt/src/agent-rules.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { writeClerkAgentRules } from '@clerk/shared/agentRules'; +import { logClerkAgentRulesResult, writeClerkAgentRules } from '@clerk/shared/agentRules'; import { isClerkAgentRulesWriteAllowed } from '@clerk/shared/utils'; let didWrite = false; @@ -14,7 +14,7 @@ export function writeAgentRulesOnce(rootDir: string) { didWrite = true; try { - writeClerkAgentRules({ cwd: rootDir, fs, path, sdks: ['nuxt'] }); + logClerkAgentRulesResult(writeClerkAgentRules({ cwd: rootDir, fs, path, sdks: ['nuxt'] })); } catch { // Writing agent hints must never break a dev server. } diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 165c09d2f7d..aaa84d39af3 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -1,9 +1,10 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CLERK_SDK_PLUGIN_END, CLERK_SDK_PLUGIN_START, type FileSystemAdapter, + logClerkAgentRulesResult, renderClerkAgentRulesBlock, renderClerkSdkPluginBlock, writeClerkAgentRules, @@ -68,6 +69,7 @@ describe('writeClerkAgentRules', () => { expect(result.claudeMd).toBe('created'); expect(memory.read('/repo/app/AGENTS.md')).toContain(''); expect(memory.read('/repo/app/CLAUDE.md')).toBe('@AGENTS.md\n'); + expect(result.files).toEqual(['/repo/app/AGENTS.md', '/repo/app/CLAUDE.md']); }); it('writes nothing when no package.json is at or above the starting folder', () => { @@ -75,7 +77,7 @@ describe('writeClerkAgentRules', () => { const result = writeClerkAgentRules({ cwd: '/home/user', fs: memory.fs, path: pathAdapter, sdks: ['express'] }); - expect(result).toEqual({ targetDir: '/home/user', agentsMd: 'skipped', claudeMd: 'skipped' }); + expect(result).toEqual({ targetDir: '/home/user', agentsMd: 'skipped', claudeMd: 'skipped', files: [] }); expect(memory.read('/home/user/AGENTS.md')).toBeUndefined(); expect(memory.read('/home/user/CLAUDE.md')).toBeUndefined(); }); @@ -262,6 +264,33 @@ describe('writeClerkAgentRules with an existing CLAUDE.md', () => { }); }); +describe('logClerkAgentRulesResult', () => { + it('logs the files that changed', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + logClerkAgentRulesResult({ + targetDir: '/repo', + agentsMd: 'created', + claudeMd: 'created', + files: ['/repo/AGENTS.md', '/repo/CLAUDE.md'], + }); + + expect(log).toHaveBeenCalledWith( + '[Clerk]: Updated /repo/AGENTS.md and /repo/CLAUDE.md for AI coding agents. Set CLERK_AGENTS_MD=false to turn this off.', + ); + log.mockRestore(); + }); + + it('stays quiet when nothing changed', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + logClerkAgentRulesResult({ targetDir: '/repo', agentsMd: 'unchanged', claudeMd: 'unchanged', files: [] }); + + expect(log).not.toHaveBeenCalled(); + log.mockRestore(); + }); +}); + describe('Clerk plugin rules', () => { it('recommends the Clerk plugin and keeps the ask-before-changes rule in the root block', () => { const block = renderClerkAgentRulesBlock(); diff --git a/packages/shared/src/__tests__/agentRulesWriter.spec.ts b/packages/shared/src/__tests__/agentRulesWriter.spec.ts index 2e4146653f7..0511c893630 100644 --- a/packages/shared/src/__tests__/agentRulesWriter.spec.ts +++ b/packages/shared/src/__tests__/agentRulesWriter.spec.ts @@ -12,7 +12,9 @@ vi.mock('../agentRules', async importOriginal => ({ describe('createClerkAgentRulesWriter', () => { beforeEach(() => { - vi.mocked(writeClerkAgentRules).mockReset(); + vi.mocked(writeClerkAgentRules) + .mockReset() + .mockReturnValue({ targetDir: '/app', agentsMd: 'unchanged', claudeMd: 'unchanged', files: [] }); for (const name of automatedEnvironmentVariables) { vi.stubEnv(name, ''); } diff --git a/packages/shared/src/__tests__/vite.spec.ts b/packages/shared/src/__tests__/vite.spec.ts index 396650506e2..b1a939a0c0f 100644 --- a/packages/shared/src/__tests__/vite.spec.ts +++ b/packages/shared/src/__tests__/vite.spec.ts @@ -12,7 +12,9 @@ vi.mock('../agentRules', async importOriginal => ({ describe('clerkAgentRulesVitePlugin', () => { beforeEach(() => { - vi.mocked(writeClerkAgentRules).mockReset(); + vi.mocked(writeClerkAgentRules) + .mockReset() + .mockReturnValue({ targetDir: '/app', agentsMd: 'unchanged', claudeMd: 'unchanged', files: [] }); for (const name of automatedEnvironmentVariables) { vi.stubEnv(name, ''); } diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 3c5bc8d2bb7..3a37824089e 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -41,6 +41,7 @@ export type WriteClerkAgentRulesResult = { targetDir: string; agentsMd: 'created' | 'updated' | 'unchanged' | 'skipped'; claudeMd: 'created' | 'updated' | 'unchanged' | 'skipped'; + files: string[]; }; const GENERIC_START = ''; @@ -235,7 +236,7 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ const { dryRun = false, fs, path, sdks } = options; const targetDir = findTargetDir(fs, path, options.cwd); if (!targetDir) { - return { targetDir: path.resolve(options.cwd), agentsMd: 'skipped', claudeMd: 'skipped' }; + return { targetDir: path.resolve(options.cwd), agentsMd: 'skipped', claudeMd: 'skipped', files: [] }; } const targetFile = pickTargetFile(fs, path, targetDir); @@ -291,5 +292,17 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ : claudeExists ? 'unchanged' : 'skipped', + files: [ + ...(targetStatus === 'unchanged' ? [] : [targetFile]), + ...(shouldCreateClaude || shouldImportAgentsInClaude ? [claudePath] : []), + ], }; } + +export function logClerkAgentRulesResult({ files }: WriteClerkAgentRulesResult) { + if (files.length > 0) { + console.log( + `[Clerk]: Updated ${files.join(' and ')} for AI coding agents. Set CLERK_AGENTS_MD=false to turn this off.`, + ); + } +} diff --git a/packages/shared/src/agentRulesWriter.ts b/packages/shared/src/agentRulesWriter.ts index 2fb32e28aeb..8afc83457b5 100644 --- a/packages/shared/src/agentRulesWriter.ts +++ b/packages/shared/src/agentRulesWriter.ts @@ -1,4 +1,10 @@ -import { type ClerkAgentRulesSdk, type FileSystemAdapter, type PathAdapter, writeClerkAgentRules } from './agentRules'; +import { + type ClerkAgentRulesSdk, + type FileSystemAdapter, + logClerkAgentRulesResult, + type PathAdapter, + writeClerkAgentRules, +} from './agentRules'; import { isClerkAgentRulesWriteAllowed } from './utils'; type NodeProcess = { @@ -24,7 +30,7 @@ export function createClerkAgentRulesWriter(sdk: ClerkAgentRulesSdk) { } try { - writeClerkAgentRules({ cwd: cwd ?? nodeProcess.cwd(), fs, path, sdks: [sdk] }); + logClerkAgentRulesResult(writeClerkAgentRules({ cwd: cwd ?? nodeProcess.cwd(), fs, path, sdks: [sdk] })); } catch { return; } diff --git a/packages/shared/src/vite.ts b/packages/shared/src/vite.ts index 1d26a7f1fd2..85c9112ca89 100644 --- a/packages/shared/src/vite.ts +++ b/packages/shared/src/vite.ts @@ -1,4 +1,4 @@ -import { type ClerkAgentRulesSdk, writeClerkAgentRules } from './agentRules'; +import { type ClerkAgentRulesSdk, logClerkAgentRulesResult, writeClerkAgentRules } from './agentRules'; import { isClerkAgentRulesWriteAllowed } from './utils'; type ViteResolvedConfig = { @@ -21,7 +21,7 @@ export function clerkAgentRulesVitePlugin(sdk: Extract Date: Thu, 22 Oct 2026 16:32:39 -0400 Subject: [PATCH 16/17] test(repo): update typedoc snapshot for InstanceType union order Co-Authored-By: Claude Opus 5.5 --- .typedoc/__tests__/__snapshots__/clerk-properties.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.typedoc/__tests__/__snapshots__/clerk-properties.mdx b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx index 2a9ba670c16..160e1ad3aa6 100644 --- a/.typedoc/__tests__/__snapshots__/clerk-properties.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx @@ -4,7 +4,7 @@ | `billing` | [`BillingNamespace`](/docs/reference/objects/billing) | The `Billing` object used for managing billing. | | `client` | undefined \| [ClientResource](/docs/reference/objects/client) | The `Client` object for the current window. | | `domain` | `string` | The current Clerk app's domain. Prefixed with `clerk.` on production if not already prefixed. Returns `""` when ran on the server. | -| `instanceType` | undefined \| "production" \| "development" | Indicates whether the Clerk instance is running in a production or development environment. | +| `instanceType` | undefined \| "development" \| "production" | Indicates whether the Clerk instance is running in a production or development environment. | | `isSatellite` | `boolean` | Indicates whether the instance is a satellite app. | | `isSignedIn` | `boolean` | Indicates whether the current user has a valid signed-in client session. | | `isStandardBrowser` | undefined \| boolean | Indicates whether the instance is being loaded in a standard browser environment. Set to `false` on native platforms where cookies cannot be set. When `undefined`, Clerk assumes a standard browser. | From a40ba985064c8e2a6e5503e23349add67ca32901 Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Thu, 22 Oct 2026 17:38:08 -0400 Subject: [PATCH 17/17] fix(shared): keep SDK agent rules blocks in sorted order Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/__tests__/agentRules.spec.ts | 16 ++++++++++++++++ packages/shared/src/agentRules/index.ts | 9 +++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index aaa84d39af3..31e243af363 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -125,6 +125,22 @@ describe('writeClerkAgentRules', () => { expect(content).not.toContain('\nold\n'); }); + it('writes SDK blocks in sorted order', () => { + const memory = createMemoryFs(); + + writeClerkAgentRules({ + cwd: '/repo', + fs: memory.fs, + path: pathAdapter, + sdks: ['vue', 'astro', 'react'], + }); + + const content = memory.read('/repo/AGENTS.md') ?? ''; + const positions = ['astro', 'react', 'vue'].map(sdk => content.indexOf(``)); + expect(positions.every(position => position !== -1)).toBe(true); + expect(positions).toEqual([...positions].sort((a, b) => a - b)); + }); + it('does not write files in dry-run mode', () => { const memory = createMemoryFs(); diff --git a/packages/shared/src/agentRules/index.ts b/packages/shared/src/agentRules/index.ts index 3a37824089e..80d5538df24 100644 --- a/packages/shared/src/agentRules/index.ts +++ b/packages/shared/src/agentRules/index.ts @@ -247,15 +247,20 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ let next = upsertMarkedBlock(existing, renderClerkAgentRulesBlock(eol), GENERIC_START, GENERIC_END, eol); + let insertAfter = GENERIC_END; for (const sdk of uniqueSdks(sdks)) { + const sdkEnd = ``; next = upsertMarkedBlock( next, renderClerkSdkAgentRulesBlock(sdk, eol), ``, - ``, + sdkEnd, eol, - GENERIC_END, + insertAfter, ); + if (next.includes(sdkEnd)) { + insertAfter = sdkEnd; + } } const targetStatus = fs.existsSync(targetFile) ? (next === existing ? 'unchanged' : 'updated') : 'created';