| 1 | # Write your first Codewhale plugin |
| 2 | |
| 3 | > 阅读简体中文版:[zh_hans/PLUGIN_AUTHORING.md](zh_hans/PLUGIN_AUTHORING.md) |
| 4 | |
| 5 | Start with a skill: a Markdown instruction file inside a small plugin bundle. |
| 6 | The [hello-codewhale example](examples/plugins/hello-codewhale/plugin.json) |
| 7 | contains two files, declares no server or hook, and asks for no tool use. |
| 8 | This walkthrough takes it from source files to a reviewed, enabled skill. |
| 9 | |
| 10 | ## 1. Create the bundle |
| 11 | |
| 12 | Use the checked-in example, or create this directory outside an installed |
| 13 | plugins directory: |
| 14 | |
| 15 | ```text |
| 16 | hello-codewhale/ |
| 17 | ├── plugin.json |
| 18 | └── skills/ |
| 19 | └── hello/ |
| 20 | └── SKILL.md |
| 21 | ``` |
| 22 | |
| 23 | `plugin.json`: |
| 24 | |
| 25 | ```json |
| 26 | { |
| 27 | "$schema": "https://agent-plugins.org/schemas/plugin.json", |
| 28 | "name": "hello-codewhale", |
| 29 | "version": "0.1.0", |
| 30 | "description": "A minimal, explicitly invoked greeting skill." |
| 31 | } |
| 32 | ``` |
| 33 | |
| 34 | `skills/hello/SKILL.md`: |
| 35 | |
| 36 | ```markdown |
| 37 | --- |
| 38 | name: hello |
| 39 | description: Greet the user when they explicitly try the hello-codewhale example. |
| 40 | invocation: explicit-only |
| 41 | --- |
| 42 | |
| 43 | Respond with one short greeting in the user's language. Include the exact text |
| 44 | `hello-codewhale:hello` so they can identify the example they invoked. |
| 45 | |
| 46 | Use only the conversation. Do not call tools, run commands, read or write files, |
| 47 | or contact external services. |
| 48 | ``` |
| 49 | |
| 50 | Codewhale finds `skills/` automatically. `explicit-only` keeps this example out |
| 51 | of the model's automatic skill catalogue; you load it by name. See |
| 52 | [Skills](SKILLS.md#invocation-and-alias-metadata) for invocation metadata and |
| 53 | [Plugin bundles](PLUGIN_BUNDLES.md#manifest) for the authoritative manifest |
| 54 | contract. Keep new native bundles in `plugin.json`; no second manifest is |
| 55 | needed. |
| 56 | |
| 57 | ## 2. Install, inspect, and trust |
| 58 | |
| 59 | Start Codewhale in the repository root. Enter these commands **inside the |
| 60 | Codewhale session**, one at a time: |
| 61 | |
| 62 | ```text |
| 63 | /plugin install ./docs/examples/plugins/hello-codewhale |
| 64 | /plugin validate hello-codewhale |
| 65 | /plugin show hello-codewhale |
| 66 | ``` |
| 67 | |
| 68 | For your own bundle, replace the install path with its directory. Installation |
| 69 | copies it to `~/.codewhale/plugins/hello-codewhale/`, disabled and untrusted. |
| 70 | Review the installed source, skill inventory, and permissions. This example |
| 71 | should declare only Skills, with no MCP server, hook, or requested network host. |
| 72 | |
| 73 | The install review prints a command containing two full hashes: |
| 74 | |
| 75 | ```text |
| 76 | /plugin trust hello-codewhale <full-content-sha256>.<full-capability-sha256> |
| 77 | ``` |
| 78 | |
| 79 | Run the exact command printed by your review; the angle-bracket text above is |
| 80 | a placeholder. If you need a fresh review, use `/plugin trust hello-codewhale` |
| 81 | without a token. Trust records the reviewed content and capability hashes and |
| 82 | creates a runtime snapshot. It does not enable the plugin. |
| 83 | |
| 84 | ```text |
| 85 | /plugin enable hello-codewhale |
| 86 | /skills hello-codewhale: |
| 87 | /skills inspect |
| 88 | ``` |
| 89 | |
| 90 | The skill is named `hello-codewhale:hello`: the bundle name qualifies the skill |
| 91 | name. `/skills inspect` identifies its reviewed plugin snapshot. |
| 92 | |
| 93 | ## 3. Invoke it and turn it off |
| 94 | |
| 95 | ```text |
| 96 | /skill hello-codewhale:hello |
| 97 | ``` |
| 98 | |
| 99 | Codewhale confirms activation. Then send `Say hello.` as a normal message. |
| 100 | The reply should be a short greeting containing `hello-codewhale:hello`. |
| 101 | The example contributes instructions only; the reply still uses your selected |
| 102 | model and its normal provider connection. The local install, review, and |
| 103 | activation steps do not need a model call. |
| 104 | |
| 105 | ```text |
| 106 | /plugin disable hello-codewhale |
| 107 | ``` |
| 108 | |
| 109 | Disabling removes the plugin's contributions while preserving its trust |
| 110 | receipt. A subsequent `/skill hello-codewhale:hello` must not activate it. |
| 111 | Enable it again when needed, provided its reviewed hashes still match. |
| 112 | |
| 113 | ## 4. Iterate and review changes |
| 114 | |
| 115 | The installed bundle is a copy. Editing the example's original source does |
| 116 | not update that copy. To try a changed local source, disable and uninstall the |
| 117 | installed example, then install the source directory again: |
| 118 | |
| 119 | ```text |
| 120 | /plugin disable hello-codewhale |
| 121 | /plugin uninstall hello-codewhale |
| 122 | /plugin install ./docs/examples/plugins/hello-codewhale |
| 123 | /plugin validate hello-codewhale |
| 124 | ``` |
| 125 | |
| 126 | Uninstall removes the installed copy; it leaves the original example source |
| 127 | alone. Review the new token, trust it, and enable it again. For bundles |
| 128 | installed from a remote source, use `/plugin update <name>`; see |
| 129 | [Installing plugins](PLUGINS.md#update-and-uninstall). |
| 130 | |
| 131 | When files in a discovered bundle change directly, `/plugin reload` refreshes |
| 132 | the registry. Changed content invalidates the old receipt, even if you leave |
| 133 | the version unchanged. Reload does not grant trust. Use `/plugin revoke <name>` |
| 134 | to remove trust explicitly. |
| 135 | |
| 136 | ## Add only the components you need |
| 137 | |
| 138 | All components use the same bundle review and existing Codewhale runtime: |
| 139 | |
| 140 | | Component | Authoring surface | |
| 141 | | --- | --- | |
| 142 | | Skills | `skills/<name>/SKILL.md`; [instruction and invocation contract](SKILLS.md). | |
| 143 | | MCP | A sibling `mcp.json`; [bundle transport and credential rules](PLUGIN_BUNDLES.md#validation-both-formats). | |
| 144 | | Commands | Markdown command files; [command metadata](architecture/command-dispatch.md#user-commands). | |
| 145 | | Agent profiles | Fleet TOML profiles; [Fleet authoring](FLEET.md#authoring-agent-profiles-fleet-setup). | |
| 146 | | Hooks | `HooksConfig` TOML files; [events and process behavior](HOOKS.md). | |
| 147 | |
| 148 | Declare Commands, Agents, and Hooks paths under |
| 149 | `extensions["net.codewhale"]` in `plugin.json`, as specified in |
| 150 | [Plugin bundles](PLUGIN_BUNDLES.md#active-and-inactive-component-surfaces). |
| 151 | Do not place MCP server fields or arbitrary runtime entrypoints at the manifest |
| 152 | root. LSP and native extensions can be inventoried but are not executable |
| 153 | plugin adapters. |
| 154 | |
| 155 | Plugin trust is **not an OS sandbox**. A local MCP server or hook can launch a |
| 156 | process; review its code and authority before enabling it. Skills do not grant |
| 157 | permissions: repository instructions, permission rules, sandbox policy, and |
| 158 | tool approval still apply. Keep credentials out of bundles and command |
| 159 | arguments. Use the reviewed environment references documented in the |
| 160 | [bundle validation contract](PLUGIN_BUNDLES.md#validation-both-formats) for MCP; |
| 161 | read the separate [hook environment contract](HOOKS.md#the-hook-process-environment) |
| 162 | before adding a hook. |
| 163 | |
| 164 | ## Convert an existing plugin |
| 165 | |
| 166 | [`scripts/convert-plugin.py`](../scripts/convert-plugin.py) converts explicitly |
| 167 | selected remote MCP declarations, packaged local Node MCP servers, and portable |
| 168 | Skills into a native bundle. |
| 169 | It requires Python 3.10+ and PyYAML 6+; install those separately if absent. |
| 170 | The converter installs no dependencies, scans no ambient configuration or |
| 171 | credentials, makes no network requests, and executes no source code. |
| 172 | |
| 173 | ### OpenCode |
| 174 | |
| 175 | Save this plain JSON as `opencode-mcp.json`: |
| 176 | |
| 177 | ```json |
| 178 | { |
| 179 | "mcp": { |
| 180 | "docs": { |
| 181 | "type": "remote", |
| 182 | "url": "https://example.invalid/mcp", |
| 183 | "oauth": false, |
| 184 | "enabled": false |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | ``` |
| 189 | |
| 190 | From the Codewhale repository root, run this in your shell: |
| 191 | |
| 192 | ```sh |
| 193 | python3 scripts/convert-plugin.py --format opencode-v1 \ |
| 194 | --config ./opencode-mcp.json --name migrated-tools --output ./migrated-opencode |
| 195 | ``` |
| 196 | |
| 197 | Choose `--format opencode-v2` for the `mcp.servers.<name>` layout, whose server |
| 198 | flag is `disabled` instead of `enabled`. Select the format from the data; |
| 199 | filenames and upstream branch names do not determine its version. Both formats |
| 200 | require explicit `oauth: false` for remote servers. Remote MCP output uses **Streamable HTTP only**; |
| 201 | OpenCode's fallback to legacy SSE is not reproduced. For an SSE-only endpoint, |
| 202 | author native `mcp.json` with `type: "sse"` and use the same review flow. |
| 203 | |
| 204 | When MCP servers are selected, configurations containing `tools`, |
| 205 | `permission`/`permissions`, `agent`/`agents`, legacy `mode`, or `default_agent` |
| 206 | are refused. These settings can restrict tool access beyond server enablement. |
| 207 | Manually preserve those restrictions in Codewhale before supplying an MCP-only |
| 208 | input; simply deleting the settings can widen access. |
| 209 | |
| 210 | JSONC comments and trailing commas are not |
| 211 | accepted: provide a plain JSON copy containing the declarations you intend |
| 212 | to port. |
| 213 | |
| 214 | ### DeepSeek Harness (DSH) |
| 215 | |
| 216 | Save this static Cordis entry list as `dsh-mcp.yml`: |
| 217 | |
| 218 | ```yaml |
| 219 | - name: '@deepseek-ai/dsh-mcp-client' |
| 220 | disabled: true |
| 221 | config: |
| 222 | serverName: docs |
| 223 | transport: streamable-http |
| 224 | url: https://example.invalid/mcp |
| 225 | ``` |
| 226 | |
| 227 | ```sh |
| 228 | python3 scripts/convert-plugin.py --format dsh \ |
| 229 | --config ./dsh-mcp.yml --name migrated-dsh --output ./migrated-dsh |
| 230 | ``` |
| 231 | |
| 232 | The DSH input may also be JSON, but must be the plain entry list, not a full |
| 233 | profile or patch composition. Each row must name `@deepseek-ai/dsh-mcp-client`. |
| 234 | |
| 235 | A real dsh bundle package — an npm package whose `package.json` declares |
| 236 | `dsh.bundle.patch` — converts directly with `--bundle`: |
| 237 | |
| 238 | ```sh |
| 239 | python3 scripts/convert-plugin.py --format dsh \ |
| 240 | --bundle ./node_modules/@demo/tools-dsh --name migrated-dsh --output ./migrated-dsh |
| 241 | ``` |
| 242 | |
| 243 | The converter reads the package's `cordis.patch.yml`, applies its `insert` and |
| 244 | keyed-override operations over an empty profile (matching `applyEntryPatches`), |
| 245 | and converts each resulting row. Rows it cannot represent — runtime plugins, |
| 246 | `dsh.client` UI code, `!!js` expressions outside the documented idioms, |
| 247 | conditional `disabled` flags — are listed in `CONVERSION.md` rather than |
| 248 | silently dropped. The `!!js` idioms it does lower: `process.execPath` (becomes |
| 249 | `node`), `process.env.NAME` and `process.env.NAME || 'literal'` (resolved |
| 250 | against this machine), and `` `${process.env.NAME}...` `` templates. An `args` |
| 251 | entry that resolves to a host file is snapshotted: its containing directory is |
| 252 | copied into `mcp/<server>` and the resolution is recorded in the receipt. Rows |
| 253 | of `@deepseek-ai/dsh-skill-filesystem` contribute their `customSkillDirs` |
| 254 | children as skills when those directories live inside the package. |
| 255 | |
| 256 | ### Local Node MCP servers |
| 257 | |
| 258 | For an already packaged Node MCP server, select its original process working |
| 259 | directory explicitly. The converter copies that directory into `mcp/<server>` |
| 260 | and sets the native server's working directory to the reviewed copy. Relative |
| 261 | entrypoint imports and read-only resources keep the same layout. |
| 262 | |
| 263 | ```json |
| 264 | { |
| 265 | "mcp": { |
| 266 | "localdocs": { |
| 267 | "type": "local", |
| 268 | "command": ["node", "server.mjs"], |
| 269 | "environment": {"API_TOKEN": "{env:LOCALDOCS_TOKEN}"}, |
| 270 | "enabled": false |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | ``` |
| 275 | |
| 276 | ```sh |
| 277 | python3 scripts/convert-plugin.py --format opencode-v1 \ |
| 278 | --config ./local-mcp.json --stdio-root localdocs=./packaged-localdocs \ |
| 279 | --name local-tools --output ./migrated-local |
| 280 | ``` |
| 281 | |
| 282 | Repeat `--stdio-root SERVER=DIRECTORY` for every local server in the selected |
| 283 | configuration. OpenCode v2 uses `mcp.servers` and `disabled`. Static DSH entries |
| 284 | use `transport: stdio`, `command: node`, and `args: [server.mjs]`; DSH `env` |
| 285 | must be absent or empty because its literals/expressions are not OpenCode |
| 286 | environment references. Optional DSH/v2 `cwd` must be absent, empty, or `.`; |
| 287 | the selected root explicitly supplies the original working directory. |
| 288 | |
| 289 | Use `node` plus one relative `.mjs`, `.js`, or `.cjs` entry. Package module |
| 290 | type and sibling imports are preserved by the native launch adapter. Compile |
| 291 | TypeScript to JavaScript before packaging; the converter does not run a compiler. |
| 292 | Package dependencies and read-only resources first, inside the selected root. |
| 293 | No package manager, install script, module loader or server runs during |
| 294 | conversion. Links/reparse points, hard-linked files, hidden files/directories |
| 295 | (including `.gitignore`, `.env*`, `.npmrc` and `node_modules/.bin`), common |
| 296 | credential filenames, and private-key containers are refused. Prepare a clean |
| 297 | package directory; ignore rules are not used to silently omit files. Inspect |
| 298 | every selected file for embedded credentials before conversion. The existing |
| 299 | 4,096-file / 64 MiB aggregate bundle limit applies. |
| 300 | |
| 301 | The converter rejects shell launchers, Node flags, extra arguments, non-Node |
| 302 | interpreters, literal environment values, and loader-changing environment |
| 303 | names. Stateful servers that write into their working directory, depend on the |
| 304 | live workspace, or import files outside the package need a manual native port. |
| 305 | Copying files does not statically verify JavaScript import closure or sandbox |
| 306 | arbitrary code. Local MCP processes run with host-user authority; their network |
| 307 | and filesystem access are not restricted by the remote endpoint host list. |
| 308 | The same native install, capability review, hash-bound trust, and enable steps |
| 309 | are required before Codewhale launches the server. This adds a packaged Node |
| 310 | MCP subset; it does not execute DSH/Cordis plugin modules. |
| 311 | |
| 312 | ### Review the result |
| 313 | |
| 314 | Both examples preserve disabled servers and use a placeholder endpoint. Replace |
| 315 | the endpoint and change the source's enablement flag before reconverting when |
| 316 | you are ready to connect. The output directory must be new, with an existing |
| 317 | parent. Existing output is refused; rejected input leaves no output bundle. |
| 318 | |
| 319 | Add `--skill ./my-skill` for an explicitly selected directory containing |
| 320 | `SKILL.md`, or `--skill ./my-skill.md` for a single file; repeat the option for |
| 321 | more skills. `--config` is optional for a skills-only conversion. Skills require |
| 322 | `name` and `description` frontmatter. `disable-model-invocation: true` becomes |
| 323 | native `invocation: explicit-only`. Informational `license`, `compatibility`, |
| 324 | and `metadata` fields are retained in `SOURCE_SKILL_METADATA.json` companion |
| 325 | data. Companion files from selected skill directories are copied as data; |
| 326 | review them and the instructions before loading the skill. |
| 327 | |
| 328 | Only exact OpenCode header references such as `{env:MCP_TOKEN}` become native |
| 329 | `env_headers`; the converter never reads the variable's value. Literal headers, |
| 330 | DSH header expressions, and URL file/environment substitution are refused. |
| 331 | Configured timeouts must be whole seconds expressed in milliseconds, from |
| 332 | `1000` through `3600000`. Omitted timeouts use Codewhale's defaults. |
| 333 | |
| 334 | Executable foreign plugins and hooks, other stdio launchers, automatic OAuth, |
| 335 | configuration JavaScript, |
| 336 | YAML aliases/tags, `__jsExpr`, and unsupported skill runtime fields (including |
| 337 | `user-invocable: false`) require a manual port. Conversion does not reproduce |
| 338 | another client's runtime or bypass Codewhale's credential and sandbox rules. |
| 339 | |
| 340 | Read the generated `CONVERSION.md`, `plugin.json`, `mcp.json` when present, and |
| 341 | all selected skill and MCP source files. Then use `/plugin install ./migrated-opencode` (or the |
| 342 | DSH output path), `/plugin validate <name>`, and the same hash-bound trust and |
| 343 | enable flow above. Conversion alone proves neither connectivity nor runtime |
| 344 | compatibility; the output is not installed, trusted, or enabled. |
| 345 | |
| 346 | Source audit, 2026-09-08: OpenCode's [v1 MCP documentation](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/web/src/content/docs/mcp-servers.mdx) |
| 347 | and [v2 MCP schema](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/core/src/config/mcp.ts) |
| 348 | at `d6855b6b47`, and DSH's [MCP client reference](https://github.com/deepseek-ai/deepseek-harness/blob/c389f96bf3a9b6807cb71ed6bdad5849be0df6d8/packages/mcp/mcp-client/README.md) |
| 349 | at `c389f96bf3`. Upstream supports more than this deliberately bounded converter. |
| 350 | |
| 351 | ## Community context |
| 352 | |
| 353 | This guide responds to [giancarlocp's request for plugin authoring guidance |
| 354 | and OpenCode conversion in discussion #5827](https://github.com/Hmbown/Codewhale/discussions/5827). |
| 355 | The Chinese companion follows the documentation work requested by |
| 356 | [SparkofSpike in issue #5482](https://github.com/Hmbown/Codewhale/issues/5482). |
| 357 |