Packaging AI Coding Workflows As Plugins

In this article we will cover how to package a portable agent skill into installable plugins for Cursor, Claude Code, and VS Code Copilot.
By Boris Delovski • Updated on Jul 21, 2026
blog image

In a previous article, we built a complete, portable skill. That skill makes our work easier, but sharing it is another matter. As soon as a teammate wants it, you're back to saying "copy this folder into the right place and don't touch anything," and every time you make an improvement, you end up coordinating updates over chat. Skills solve the problem of repeatable agent workflows. Plugins solve the problem of getting those workflows out to tools, teams, and projects.

In this article, we will explain what an agent plugin actually is. We'll package the technology-adoption reviewer into two installable plugins, one for Cursor and one in the Claude format, which conveniently covers both Claude Code and VS Code Copilot. We'll break down the manifest and hook files that make the packages work, look closely at one genuinely interesting engineering problem that hooks introduce, and finish with local installation and marketplace distribution.

What an Agent Plugin Is

An agent plugin is an installable package of agent customizations. It usually wraps one or more skills together with the host-specific wiring those skills need, including:

  • a manifest
  • hook configurations
  • scripts
  • sometimes rules, agents, or MCP configurations

The key idea to internalize is that the plugin is not the workflow. The workflow still lives in the portable files we built previously, such as the SKILL.md file, the reference checklists, the report template, the examples, and the validator. The plugin is the wrapper that makes that workflow discoverable, installable, versioned, and updatable inside a specific host. This is the same portable core and host adapter split from the previous article, now built directly into the folder structure.

That difference also helps answer a simple question. Do you actually need a plugin? If the workflow is just for you, a skill folder is enough. A plugin starts to pay off when you're copying folders between projects by hand, when each tool needs its own setup steps, when hooks are easy to forget to set up, and when team standards drift because everyone ends up running a slightly different version of the "same" workflow. A plugin replaces all of that with one package you can install, check, and release.

One Workflow, Two Packages, Three Hosts

We will build two standalone plugin folders for the same workflow. The first uses the Cursor plugin format. The second uses the Claude plugin format, and this is where the tooling landscape has improved a lot. Not long ago, getting a custom workflow into Copilot would have meant building an entire VS Code extension. That is no longer the case. VS Code now ships agent plugins thatauto-detect the Claude plugin format, by looking for the same file that Claude looks for,.claude-plugin/plugin.json. This means one Claude-format folder covers Claude Code and VS Code Copilot at the same time.One caveat before you rely on this. Agent plugins in VS Code are currently a preview feature, turned on through the chat.plugins.enabled setting, so the details here may change as it matures.

The two packages end up as siblings with nearly identical shapes:

tech-adoption-review-cursor/            tech-adoption-review-copilot/
├── .cursor-plugin/                     ├── .claude-plugin/
│   └── plugin.json                     │   └── plugin.json
├── skills/                             ├── skills/
├── hooks/                              ├── hooks/
│   └── hooks-cursor.json               │   └── hooks.json
└── scripts/                            └── scripts/
    └── run_validation_hook.py              └── run_validation_hook.py

Each folder is complete on its own, and each contains its own copy of the same skill core. Modern plugins can bundle more than this, for example specialized agents, rules, MCP configuration, but ours are intentionally simple and contain only the skill, a manifest, and a validation hook. That is enough to demonstrate every important concept.

The Manifest

The manifest is the plugin's entry point. It tells the host what the plugin is called, what version it is, and where its components live. Without it, the folder is just a set ofwhich leaves the host with no reliable way to identify the plugin or know which pieces belong to it. Here is the Claude-format manifest, stored at.claude-plugin/plugin.json:

{
  "name": "tech-adoption-review",
  "description": "Review the quality of a technology-adoption case (Deep Research report, RFC, ADR, adoption memo, vendor assessment). Checks whether the case is clear, evidence-backed, balanced, risk-aware, and decision-ready. Does not make the adoption decision.",
  "version": "2.2.1",
  "author": { "name": "Boris Delovski" },
  "license": "MIT",
  "keywords": ["technology-adoption", "architecture-review", "skills", "review"],
  "skills": "./skills/",
  "hooks": "./hooks/hooks.json"
}

Only a handful of fields carry real weight. The name identifies the plugin to the host, version makes releases meaningful, skills points at the local skills folder, and hooks points at the hook configuration. Notice that the description both explains what the plugin doesand restates the skill's boundary, so the boundary travels with the package everywhere it is installed. The remaining fields are not strictly required, but they are what you will typically encounter when inspecting other people's manifests.

The Cursor manifest is nearly identical, with three differences worth knowing. First, it lives in .cursor-plugin/ instead of .claude-plugin/. Second, it adds a human-readabledisplayName field alongside the machine-readable name.Third, it points to a Cursor-specific hook file, ./hooks/hooks-cursor.json. In other words, the manifests differ at exactly the layer they are supposed to differ at, which is the host adapter.

Article continues below

Hooks

Hooks are the part of the plugin that elevates the workflow from "instructions the agent usually follows" to "a process with a mechanical guarantee." A hook runs a deterministic command at a specific lifecycle moment, for example when a session starts, before a shell command executes, after a file is edited, or when the agent finishes responding. A hook has two parts: a configuration that tells the host when to run, and a command or script that does the actual work. Validation is our use case, but formatting generated files, logging, running tests, and halting a workflow when required conditions are not met are all natural fits.

Our hook fires onStop event , the moment the agent finishes its turn, and runs a wrapper script that locates the review report and passes it to the validator inside the skill folder. The Claude-format configuration looks like this:

{
  "description": "Validates the generated tech-adoption review report when the agent stops.",
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/run_validation_hook.py\"",
            "windows": "python \"${CLAUDE_PLUGIN_ROOT}/scripts/run_validation_hook.py\"",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

The ${CLAUDE_PLUGIN_ROOT} variable resolves paths relative to the installed plugin, so the package works no matter where it is installed. The windows field provides an alternative command for Windows hosts, and timeout caps the hook's runtime so a hung validator cannot stall the session. The Cursor version expresses the same idea in a flatter format, with a lowercase stop event and a ${CURSOR_PLUGIN_ROOT} variable. Other than that, the flow is identical.

Stop is of course just one of many events. Both hosts expose hooks around session lifecycle, individual tool calls, file edits, and prompt submission, and the full lists in the Cursor and Claude Code documentation are worth taking a look at once you start building your own custom plugins.

The Fixed-Filename Problem

Here is where the plugin forced a real design change that we had to implement. In the previous setup, validation was in-band, meaning either we ran the validator ourselves on a known path, or the agent ran it on the report it had just written. Either way, the caller knew where the report was.

A Stop hook is different. When the event fires, the host runs the wrapper as a separate process, outside the conversation. It has no access to the agent's context. It does not know what the agent wrote, where it wrote it, or whether it wrote anything at all, and the Stop-event payload does not include a file path. Simply put: the agent knows what it wrote, but the hook does not.

To fix this we make the skillpromise a filename. The skill core gains one instruction, added in three places. The workflow now says to save the final report as tech-adoption-review.md in the workspace root when file-writing tools are available, the output requirements make that filename part of the contract, and the final self-check has the agent verify it saved the file or state why it could not. That single instruction is the only difference between the skill core from the previous article and the one shipped in these plugins, and it is why the skill version bumps from 2.1.0 to 2.2.0. The wrapper checks a couple of override mechanisms first, looking for an environment variable for deterministic testing and a payload path in case hosts ever provide one, but the promised filename is the path that fires in practice. Without that instruction, the hook would silently never find anything to validate.

There is also one more thing we need to address here. The Stop hook fires onevery agent stop, and most stops have nothing to do with a review. So if the wrapper finds no report, it exits quietly. This is intentional, as a missing report means "stay out of the way," while a malformed report means "block."

And how does blocking actually work? In the Claude format, on both Claude Code and VS Code agent mode, exit code 2 blocks the agent from stopping, and whatever the hook wrote to stderr goes back to the model,which then gets a chance to fix the report before finishing. Any other non-zero exit code is merely a warning. Both hosts also add a stop_hook_active flag to the payload so the wrapper can avoid an endless stop-validate-stop loop. Two simple rules follow from these semantics: write the real failure reason to stderr, because that is what the model sees, and keep stdout empty, so the host can read the hook result cleanly.

Installing the Plugins Locally

Installing these plugins locally is very easy. For Cursor, copy the plugin folder into the local plugins directory under your user profile (.cursor/plugins/local/), restart Cursor, and confirm it appears under Settings → Plugins.

For Claude Code, point the CLI at the plugin root for the current session:

claude --plugin-dir ./tech-adoption-review-copilot

The folder you pass must be the plugin root, the one containing.claude-plugin/plugin.json.

For VS Code Copilot, enable thechat.plugins.enabled preview setting, register the same folder under chat.pluginLocations in your settings, and check the Extensions view for it.

In all three hosts, invoking the workflow is then just a matter of asking the agent to review a document using the tech-adoption-review skill.

Publishing Through a Marketplace

Local folders are fine for development, but the point of a plugin is that teammates install it by name. A marketplace makes that possible, and it is far less infrastructure than the word suggests. Amarketplace is just a Git repository with one extra catalog file. The repository holds the plugin folders under plugins/, and a marketplace.json file lists what is available. Here is how a typical marketplace.json file looks like:

{
  "name": "my-marketplace",
  "owner": { "name": "Your Name" },
  "plugins": [
    {
      "name": "tech-adoption-review",
      "description": "Reviews the quality of a technology-adoption case.",
      "source": "./plugins/tech-adoption-review-copilot"
    }
  ]
}

As you can see above,plugin.json describes one plugin, while marketplace.json lists many. For Claude Code, using the marketplace is two commands. First you need to register the repository, then you need to install the plugin from it:

/plugin marketplace add your-org/my-marketplace
/plugin install tech-adoption-review@my-marketplace

VS Code can register the same marketplace and browse its plugins from the Extensions view. Cursor's flow is dashboard-based instead. First, an admin imports the GitHub repository under Settings → Plugins. After, team members install from the marketplace panel with a click. Be aware that team marketplaces in Cursor require a Teams or Enterprise plan.

From Prompt To Plugin

With this article, the progression we started several articles ago is complete. A prompt captured what you say. A skill turned the workflow into portable, reviewable files with a boundary and a mechanical check. Finally, by creating a plugin we turned those files into a versioned package that installs by name, validates automatically through hooks, and distributes through what is ultimately just a Git repository.

Boris Delovski

Data Science Trainer

Boris Delovski

Boris is a data science trainer and consultant who is passionate about sharing his knowledge with others.

Before Edlitera, Boris applied his skills in several industries, including neuroimaging and metallurgy, using data science and deep learning to analyze images.