.agents: Centralized Rules and Skills for Any AI Editor
When everyone on the team uses a different AI editor, rules drift between .cursor, .agent, and .claude. We centralized everything in .agents/ with symlinks and tucked the wiring into VS Code so the repo stays focused.
How we got here
We were all using AI coding assistants — just not the same ones. I had been on Cursor for a while; a teammate used Antigravity; another was trying Pi. Each tool worked fine. The friction showed up when we tried to share rules.
Cursor looks in .cursor/rules. Antigravity used .agent (it now prefers .agents, which we didn't know at the time). Pi has .pi. Claude Code uses .claude. Skills were already easier to share; rules were parallel folders that drifted on the first PR.
Someone would edit the TypeScript rule in .cursor, someone else in .agent, and the same question kept coming up: which one is canonical? We hadn't picked a single place.
We looked at how Skills handled linking and asked: why not do the same for rules? We ended up centralizing everything in .agents/.
What we did
.agents/ is where agent config actually lives — rules, skills, and AGENTS.md for project scope. Editors don't duplicate; they symlink there via a script that runs on pnpm install.
Team rule: edit in `.agents/` only. Never in .cursor/ or .claude/. Those folders exist so tools can find the files, not so humans write there.
Setup in brief
A ~50-line init script hooks into prepare. Editor folders are gitignored; only .agents/ is versioned. VS Code hides the symlink folders so the file tree shows one obvious place to edit.
Does it still make sense?
Yes — the ecosystem moved toward us. Antigravity documents .agents/rules as official. Pi and Cursor already discover skills in .agents/skills/. Symlinks are still needed for Cursor rules and Claude Code.
Caveats: Pi uses root AGENTS.md, not a rules folder. Frontmatter differs across tools. Editing through a symlink can break the link — always edit the source in .agents/.
Steps to implement in your repo
Before you start: Node.js 18+, a root package.json, and both .agents/rules and .agents/skills folders (can be empty) or init exits with an error.
1. Create .agents/ as source of truth
mkdir -p .agents/rules .agents/skills .agents/scripts
touch .agents/AGENTS.md.agents/
├── AGENTS.md
├── rules/
├── skills/
└── scripts/init.js # ← copy the file belowMinimal .agents/AGENTS.md:
# My project
Stack and scope here.
Edit rules only in `.agents/rules/`, skills in `.agents/skills/`.
Do not edit `.cursor/` or `.claude/` directly.Move existing rules from .cursor/rules into .agents/rules/ once.
2. Create the init — copy this entire file
Create .agents/scripts/init.js:
#!/usr/bin/env node
'use strict';
/**
* .agents init — symlinks rules and skills into AI editor folders.
* Requires: .agents/rules and .agents/skills (can be empty).
*/
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '../..');
const EDITORS = ['.cursor', '.agent', '.pi', '.claude'];
const agentsRules = path.join(root, '.agents', 'rules');
const agentsSkills = path.join(root, '.agents', 'skills');
function linkDir(linkPath, targetPath) {
if (process.platform === 'win32') {
fs.symlinkSync(targetPath, linkPath, 'junction');
return;
}
const relativeTarget = path.relative(path.dirname(linkPath), targetPath);
fs.symlinkSync(relativeTarget, linkPath, 'dir');
}
function setupEditor(editorName) {
const editorPath = path.join(root, editorName);
fs.mkdirSync(editorPath, { recursive: true });
for (const [name, targetPath] of [
['rules', agentsRules],
['skills', agentsSkills],
]) {
const linkPath = path.join(editorPath, name);
fs.rmSync(linkPath, { recursive: true, force: true });
linkDir(linkPath, targetPath);
}
}
if (!fs.existsSync(agentsRules) || !fs.existsSync(agentsSkills)) {
console.error('⚠️ .agents/rules or .agents/skills not found');
console.error(' Run: mkdir -p .agents/rules .agents/skills');
process.exit(1);
}
console.log('🔧 Initializing AI editor symlinks...');
for (const editor of EDITORS) {
setupEditor(editor);
console.log(` ✅ ${editor}/rules → .agents/rules`);
console.log(` ✅ ${editor}/skills → .agents/skills`);
}
console.log('');
console.log('✅ AI editor symlinks ready (.cursor, .agent, .pi, .claude)');Add new editors by extending the EDITORS array.
3. Wire init into package.json
{
"scripts": {
"setup:ai:init": "node .agents/scripts/init.js",
"setup:githooks:init": "node .githooks/scripts/init.js",
"prepare": "pnpm run setup:githooks:init && pnpm run setup:ai:init"
}
}Or without Git hooks:
"setup:ai:init": "node .agents/scripts/init.js",
"prepare": "pnpm run setup:ai:init"Manual test:
pnpm run setup:ai:init
ls -la .cursorYou should see rules -> ../.agents/rules and skills -> ../.agents/skills.
4. Gitignore editor symlink folders
Only .agents/ is versioned.
5. Hide symlink folders in .vscode/settings.json (files.exclude)
What does this do in VS Code? Cursor and VS Code read workspace settings from .vscode/settings.json. The files.exclude key only hides folders in the file explorer — it does not delete them from disk, and it does not affect Git or AI editors that still need those paths. It is ergonomics: fewer folders that look like duplicates (.cursor, .claude, .githooks) and one obvious place to edit (.agents/, src/). To open something hidden: Ctrl/Cmd+P, type the path (e.g. .githooks/pre-commit), or temporarily remove the entry from files.exclude.
Show .agents/ in the tree; hide .cursor, .claude, .agent, .pi, .githooks.
6. Symlink AGENTS.md to repo root if it lives under .agents/ (Pi and Cursor look there).
7. Verify: pnpm run setup:ai:init, check symlinks, edit a rule in .agents/, confirm editors see it.
8. Team process: review rule changes like code; edit only in .agents/.
Quick reference
Source of truth: .agents/
Activation: pnpm install → prepare → setup:ai:init
Symlinks: mainly .cursor/rules and .claude/
Team rule: edit only in .agents/