Claude Code has quietly become the standard AI coding agent in many engineering teams, and in 2026 the most effective teams are no longer configuring it one developer at a time. They package their slash commands, subagents, skills, hooks, and MCP servers into plugins, publish them to a plugin marketplace hosted in a plain Git repository, and roll them out to everyone with a single settings entry.
In this tutorial you will build a production-quality plugin called team-toolkit from scratch, test it locally, publish it to GitHub as a marketplace, and enable it automatically for an entire team. By the end you will understand every file in the plugin format and know exactly how the pieces compose.
Prerequisites
Before starting, ensure you have:
- Claude Code installed and authenticated (run
claude --versionto check; any 2.x release supports plugins) - Node.js 20+ installed
- A GitHub account (or any Git host your team can clone from)
- Basic familiarity with Claude Code concepts such as slash commands and
CLAUDE.md
No prior plugin experience is required.
What You'll Build
You will create a plugin named team-toolkit that bundles five capabilities:
- A
/team-toolkit:changelogslash command that drafts a changelog entry from recent commits - A
code-auditorsubagent that reviews code for your team's conventions - A
conventional-commitsskill that Claude loads automatically when writing commit messages - A hook that runs your formatter after every file edit
- A bundled MCP server configuration for filesystem access to a shared docs folder
Then you will create a second repository that acts as a marketplace, so anyone on your team can install the plugin with one command.
Step 1: Understand the Plugin Anatomy
A Claude Code plugin is just a directory with a manifest. Every component is optional — a plugin can ship only a hook, only commands, or all of them together. The full layout looks like this:
team-toolkit/
├── .claude-plugin/
│ └── plugin.json # Required manifest
├── commands/ # Slash commands (Markdown files)
│ └── changelog.md
├── agents/ # Subagents (Markdown files)
│ └── code-auditor.md
├── skills/ # Agent skills (folders with SKILL.md)
│ └── conventional-commits/
│ └── SKILL.md
├── hooks/
│ └── hooks.json # Event handlers
├── scripts/ # Helper scripts used by hooks
│ └── format.sh
└── .mcp.json # Bundled MCP serversTwo rules matter more than anything else:
- The manifest lives at
.claude-plugin/plugin.json— inside the hidden directory, not at the repository root. - The component directories (
commands/,agents/,skills/,hooks/) live at the plugin root, not inside.claude-plugin/.
Mixing these two up is the single most common reason a plugin loads but its commands never appear.
Step 2: Scaffold the Plugin and Manifest
Create the directory structure and the manifest:
mkdir -p team-toolkit/.claude-plugin
mkdir -p team-toolkit/commands team-toolkit/agents
mkdir -p team-toolkit/skills/conventional-commits
mkdir -p team-toolkit/hooks team-toolkit/scripts
cd team-toolkitNow create .claude-plugin/plugin.json:
{
"name": "team-toolkit",
"description": "Noqta engineering conventions: changelog command, code auditor, commit skill, format-on-edit hook",
"version": "1.0.0",
"author": {
"name": "Noqta Engineering",
"email": "dev@noqta.tn"
},
"keywords": ["conventions", "review", "changelog"]
}Only name is strictly required, but description and version show up in the plugin manager UI, so treat them as mandatory in practice. The name must be kebab-case with no spaces — it becomes the namespace prefix for every command the plugin ships.
Step 3: Add a Slash Command
Plugin slash commands are Markdown files in commands/. The filename becomes the command name, namespaced by the plugin: commands/changelog.md becomes /team-toolkit:changelog. Namespacing means your plugin's commands never collide with another plugin's.
Create commands/changelog.md:
---
description: Draft a changelog entry from recent commits
argument-hint: [version] [since-tag]
allowed-tools: Bash(git log:*), Bash(git diff:*), Read
---
Draft a changelog entry for version $1.
1. Run `git log --oneline $2..HEAD` to list commits since tag $2.
If no tag argument was given, use the most recent tag.
2. Group the commits into Added, Changed, Fixed, and Removed sections
following the Keep a Changelog format.
3. Rewrite each line as a user-facing sentence — drop internal refactor
noise unless it affects behavior.
4. Output the entry as Markdown ready to paste into CHANGELOG.md.
Do not modify any files.Three details worth noting:
argument-hintis what users see when autocompleting the command.$1and$2receive positional arguments;$ARGUMENTSwould receive the whole argument string.allowed-toolspre-approves specific tool patterns so the command runs without permission prompts — scope it as tightly as you can.
Step 4: Add a Subagent
Subagents are specialized assistants with their own system prompt, tool restrictions, and context window. Claude delegates to them automatically when a task matches their description, or you can invoke them explicitly.
Create agents/code-auditor.md:
---
name: code-auditor
description: Reviews code changes against Noqta engineering conventions. Use proactively after significant edits or before committing.
tools: Read, Grep, Glob, Bash
model: inherit
---
You are a strict but pragmatic code auditor for a TypeScript/Next.js team.
When invoked:
1. Run `git diff HEAD` to see pending changes.
2. Check every changed file against these conventions:
- No console.log in production code (a logger must be used)
- No empty catch blocks
- Files stay under 300 lines
- Repeated logic is extracted into shared utilities
3. Report findings ordered by severity. For each finding include
the file path, line number, and a concrete suggested fix.
4. If everything passes, say so explicitly and list what you checked.
Never modify files yourself. You are read-only by convention.The description field is what Claude reads when deciding whether to delegate — phrases like "use proactively" genuinely influence delegation behavior. Setting model: inherit keeps the subagent on the same model as the main conversation; you can also pin a specific model like haiku for cheap mechanical checks.
Step 5: Add a Skill
Skills differ from commands in one fundamental way: commands are invoked by the user, skills are loaded by Claude itself when the task matches. A skill is a folder containing a SKILL.md with frontmatter that tells Claude when to load it.
Create skills/conventional-commits/SKILL.md:
---
name: conventional-commits
description: Format git commit messages using the team's conventional commit standard. Use whenever writing, amending, or reviewing a commit message.
---
# Conventional Commits — Team Standard
## Format
type(scope): subject
body (optional)
footer (optional)
## Types
feat, fix, docs, style, refactor, test, chore
## Rules
1. Subject in imperative mood, lowercase, no trailing period
2. Scope is the affected module: auth, api, ui, content, build
3. One logical change per commit — never batch unrelated edits
4. Reference issues in the footer: "Refs #123" or "Closes #123"
5. Body explains the why for any non-obvious change
## Examples
feat(auth): add password reset flow
fix(api): handle null values in user profile endpoint
The endpoint returned 500 when optional fields were null.
Added null checks with sensible defaults.
Closes #456Because the skill ships in the plugin, every developer who installs team-toolkit gets identical commit conventions applied automatically — no more copy-pasting guidelines into each project's CLAUDE.md.
Step 6: Add a Hook
Hooks run shell commands at lifecycle events — deterministic automation that does not depend on the model deciding to do something. We will format files after every edit.
Create hooks/hooks.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
}
]
}
]
}
}And the script at scripts/format.sh:
#!/usr/bin/env bash
# Reads the hook payload from stdin, formats the edited file if supported.
set -euo pipefail
payload=$(cat)
file_path=$(echo "$payload" | jq -r '.tool_input.file_path // empty')
if [[ -z "$file_path" ]]; then
exit 0
fi
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
if command -v prettier >/dev/null 2>&1; then
prettier --write "$file_path" >/dev/null 2>&1 || true
fi
;;
esac
exit 0Make it executable:
chmod +x scripts/format.shThe critical detail is ${CLAUDE_PLUGIN_ROOT} — an environment variable that resolves to the plugin's installed location at runtime. Never hardcode paths in plugin hooks; the plugin will be installed somewhere you do not control on each user's machine. Useful events besides PostToolUse include PreToolUse (block actions before they run), UserPromptSubmit (inject context on every prompt), SessionStart, and Stop.
Step 7: Bundle an MCP Server
Plugins can ship MCP server configurations that activate when the plugin is enabled. Create .mcp.json at the plugin root:
{
"mcpServers": {
"team-docs": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"${CLAUDE_PLUGIN_ROOT}/docs"
]
}
}
}This example exposes a docs/ folder shipped inside the plugin itself — handy for architecture decision records or API references the whole team should query. In real deployments you will more often point at an internal MCP server (a database gateway, an issue tracker, an observability API). The same rule applies: use ${CLAUDE_PLUGIN_ROOT} for anything path-relative, and environment variables for secrets — never hardcode credentials in .mcp.json.
Create a placeholder so the folder exists:
mkdir -p docs
echo "# Team Engineering Docs" > docs/README.mdStep 8: Test the Plugin Locally
Before publishing anything, validate the structure. Claude Code ships a validator:
claude plugin validate .Fix any manifest or schema errors it reports. Then test the real behavior using a local marketplace. The cleanest pattern is to keep plugins inside a marketplace repo from day one, so set that up now:
cd ..
mkdir -p noqta-claude-plugins/.claude-plugin
mv team-toolkit noqta-claude-plugins/plugins/team-toolkitWait to create the marketplace manifest — that is Step 9. For a quick local test you can add the marketplace directory directly:
claude
# then inside the session:
/plugin marketplace add ./noqta-claude-plugins
/plugin install team-toolkit@noqta-claude-pluginsRestart the session when prompted, then verify each component:
- Type
/team-toolkit:changelog 1.2.0— the command should appear in autocomplete and run. - Ask "review my pending changes for convention violations" — Claude should delegate to
code-auditor. - Ask Claude to commit something — the
conventional-commitsskill should shape the message. - Edit any TypeScript file — Prettier should run automatically via the hook.
- Run
/mcp— theteam-docsserver should be listed and connected.
If a component does not appear, run claude --debug and watch the plugin loading output at startup; it names each manifest it parses and any errors.
Step 9: Create the Marketplace and Publish
A marketplace is any Git repository containing .claude-plugin/marketplace.json. Create noqta-claude-plugins/.claude-plugin/marketplace.json:
{
"name": "noqta-claude-plugins",
"owner": {
"name": "Noqta Engineering",
"email": "dev@noqta.tn"
},
"metadata": {
"description": "Internal Claude Code plugins for the Noqta team",
"version": "1.0.0"
},
"plugins": [
{
"name": "team-toolkit",
"source": "./plugins/team-toolkit",
"description": "Team conventions: changelog command, code auditor, commit skill, format-on-edit hook",
"version": "1.0.0",
"category": "productivity"
}
]
}The source field accepts a relative path (plugin lives in this repo), a GitHub reference, or a Git URL — so a marketplace can also aggregate plugins from other repositories. Validate and push:
cd noqta-claude-plugins
claude plugin validate .
git init && git add -A
git commit -m "feat: add team-toolkit plugin and marketplace manifest"
gh repo create noqta/claude-plugins --private --source . --pushAnyone with access can now install it:
/plugin marketplace add noqta/claude-plugins
/plugin install team-toolkit@noqta-claude-pluginsFor updates: bump version in both plugin.json and the marketplace entry, push, and users pull changes with /plugin marketplace update noqta-claude-plugins.
Step 10: Roll It Out to the Whole Team
The real power move is automatic rollout. In any repository, commit a .claude/settings.json that declares the marketplace and enables the plugin:
{
"extraKnownMarketplaces": {
"noqta-claude-plugins": {
"source": {
"source": "github",
"repo": "noqta/claude-plugins"
}
}
},
"enabledPlugins": {
"team-toolkit@noqta-claude-plugins": true
}
}Now every developer who opens that repository with Claude Code gets prompted once to trust the marketplace, and the plugin installs and enables itself. New teammate onboarding goes from a wiki page of setup steps to git clone.
Testing Your Implementation
Run through this checklist to confirm everything works end to end:
claude plugin validate .passes in both the plugin and marketplace repos/pluginshowsteam-toolkitas installed and enabled/team-toolkit:changelog 1.0.0produces a grouped changelog draft- The
code-auditorsubagent appears in/agents - Editing a
.tsfile triggers Prettier (check the file's formatting changes) /mcpliststeam-docsas connected- A fresh clone of a repo with the
.claude/settings.jsonfrom Step 10 prompts to install the plugin
Troubleshooting
Commands don't appear after install. Verify the directory layout: plugin.json inside .claude-plugin/, but commands/ at the plugin root. Restart Claude Code after installing — components register at session start.
Hook never fires. Check that format.sh is executable (chmod +x) and that you used ${CLAUDE_PLUGIN_ROOT} rather than a relative path. Run claude --debug to see hook registration and execution logs.
MCP server fails to connect. Run /mcp to see the error. The most common cause is a command that is not on PATH in the environment where Claude Code runs — prefer npx -y package-name over globally installed binaries.
Marketplace add fails with a manifest error. The file must be at .claude-plugin/marketplace.json exactly, and every source path must exist relative to the repository root. claude plugin validate . catches both.
Plugin works for you but not teammates. Almost always a hardcoded path or a missing dependency (like jq or prettier) on their machine. Hooks should degrade gracefully — note how format.sh exits 0 when Prettier is absent.
Next Steps
- Add a
/team-toolkit:releasecommand that chains the changelog draft with a version bump and tag - Ship your deployment runbook as a second skill so incident response is loaded on demand
- Explore our guide on writing effective SKILL.md files to sharpen skill descriptions
- Build a custom MCP server in TypeScript and bundle it in the plugin
- Compare with the Claude Agent SDK when you need programmatic agents rather than interactive tooling
Conclusion
You built a complete Claude Code plugin — slash command, subagent, skill, hook, and bundled MCP server — validated it, published it through a Git-hosted marketplace, and wired up automatic team rollout via repository settings. The plugin format's strength is that each piece is plain Markdown or JSON in a versioned repository: your team's AI tooling now evolves through pull requests, code review, and releases like any other software. Start small with one command and one hook, and grow the toolkit as conventions solidify.