Part 3 · Skills & Plugins

Mastering Claude Code · ~7 min

Building Plugins

One command scaffolds a plugin in your skills directory; one manifest key arms a watcher that runs itself every session. No marketplace round-trip.

Why this, for you: you have a skill that works, but it needs a hook beside it, an agent behind it, and a background watcher that fires without anyone remembering to start it. A loose SKILL.md can't carry that. The plugin is the bundle — and since v2.1.157 you no longer need a marketplace to run one locally.

A plugin is distribution packaging, not a separate logic layer. So the question is never "should this be a plugin?" in the abstract — it's "does this bundle now carry more than one skill, or need to ship, or need a watcher armed for me?" When the answer is yes, three moves get you from nothing to a running plugin.

1 Scaffold in one command

Claude Code 2.1.157 (shipped 2026-05-29) auto-loads any folder under a skills directory that contains a .claude-plugin/plugin.json manifest — no marketplace, no install step. claude plugin init writes that layout for you:

# from the repo root — alias: claude plugin new claude plugin init repo-helper --with hooks \ --description "Repo conventions and lint hook"

That produces a .claude/skills/repo-helper/ tree: the manifest (name, description, and author pulled from git config user.name/user.email), a starter skill under skills/, and — because of --with hooks — a hooks/hooks.json with a sample handler ready to edit. On the next session it loads as repo-helper@skills-dir, discovered in place rather than copied into the plugin cache.

A skills tree now holds three coexisting shapes, told apart by the manifest: a bare foo/SKILL.md is a plain skill (/foo); foo/.claude-plugin/plugin.json is a plugin (foo@skills-dir); and foo/skills/bar/SKILL.md is a skill packaged inside that plugin, invoked as /foo:bar.

The manifest charges a namespacing tax

For a single skill in a single repo, loose .claude/skills/<name>/SKILL.md stays the cheaper default — it's shorter to invoke (/deploy, not /my-plugin:deploy) and it walks up to the repo root from any subdirectory. A project-scope @skills-dir plugin does not walk up: launch from apps/api/ and a plugin at the root is silently missed. Launch from the root, or run /reload-plugins after you cd.

2 Scope decides what's allowed to run

Where you scaffold sets who sees the plugin and what its components may do. Personal scope carries no restrictions; project scope is treated as repo-supplied content and runs under extra guards.

Skills directoryScopeWhat's restricted
~/.claude/skills/personalNothing — loads in every project, it's yours
<cwd>/.claude/skills/projectLoads only after the workspace trust dialog

At project scope the bundled pieces inherit the same suspicion as any repo content: MCP servers face the per-server approval a project .mcp.json gets, LSP servers start only after you trust the workspace, and background monitors do not load at all. That last one is the trap of the next section — if your plugin's whole point is a watcher, project scope ships dead code.

A SKILL.md edit takes effect immediately, but changes to hooks/, .mcp.json, agents/, and output-styles/ need /reload-plugins or a restart. Iterating on the skill is hot; iterating on the wiring is not.

3 Background monitors: supervision the harness arms for you

v2.1.105 added a top-level monitors manifest key. Each entry is one persistent background process whose every stdout line becomes a notification to Claude — and the harness arms it automatically at session start or on skill invoke. The consumer writes no boilerplate.

# plugin.json points at monitors/monitors.json "monitors": "./monitors/monitors.json" # monitors.json — name, command, description are required [{ "name": "protected-path-writes", "command": "${CLAUDE_PLUGIN_ROOT}/bin/watch-protected.sh", "description": "Writes to /etc/ or *.pem files" }]

This is the third declarative supervision primitive, distinct from the other two. The optional when field is "always" by default, or "on-skill-invoke:<skill-name>" to defer the spawn until a named skill first runs.

PrimitiveTriggerCan block?
HookTool-call boundary (PreToolUse, PostToolUse…)Yes (PreToolUse)
Monitor toolClaude calls it imperatively in-sessionNo
Plugin monitorAuto-armed at session start / on skill invokeNo
The plugin monitor removes the "did the agent remember to arm the watcher?" failure mode by moving the trigger out of model reasoning and into the harness. The author ships the watcher once; every consumer gets it armed at session start.

What auto-arm quietly doesn't cover

Monitors are session-scoped — torn down at session end, so they're no substitute for systemd or cron when enforcement must survive a reboot. They run unsandboxed at hook trust level — vet a marketplace monitor like any shell script. Only stdout is forwarded: a watcher that errors to stderr or exits non-zero looks healthy. Chatty monitors are auto-killed, so pipe through grep --line-buffered. And Bedrock, Vertex AI, and Foundry skip monitors entirely.

↪ Your win: from loose skill to running bundle

Retrieval practice — recall, don't peek

Question 1What turns a folder under a skills directory into an auto-loaded plugin?

Question 2A skill packaged inside the plugin foo as foo/skills/bar/SKILL.md is invoked as…

Question 3At project scope, what does a plugin's background monitor do?

Question 4The chief value of the monitors key over a Monitor-tool call is that it…

Question 5 · spaced recall from an earlier lessonFrom the hooks lesson: behavior that must happen 100% of the time belongs in a hook because hooks are…

Ask me anything. Want help deciding whether a working skill of yours has actually earned a plugin wrapper, or how to keep a chatty monitor from getting auto-killed? Next: Parallel Worktrees & Monorepos — running isolated work in batch worktrees so parallel agents never collide.
✎ Feedback