---
title: Building Plugins
description: Build an AgentsMesh plugin that generates config files for any AI coding tool — from a minimal rule generator to full feature parity with built-in targets.
---

import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';

A plugin is an npm package that exports a **descriptor** — the same object built-in targets use — telling AgentsMesh how to generate, import, lint, and detect config files for an AI tool. Plugins get full parity: generation, import, linting, global mode, conversions, and everything else.

The pipeline is straightforward:

```
.agentsmesh/rules/*.md          descriptor.generateRules()
.agentsmesh/commands/*.md   →   descriptor.generateCommands()   →   .foo-ide/rules/*.md
.agentsmesh/mcp.json            descriptor.generateMcp()            .foo-ide/mcp.json
    (canonical source)              (your plugin)                   (generated output)
```

Your plugin sits in the middle — it receives parsed canonical files and returns `{ path, content }` pairs that AgentsMesh writes to disk. The reverse direction (import) reads tool-native files and writes canonical files back to `.agentsmesh/`.

## Your First Plugin

<Steps>

1. **Create the package**

   ```bash
   mkdir agentsmesh-target-foo-ide
   cd agentsmesh-target-foo-ide
   npm init -y
   ```

   Set `"type": "module"` and declare `agentsmesh` as a peer dependency in `package.json`:

   ```json
   {
     "name": "agentsmesh-target-foo-ide",
     "version": "1.0.0",
     "type": "module",
     "main": "index.js",
     "exports": {
       ".": "./index.js"
     },
     "peerDependencies": {
       "agentsmesh": ">=0.6"
     }
   }
   ```

2. **Write the descriptor**

   Create `index.js` with a single named export:

   ```js
   // index.js
   export const descriptor = {
     // ---- Identity ----
     id: 'foo-ide',

     // ---- User-facing metadata (required) ----
     // Drives display names in `agentsmesh matrix`, the support-matrix doc,
     // and any UI rendering that lists targets. Plugins missing `metadata`
     // are rejected at load time.
     metadata: {
       displayName: 'Foo IDE',
       category: 'ide',                       // 'cli' | 'ide' | 'agent-platform'
       officialUrl: 'https://example.test/foo-ide',
       shortDescription: 'Foo IDE — a fictional editor with native rule support.',
     },

     // ---- Generation ----
     generators: {
       name: 'foo-ide',
       generateRules(canonical) {
         return canonical.rules.map((rule) => ({
           path: `.foo-ide/rules/${rule.slug}.md`,
           content: rule.body,
         }));
       },
       async importFrom() { return []; },
     },

     // ---- Capabilities: what this tool supports ----
     capabilities: {
       rules: 'native',
       additionalRules: 'none', commands: 'none', agents: 'none',
       skills: 'none', mcp: 'none', hooks: 'none',
       ignore: 'none', permissions: 'none',
     },

     // ---- Layout: where outputs live on disk ----
     project: {
       managedOutputs: {
         dirs: ['.foo-ide/rules'],
         files: [],
       },
       paths: {
         rulePath: (slug) => `.foo-ide/rules/${slug}.md`,
         commandPath: () => null,
         agentPath: () => null,
       },
     },

     // ---- Plumbing ----
     emptyImportMessage: 'No Foo IDE config found.',
     lintRules: null,
     buildImportPaths: async () => {},
     detectionPaths: ['.foo-ide'],
   };
   ```

   Every non-`none` capability must have a matching generator. The `metadata` block must be present (all four subfields are required). AgentsMesh validates this at load time with a Zod schema and warns (or hard-errors under `AGENTSMESH_STRICT_PLUGINS=1`) if something is missing.

   <Aside type="tip">
     Nine capability fields looks like a lot, but the explicit declaration lets AgentsMesh skip irrelevant generators, show accurate results in `agentsmesh matrix`, and validate that every advertised feature has an implementation.
   </Aside>

3. **Test locally**

   ```bash
   agentsmesh plugin add ./path/to/agentsmesh-target-foo-ide
   agentsmesh generate --dry-run    # preview without writing
   agentsmesh generate              # generate for real
   agentsmesh matrix                # confirm it appears
   ```

4. **Publish to npm**

   ```bash
   npm publish
   ```

   Users install with:

   ```bash
   agentsmesh plugin add agentsmesh-target-foo-ide
   agentsmesh generate
   ```

</Steps>

You can also export a `descriptors` array (for multi-target packages) or a `default` export — AgentsMesh checks all three.

## Adding More Features

The minimal plugin above only generates rules. Most AI tools support commands, agents, MCP servers, and more. Each feature follows the same pattern: declare a capability, add a generator, and register the output path.

### Commands and Agents

Add `generateCommands` to `generators`, change `capabilities.commands` to `'native'`, and register the output path:

```js
generators: {
  // ...existing rules generator...
  generateCommands(canonical) {
    return canonical.commands.map((cmd) => ({
      path: `.foo-ide/commands/${cmd.name}.md`,
      content: `# ${cmd.name}\n\n${cmd.description}\n\n${cmd.body}`,
    }));
  },
},
capabilities: {
  // ...
  commands: 'native',       // was 'none'
},
project: {
  managedOutputs: {
    dirs: ['.foo-ide/rules', '.foo-ide/commands'],   // add the new dir
    files: [],
  },
  paths: {
    // ...
    commandPath: (name) => `.foo-ide/commands/${name}.md`,   // was () => null
  },
},
```

Agents follow the same pattern with `generateAgents`, `capabilities.agents`, and `agentPath`.

<Aside type="tip">
  **Choosing capability levels:** Use `'native'` when the tool reads the generated files directly. Use `'embedded'` when your plugin projects the feature into another format (like agents embedded as skills) with metadata for round-trip import. Use `'partial'` when some fields cannot be represented.
</Aside>

### Skills

Skills are directory-based — each skill is a folder with a `SKILL.md` file and optional supporting files:

```js
generators: {
  generateSkills(canonical) {
    return canonical.skills.map((skill) => ({
      path: `.foo-ide/skills/${skill.name}/SKILL.md`,
      content: `# ${skill.name}\n\n${skill.body}`,
    }));
  },
},
capabilities: {
  skills: 'native',
},
project: {
  skillDir: '.foo-ide/skills',
  managedOutputs: {
    dirs: ['.foo-ide/skills'],   // add skills dir
    files: [],
  },
},
```

### MCP, Permissions, Hooks, and Ignore

These four features are "settings-backed" — they can be satisfied either by a dedicated generator or by `emitScopedSettings` if the tool uses a single settings file.

With a dedicated generator:

```js
generators: {
  generateMcp(canonical) {
    if (!canonical.mcp) return [];
    return [{
      path: '.foo-ide/mcp.json',
      content: JSON.stringify(canonical.mcp, null, 2),
    }];
  },
  generateIgnore(canonical) {
    if (canonical.ignore.length === 0) return [];
    return [{
      path: '.fooignore',
      content: canonical.ignore.join('\n'),
    }];
  },
},
capabilities: {
  mcp: 'native',
  ignore: 'native',
},
```

If your tool reads all settings from a single file, see [Scoped Settings](#scoped-settings) in the Advanced section.

### Capability Levels

| Level | Meaning |
|-------|---------|
| `'native'` | The tool has first-class support for this feature |
| `'embedded'` | AgentsMesh projects the feature with metadata for round-trip |
| `'partial'` | Supported with limitations |
| `'none'` | Not supported — generator is skipped |

Capabilities can also be objects with an optional `flavor` field:

```js
capabilities: {
  agents: { level: 'embedded', flavor: 'markdown' },
},
```

## Adding Import Support

Import is the reverse pipeline — `agentsmesh import --from foo-ide` reads the tool's native files and writes canonical files to `.agentsmesh/`.

<Tabs>
  <TabItem label="Declarative (recommended)">
    Add an `importer` block to the descriptor. The shared runner walks each feature in canonical order, resolves scope-specific source paths, and dispatches to the right helper:

    ```js
    export const descriptor = {
      // ...other fields...
      importer: {
        rules: {
          feature: 'rules',
          mode: 'directory',
          source: { project: ['.foo-ide/rules'], global: ['.foo-ide/rules'] },
          canonicalDir: '.agentsmesh/rules',
          extensions: ['.md'],
          preset: 'rule',
        },
        mcp: {
          feature: 'mcp',
          mode: 'mcpJson',
          source: { project: ['.foo-ide/mcp.json'] },
          canonicalDir: '.agentsmesh',
          canonicalFilename: '.agentsmesh/mcp.json',
        },
        ignore: {
          feature: 'ignore',
          mode: 'flatFile',
          source: { project: ['.fooignore'] },
          canonicalDir: '.agentsmesh',
          canonicalFilename: '.agentsmesh/ignore',
        },
      },
      generators: {
        // ...
        async importFrom(projectRoot, options) {
          const { runDescriptorImport } = await import('agentsmesh/targets');
          return runDescriptorImport(descriptor, projectRoot, options?.scope ?? 'project');
        },
      },
    };
    ```

    Features whose `source` omits the active scope are automatically skipped — no `if (scope === 'global')` branches in your code.

    **Importer modes:**

    | Mode | Use case | Required fields |
    |------|----------|----------------|
    | `singleFile` | Root rule with fallback chain | `canonicalRootFilename`, optional `markAsRoot` |
    | `directory` | Recursive scan of a directory | `extensions`, `preset` (`rule`/`command`/`agent`) or custom `map` |
    | `flatFile` | Verbatim copy (e.g. ignore file) | `canonicalFilename` |
    | `mcpJson` | Parse `mcpServers` JSON | `canonicalFilename` |

    <Aside type="note">
      Use `directory` mode with a built-in `preset` (`'rule'`, `'command'`, `'agent'`) unless the tool's frontmatter differs from canonical format, in which case supply a custom `map` function or use `frontmatterRemap` for small field translations.
    </Aside>

  </TabItem>
  <TabItem label="Imperative">
    Write a custom `importFrom` that reads files from disk and returns `ImportResult[]`:

    ```js
    generators: {
      async importFrom(projectRoot, options) {
        const { readFileSync, existsSync } = await import('node:fs');
        const { join } = await import('node:path');
        const results = [];

        const mcpPath = join(projectRoot, '.foo-ide/mcp.json');
        if (existsSync(mcpPath)) {
          const content = readFileSync(mcpPath, 'utf-8');
          const { writeMcpWithMerge } = await import('agentsmesh/targets');
          await writeMcpWithMerge(projectRoot, '.agentsmesh/mcp.json', JSON.parse(content).mcpServers);
          results.push({
            fromTool: 'foo-ide',
            fromPath: mcpPath,
            toPath: '.agentsmesh/mcp.json',
            feature: 'mcp',
          });
        }

        return results;
      },
    },
    ```

    Use imperative import when the target format requires non-trivial parsing — embedded rule splitters, TOML/YAML native configs, or multi-file merges.

    <Aside type="tip">
      You can mix both approaches: use the declarative `importer` block for standard features and call `runDescriptorImport` from your imperative `importFrom` for those, while handling custom parsing manually.
    </Aside>

  </TabItem>
</Tabs>

## Project Layout

You have already seen `project.paths` and `project.managedOutputs` in the minimal plugin. Here are the other layout fields:

| Field | Purpose |
|-------|---------|
| `managedOutputs` | Directories and files that AgentsMesh fully manages — stale artifacts are deleted on `generate` |
| `rootInstructionPath` | Path to the primary root instruction file (e.g. `.foo-ide/ROOT.md`) |
| `skillDir` | Target-native skills directory for skill generation and import |
| `outputFamilies` | Declared output path families for reference rewriting between targets |

Output families enable AgentsMesh to rewrite internal file references from canonical paths to target-relative paths:

```js
project: {
  outputFamilies: [
    { id: 'rules', kind: 'primary', pathPrefix: '.foo-ide/rules/' },
    { id: 'commands', kind: 'additional', pathPrefix: '.foo-ide/commands/' },
  ],
},
```

## Testing and Validation

AgentsMesh validates every plugin descriptor with a Zod schema at load time. Invalid descriptors are warned and skipped — one bad plugin does not block other targets.

Common validation errors:
- `id` must match `^[a-z][a-z0-9-]*$`
- `metadata.displayName`, `metadata.category`, `metadata.officialUrl`, and `metadata.shortDescription` are all required; `category` must be one of `'cli' | 'ide' | 'agent-platform'`
- `generators.generateRules` and `generators.importFrom` are required
- Non-`none` capabilities must have their matching generator (or `emitScopedSettings` for settings-backed features)

For CI release jobs that must not silently skip a broken plugin, set `strict: true` on the plugin entry or run with `AGENTSMESH_STRICT_PLUGINS=1`. See the [`plugin` CLI reference](../../cli/plugin/#strict-mode) for the full contract.

```bash
agentsmesh generate --dry-run    # preview generated output
agentsmesh matrix                # verify capabilities appear correctly
agentsmesh lint                  # run lint hooks across all targets
```

## Publishing

1. Ensure `"type": "module"` and `"peerDependencies"` are set in `package.json`
2. Export `descriptor` (named), `descriptors` (array), or `default` from your entry point
3. Name your package `agentsmesh-target-<id>` by convention
4. Publish:

```bash
npm publish
```

Users install and generate:

```bash
agentsmesh plugin add agentsmesh-target-foo-ide
agentsmesh generate
```

---

## Advanced Features

The features below are used by targets with complex requirements. Most plugins will not need them. Each section is self-contained.

### Global Mode

To support `agentsmesh generate --global`, add `globalSupport` — a single object bundling global capabilities, detection paths, and layout:

```js
globalSupport: {
  capabilities: {
    rules: 'native',
    additionalRules: 'none', commands: 'embedded', agents: 'embedded',
    skills: 'native', mcp: 'native', hooks: 'none',
    ignore: 'native', permissions: 'none',
  },
  detectionPaths: ['.foo-ide', '.foo-ide/ROOT.md'],
  layout: {
    rootInstructionPath: '.foo-ide/ROOT.md',
    skillDir: '.foo-ide/skills',
    paths: {
      rulePath: (slug) => `.foo-ide/rules/${slug}.md`,
      commandPath: (name) => `.foo-ide/commands/${name}.md`,
      agentPath: (name) => `.foo-ide/agents/${name}.md`,
    },
    rewriteGeneratedPath(path) {
      return path;
    },
  },
},
```

<Aside type="caution">
  `rewriteGeneratedPath` is required for global mode — it transforms project-level paths into user-level paths under `$HOME`. Return `null` to suppress a specific path from global output.
</Aside>

### Feature Conversions

If your target lacks native support for commands or agents but can represent them as skills, declare `supportsConversion`:

```js
supportsConversion: { commands: true, agents: true },
capabilities: {
  commands: 'embedded',
  agents: 'embedded',
},
```

When declared, conversion defaults to enabled. Users can override in `agentsmesh.yaml`:

```yaml
conversions:
  commands_to_skills:
    foo-ide: true
  agents_to_skills:
    foo-ide:
      project: true
      global: false
```

See the [Conversions reference](../../configuration/conversions/) for full syntax.

### Per-Feature Lint Hooks

Add target-specific validation for individual features:

```js
lint: {
  commands(canonical, options) {
    const issues = [];
    for (const cmd of canonical.commands) {
      if (!cmd.description) {
        issues.push({
          level: 'warning',
          file: cmd.source || '',
          target: 'foo-ide',
          message: `Command '${cmd.name}' is missing a description`,
        });
      }
    }
    return issues;
  },
},
```

The `options` parameter includes `{ scope: 'project' | 'global' }`. Available hooks: `commands`, `mcp`, `permissions`, `hooks`, `ignore`.

### Scoped Settings

If your tool reads all config from a native settings file, use `emitScopedSettings` instead of individual generators for `mcp`, `hooks`, `ignore`, and `permissions`:

```js
emitScopedSettings(canonical, scope) {
  const settings = {
    version: 1,
    featureCount: canonical.rules.length,
  };
  return [{ path: '.foo-ide/settings.json', content: JSON.stringify(settings, null, 2) }];
},
```

### Hook Post-Processing

Generate additional files alongside hooks (like wrapper scripts) with `postProcessHookOutputs`:

```js
async postProcessHookOutputs(projectRoot, canonical, outputs) {
  const processed = [...outputs];
  for (const output of outputs) {
    if (output.path.endsWith('.json')) {
      processed.push({
        path: output.path.replace('.json', '.wrapper.sh'),
        content: '#!/bin/sh\nexec "$@"\n',
      });
    }
  }
  return processed;
},
```

### Scope Extras

Generate additional global outputs beyond the standard feature loop with `globalSupport.scopeExtras`:

```js
globalSupport: {
  // ...capabilities, detectionPaths, layout...
  async scopeExtras(canonical, projectRoot, scope, enabledFeatures) {
    if (scope === 'global' && enabledFeatures.has('rules')) {
      return [{ path: '.foo-ide/scope-info.txt', content: `scope=${scope}` }];
    }
    return [];
  },
},
```

### Shared Artifacts

If your target generates files into a path shared with other targets, declare ownership:

```js
sharedArtifacts: {
  '.foo-ide/skills/': 'owner',
},
```

Use `'owner'` if your target controls writes to that prefix, or `'consumer'` if you only read from it. Only one target can be the owner of a given prefix — AgentsMesh fails fast at startup if two targets claim the same one.

### Content Merging

If your target needs to merge generated content into an existing file rather than overwriting, use `mergeGeneratedOutputContent`:

```js
mergeGeneratedOutputContent(existingContent, pendingContent, newContent, path) {
  if (path.endsWith('settings.json')) {
    const existing = JSON.parse(existingContent || '{}');
    const update = JSON.parse(newContent);
    return JSON.stringify({ ...existing, ...update }, null, 2);
  }
  return newContent;
},
```

## Descriptor Reference

### Required Fields

| Field | Type | Purpose |
|-------|------|---------|
| `id` | `string` | Unique target ID (`^[a-z][a-z0-9-]*$`) |
| `metadata` | `TargetMetadata` | User-facing metadata (`displayName`, `category: 'cli' \| 'ide' \| 'agent-platform'`, `officialUrl`, `shortDescription`). Drives matrix display and target lists |
| `generators` | `TargetGenerators` | Feature generators (`generateRules`, `importFrom` required; others per capability) |
| `capabilities` | `TargetCapabilities` | Support levels for all 9 features |
| `emptyImportMessage` | `string` | Shown when `import --from` finds nothing |
| `lintRules` | `function \| null` | Rule linter callback, or `null` to skip |
| `project` | `TargetLayout` | Project-scope layout (paths, managed outputs) |
| `buildImportPaths` | `function` | Populates import reference map |
| `detectionPaths` | `string[]` | Paths for target detection during `init` |

### Optional Fields

| Field | Type | Purpose |
|-------|------|---------|
| `globalSupport` | `GlobalTargetSupport` | Global-scope support (capabilities, detection, layout, scopeExtras) |
| `supportsConversion` | `{ commands?, agents? }` | Enables user-configurable feature projection |
| `conversionDefaults` | `{ commandsToSkills?, agentsToSkills? }` | Built-in defaults for `commands_to_skills` / `agents_to_skills` projection. `undefined` falls back to caller-supplied default |
| `excludeFromStarterInit` | `boolean` | When `true`, `agentsmesh init` skips this target in the default bulk starter scaffold; explicit `--target <id>` still works |
| `preservesManualActivation` | `boolean` | When `true`, the target preserves manual-only activation semantics (e.g. Cursor's `alwaysApply: false`); other targets get a lint warning on `trigger: 'manual'` rules |
| `importer` | `TargetImporterDescriptor` | Declarative importer for scan + map orchestration |
| `lint` | `TargetLintHooks` | Per-feature lint hooks |
| `sharedArtifacts` | `Record<string, 'owner' \| 'consumer'>` | Shared output path ownership |
| `emitScopedSettings` | `function` | Emit native settings sidecar files |
| `mergeGeneratedOutputContent` | `function` | Custom merge for generated outputs |
| `postProcessHookOutputs` | `function` | Async post-pass for hook outputs |

## What's Next

- [Plugin CLI commands](../../cli/plugin/) — `plugin add`, `plugin remove`, `plugin list`
- [Generation Pipeline](../../reference/generation-pipeline/) — how the engine processes your descriptor
- [Supported Tools Matrix](../../reference/supported-tools/) — see how built-in targets declare capabilities
- [Conversions reference](../../configuration/conversions/) — user-facing conversion config syntax
- [Extending AgentsMesh](../extending/) — scaffold a new built-in target (contributing upstream)
- [Rich plugin test fixture](https://github.com/sampleXbro/agentsmesh/blob/master/tests/fixtures/plugins/rich-plugin/index.js) — comprehensive example exercising every descriptor field
