Skip to content

Commit 04262f8

Browse files
royangerclaude
andcommitted
fix(shared): import AGENTS.md from an existing CLAUDE.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 06ded64 commit 04262f8

3 files changed

Lines changed: 121 additions & 4 deletions

File tree

‎.changeset/write-agents-md-in-dev.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
'@clerk/tanstack-react-start': minor
77
---
88

9-
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/<sdk>/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()`.
9+
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/<sdk>/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()`.

‎packages/shared/src/__tests__/agentRules.spec.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,99 @@ describe('writeClerkAgentRules', () => {
120120
});
121121
});
122122

123+
describe('writeClerkAgentRules with an existing CLAUDE.md', () => {
124+
it('adds an @AGENTS.md import when CLAUDE.md does not load AGENTS.md', () => {
125+
const memory = createMemoryFs({
126+
'/repo/AGENTS.md': 'Agent rules\n',
127+
'/repo/CLAUDE.md': 'Claude rules\n',
128+
});
129+
130+
const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
131+
132+
expect(result.claudeMd).toBe('updated');
133+
expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n\n@AGENTS.md\n');
134+
expect(memory.read('/repo/AGENTS.md')).toContain('<!-- BEGIN:clerk-agent-rules:nextjs -->');
135+
});
136+
137+
it('adds the import only once across runs', () => {
138+
const memory = createMemoryFs({
139+
'/repo/AGENTS.md': 'Agent rules\n',
140+
'/repo/CLAUDE.md': 'Claude rules\n',
141+
});
142+
143+
writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
144+
const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
145+
146+
expect(result.claudeMd).toBe('unchanged');
147+
expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n\n@AGENTS.md\n');
148+
});
149+
150+
it.each(['@AGENTS.md', '@./AGENTS.md'])('leaves CLAUDE.md alone when it already imports %s', importLine => {
151+
const claude = `Claude rules\n\n${importLine}\n`;
152+
const memory = createMemoryFs({
153+
'/repo/AGENTS.md': 'Agent rules\n',
154+
'/repo/CLAUDE.md': claude,
155+
});
156+
157+
const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
158+
159+
expect(result.claudeMd).toBe('unchanged');
160+
expect(memory.read('/repo/CLAUDE.md')).toBe(claude);
161+
});
162+
163+
it('leaves CLAUDE.md alone when it mirrors AGENTS.md, as a symlink does', () => {
164+
const memory = createMemoryFs({
165+
'/repo/AGENTS.md': 'Shared rules\n',
166+
'/repo/CLAUDE.md': 'Shared rules\n',
167+
});
168+
169+
const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
170+
171+
expect(result.claudeMd).toBe('unchanged');
172+
expect(memory.read('/repo/CLAUDE.md')).toBe('Shared rules\n');
173+
});
174+
175+
it('writes the Clerk block into CLAUDE.md and skips the import when only CLAUDE.md exists', () => {
176+
const memory = createMemoryFs({ '/repo/CLAUDE.md': 'Claude rules\n' });
177+
178+
const result = writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
179+
180+
expect(result.agentsMd).toBe('updated');
181+
expect(memory.read('/repo/AGENTS.md')).toBeUndefined();
182+
expect(memory.read('/repo/CLAUDE.md')).toContain('<!-- BEGIN:clerk-agent-rules:nextjs -->');
183+
expect(memory.read('/repo/CLAUDE.md')).not.toContain('@AGENTS.md');
184+
});
185+
186+
it('keeps CRLF line endings in CLAUDE.md', () => {
187+
const memory = createMemoryFs({
188+
'/repo/AGENTS.md': 'Agent rules\r\n',
189+
'/repo/CLAUDE.md': 'Claude rules\r\n',
190+
});
191+
192+
writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
193+
194+
expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\r\n\r\n@AGENTS.md\r\n');
195+
});
196+
197+
it('reports the import but does not write it in dry-run mode', () => {
198+
const memory = createMemoryFs({
199+
'/repo/AGENTS.md': 'Agent rules\n',
200+
'/repo/CLAUDE.md': 'Claude rules\n',
201+
});
202+
203+
const result = writeClerkAgentRules({
204+
cwd: '/repo',
205+
dryRun: true,
206+
fs: memory.fs,
207+
path: pathAdapter,
208+
sdks: ['nextjs'],
209+
});
210+
211+
expect(result.claudeMd).toBe('updated');
212+
expect(memory.read('/repo/CLAUDE.md')).toBe('Claude rules\n');
213+
});
214+
});
215+
123216
describe('Clerk plugin rules', () => {
124217
it('recommends the Clerk plugin and keeps the ask-before-changes rule in the root block', () => {
125218
const block = renderClerkAgentRulesBlock();

‎packages/shared/src/agentRules/index.ts‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface WriteClerkAgentRulesOptions {
4040
export type WriteClerkAgentRulesResult = {
4141
targetDir: string;
4242
agentsMd: 'created' | 'updated' | 'unchanged';
43-
claudeMd: 'created' | 'unchanged' | 'skipped';
43+
claudeMd: 'created' | 'updated' | 'unchanged' | 'skipped';
4444
};
4545

4646
const GENERIC_START = '<!-- BEGIN:clerk-agent-rules -->';
@@ -73,6 +73,13 @@ const uniqueSdks = (sdks: ClerkAgentRulesSdk[]) => [...new Set(sdks)].sort();
7373
const detectEol = (content: string) => (content.includes('\r\n') ? '\r\n' : '\n');
7474
const normalizeEol = (content: string, eol: string) => content.replace(/\r?\n/g, eol);
7575

76+
const CLAUDE_AGENTS_IMPORT = /^\s*@(?:\.\/)?AGENTS\.md\s*$/m;
77+
78+
const appendAgentsImport = (content: string) => {
79+
const eol = detectEol(content);
80+
return content.trim() ? `${content.trimEnd()}${eol}${eol}@AGENTS.md${eol}` : `@AGENTS.md${eol}`;
81+
};
82+
7683
export const isClerkAgentRulesSdk = (value: string): value is ClerkAgentRulesSdk => value in SDK_LABELS;
7784

7885
export const clerkAgentRulesSdks = Object.keys(SDK_LABELS) as ClerkAgentRulesSdk[];
@@ -248,7 +255,15 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ
248255
}
249256

250257
const targetStatus = fs.existsSync(targetFile) ? (next === existing ? 'unchanged' : 'updated') : 'created';
251-
const shouldCreateClaude = targetFile === agentsPath && !fs.existsSync(claudePath);
258+
const claudeExists = fs.existsSync(claudePath);
259+
const shouldCreateClaude = targetFile === agentsPath && !claudeExists;
260+
const claudeContent =
261+
targetFile === agentsPath && claudeExists ? fs.readFileSync(claudePath, { encoding: 'utf8' }) : null;
262+
const shouldImportAgentsInClaude =
263+
claudeContent !== null &&
264+
claudeContent !== existing &&
265+
!claudeContent.includes(GENERIC_START) &&
266+
!CLAUDE_AGENTS_IMPORT.test(claudeContent);
252267

253268
if (!dryRun) {
254269
fs.mkdirSync?.(targetDir, { recursive: true });
@@ -258,11 +273,20 @@ export function writeClerkAgentRules(options: WriteClerkAgentRulesOptions): Writ
258273
if (shouldCreateClaude) {
259274
fs.writeFileSync(claudePath, '@AGENTS.md\n', { encoding: 'utf8', mode: 0o644 });
260275
}
276+
if (shouldImportAgentsInClaude && claudeContent !== null) {
277+
fs.writeFileSync(claudePath, appendAgentsImport(claudeContent), { encoding: 'utf8', mode: 0o644 });
278+
}
261279
}
262280

263281
return {
264282
targetDir,
265283
agentsMd: targetStatus,
266-
claudeMd: shouldCreateClaude ? 'created' : fs.existsSync(claudePath) ? 'unchanged' : 'skipped',
284+
claudeMd: shouldCreateClaude
285+
? 'created'
286+
: shouldImportAgentsInClaude
287+
? 'updated'
288+
: claudeExists
289+
? 'unchanged'
290+
: 'skipped',
267291
};
268292
}

0 commit comments

Comments
 (0)