DEV Community

Serhii Zhabskyi
Serhii Zhabskyi

Posted on

I had four AI coding assistants and the same config in five places. Here's what I built.

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)
Enter fullscreen mode Exit fullscreen mode

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.jsonpackage-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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.md become .claude/agents/code-reviewer.md for Claude (native), or get embedded in AGENTS.md for Cursor and Codex (which read that file).
  • Commands become .claude/commands/deploy.md for Claude, .gemini/commands/deploy.toml for 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: false round-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',
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The rest of the surface:

  • agentsmesh diff — preview what generate would 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 .lock conflicts after git 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/, ...
Enter fullscreen mode Exit fullscreen mode

~/.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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)