A while back my repo looked like this:
CLAUDE.md # rules for Claude Code
.claude/agents/code-reviewer.md # agent definitions
.claude/skills/refactor/SKILL.md # skill packs
.claude/commands/deploy.md # slash commands
.claude.json # MCP servers
.cursor/rules/typescript.mdc # rules for Cursor
.cursor/mcp.json # MCP servers (different schema)
.github/copilot-instructions.md # rules for Copilot
.github/agents/code-reviewer.md # agents (different format)
.continue/rules/typescript.md # rules for Continue
.continue/mcpServers/ # MCP servers (yet another schema)
Same rule - "no any, prefer unknown with narrowing" - in four places. Same MCP server configured three times in three JSON schemas. Same code-reviewer agent described twice with different frontmatter. Same deployment command in two directories.
Then someone updated unknown to never for a specific case in CLAUDE.md only. Cursor still suggested unknown. Copilot agreed with neither. We spent a standup figuring out which AI was "right."
The problem wasn't rules alone. It was the entire config surface: rules, agents, skills, commands, MCP servers, hooks, permissions, and ignore files - all duplicated per tool, all drifting independently.
What I tried first
I looked for existing tools that solve this. The most popular one in this space has the right model: one canonical source, generate per-tool configs. I used it for a few months.
But I kept hitting gaps. Cross-references between files didn't get rewritten on generate, so an agent linking to a skill produced broken paths in every target. Activation semantics got inverted across tool dialects - a manual-only rule became always-on after translation. Hook configs got generated but the scripts they referenced didn't follow. Permissions weren't modeled as a feature at all. And every new AI coding tool that shipped needed a PR to the maintainer's repo — some requests sat open for over a year.
These aren't bugs. They're architectural limits that compound once you grow past flat rule syncing into the full config surface: agents, skills with supporting files, hooks with scripts, permissions, MCP servers with tool-specific schemas.
The pattern
You already use this pattern elsewhere:
-
package.json→package-lock.json -
Dockerfile→ image -
*.proto→ client SDKs
Apply it to AI configs:
- One canonical directory describes rules, commands, agents, skills, MCP, hooks, ignore, and permissions in a tool-agnostic way.
- A generator projects them into each tool's native format.
- An importer does the reverse, so you can adopt without rewriting existing configs.
- A lock file detects drift in CI.
The interesting work is in the link rewriting, the round-trip story, and getting the full feature surface (not just rules) to project correctly across 12 tools.
What I built
agentsmesh implements this pattern. Here's the canonical directory and what it generates:
.agentsmesh/ Generated output (Claude Code):
├── rules/ ├── CLAUDE.md
│ ├── _root.md ├── .claude/
│ └── typescript.md │ ├── rules/typescript.md
├── agents/ │ ├── agents/code-reviewer.md
│ └── code-reviewer.md │ ├── skills/refactor/
├── skills/ │ │ ├── SKILL.md
│ └── refactor/ │ │ └── references/patterns.md
│ ├── SKILL.md │ ├── commands/deploy.md
│ └── references/ │ └── settings.json (hooks, perms)
│ └── patterns.md ├── .claude.json (MCP)
├── commands/
│ └── deploy.md Generated output (Cursor):
├── mcp.json ├── .cursor/
├── hooks.yaml │ ├── rules/
├── permissions.yaml │ │ ├── _root.mdc
└── ignore │ │ └── typescript.mdc
│ ├── skills/refactor/
│ │ ├── SKILL.md
│ │ └── references/patterns.md
│ └── mcp.json
├── AGENTS.md (agents + commands embedded)
└── .cursorignore
One canonical source. Two different generated trees with different directory layouts, different file formats, different frontmatter conventions — all from the same .agentsmesh/ input.
Adopt in an existing project:
npx agentsmesh import --from cursor # or claude-code, copilot, codex-cli, ...
npx agentsmesh generate
import walks your existing tool config, normalizes it into the canonical shape. generate projects it back out to every enabled tool.
Cross-references get rewritten per-target
This is the part I underestimated.
When the code-reviewer agent file says "see the refactor skill for the full playbook", that link has to resolve differently in every generated output:
| Target | The agent's skill link resolves to |
|---|---|
| Claude Code | .claude/skills/refactor/SKILL.md |
| Cursor | .cursor/skills/refactor/SKILL.md |
| Codex CLI | .codex/skills/refactor/SKILL.md |
| Claude Code (global) | ~/.claude/skills/refactor/SKILL.md |
The rebaser at src/core/reference/link-rebaser.ts rewrites every link per target. The rules are explicit:
- Inside
.agentsmesh/→ relative paths. - Outside
.agentsmesh/in project mode → repo-root absolute paths. - Outside
.agentsmesh/in global mode → no rewrite. Your home directory isn't mine to touch. - Markdown link URLs stay relative for portability.
Without this you can't have agents referencing skills, skills referencing other skills, rules pointing at commands. You're forced to keep everything in one flat file with no cross-references.
Lossless round-trip across the full feature surface
When a target doesn't natively support a feature (Cursor has no native skills directory, Copilot has no native hooks), the generator embeds them with metadata so re-importing reconstructs the original shape.
This matters for more than just rules:
- Skills with supporting files (references, templates, scripts) survive projection to tools that don't have a skills directory — they get embedded in the root instructions with metadata markers.
-
Agents defined in
.agentsmesh/agents/code-reviewer.mdbecome.claude/agents/code-reviewer.mdfor Claude (native), or get embedded inAGENTS.mdfor Cursor and Codex (which read that file). -
Commands become
.claude/commands/deploy.mdfor Claude,.gemini/commands/deploy.tomlfor Gemini (different format entirely), or get embedded in the root instructions for tools that don't have a commands directory. -
Activation semantics survive too — Cursor's
alwaysApply: falseround-trips through Claude generation and back into canonical form without getting inverted.
Hooks and permissions are first-class
hooks.yaml and permissions.yaml are canonical types alongside rules and skills. Hooks generate in each target's native format — JSON config in settings for Claude, wrapper scripts for Copilot, per-file .kiro.hook for Kiro. Permissions project to native settings where the target supports it (currently Claude Code has full native support; others embed or partially support).
Targets are data, not classes
A target is a TargetDescriptor value declared in src/targets/<id>/index.ts. The engine reads the descriptor and dispatches generically — it never branches on target name. Capability levels (native | embedded | partial | none) are declared per feature in the descriptor:
capabilities: {
rules: 'native',
commands: 'native',
agents: 'embedded', // no native agent dir → embed in root instructions
skills: 'native',
mcp: 'native',
hooks: 'partial', // limited event support
permissions: 'none',
ignore: 'native',
}
The builtin catalog is auto-discovered at build time by scanning src/targets/*/index.ts. agentsmesh target scaffold foo-ide generates the full structure — descriptor, generators, importer, linter, constants, tests — so you start from a working base. No enum to update, no shared code to edit.
Plugins ship as standalone npm packages
npx agentsmesh plugin add agentsmesh-target-foo-ide
npx agentsmesh generate
Plugins get full built-in parity — same descriptor schema, same capability levels, same lint hooks and global-mode support as built-ins. New tool support doesn't wait for me.
CI/dev workflow
- run: npx agentsmesh check # exits 1 on drift
The rest of the surface:
-
agentsmesh diff— preview whatgeneratewould change, without writing. -
agentsmesh lint— validate canonical config against per-target constraints. -
agentsmesh watch— regenerate target files on save during local editing. -
agentsmesh merge— resolve three-way.lockconflicts aftergit merge. -
agentsmesh matrix— print the full support matrix for all targets and features.
Global mode for personal config
npx agentsmesh init --global
npx agentsmesh import --global --from claude-code
npx agentsmesh generate --global # writes ~/.claude/, ~/.cursor/, ~/.codex/, ...
~/.agentsmesh/ is for personal setup across every repo you touch. Every CLI command accepts --global.
Native Windows
Path-format detection (/proj vs C:\proj) decoupled from host platform. chokidar polling on Windows for ReadDirectoryChangesW edge cases. basename() everywhere instead of split('/'). CI matrix runs Linux + macOS + Windows.
Typed programmatic API
Same surface as the CLI, exported with full .d.ts under strict TS:
import { loadProjectContext, generate, lint, diff, check } from 'agentsmesh';
const project = await loadProjectContext(process.cwd());
await generate(project);
const drift = await check(project);
Subpath imports for narrower bundles: agentsmesh/engine, agentsmesh/canonical, agentsmesh/targets.
JSON Schema for every config
agentsmesh.yaml, hooks.yaml, permissions.yaml, mcp.json, pack.json — all ship with generated $schema references. VS Code and JetBrains autocomplete + validation work out of the box.
Community packs
npx agentsmesh install github:org/shared-config@v1.0.0
npx agentsmesh install --sync # restore all packs after clone
Packs live in .agentsmesh/packs/, track in installs.yaml, and merge into canonical config on every generate.
What it doesn't do
- No GUI. It's a CLI and a library.
- Doesn't manage secrets. MCP server tokens stay in your existing secret store.
- Won't help if you only use one tool. The canonical layer is overhead.
- Smaller ecosystem. Fewer community packs and fewer eyes on it.
Try it
npm install -D agentsmesh
npx agentsmesh init
# or, in an existing project:
npx agentsmesh import --from cursor
npx agentsmesh generate
Linux, macOS, Windows (native, not WSL). MIT. Node 20+. ESM-only. Strict TypeScript.
Source: github.com/sampleXbro/agentsmesh · Docs: samplexbro.github.io/agentsmesh
Top comments (0)