AI how-to notes · 2026-09-17 · last run 2026-09-17 · 한국어 원문
Build and load a local Claude Code plugin — claude plugin validate and --plugin-dir measured on Windows
The short version
Following the official quickstart, two files — plugin.json and SKILL.md — were enough for claude plugin validate to print ✔ Validation passed. For a plugin loaded with --plugin-dir, claude --plugin-dir <folder> plugin list and plugin details show whether it loaded, without opening a chat session. The mistake of putting skills/ inside .claude-plugin/ passed validate, but details exposed it as Skills (0).
Who this is for
You want to try a plugin on your own machine before publishing it to a marketplace. You edited plugin.json and the plugin no longer shows up. Or you want to load a plugin for one invocation only, without installing it into your user settings.
What we tested on
| Item | Value |
|---|---|
| OS | Windows 11 Home (10.0.26200) |
| Shell | Git Bash |
| Claude Code | 2.1.274 (Claude Code) |
| Test folder | C:\Users\User\AppData\Local\Temp\longtail_B2\plugins |
| Reference docs | code.claude.com/docs/en/plugins , code.claude.com/docs/en/plugins-reference , code.claude.com/docs/en/plugin-marketplaces (all checked 2026-09-17) |
Choosing what to use from the help text
claude plugin --help lists validate, details, list, init, install, and others. Here is what we used and what we left alone.
validate [options] <path> Validate a plugin or marketplace
manifest, or the skills, agents, and
commands in a directory
details [options] <name> Show a plugin's component inventory and
projected token cost
list [options] List installed plugins
init|new [options] <name> Scaffold a new plugin at
~/.claude/skills/<name>/ (auto-loads next
session as <name>@skills-dir)Per the help text, init creates files in your user folder (~/.claude/skills/). We skipped init and install and wrote the files by hand in a temp folder. For loading, we used this option from claude --help.
--plugin-dir <path> Load a plugin from a directory or .zip
for this session only; a folder of
plugins loads each child (repeatable:
--plugin-dir A --plugin-dir B.zip)The options in claude plugin validate --help are --json (Output the validation report as JSON (same exit codes)) and --strict (Treat warnings as errors (exit 1)).
Step 1 — a minimal plugin
We copied the quickstart example from the plugins doc verbatim. The layout:
my-first-plugin/
├── .claude-plugin/
│ └── plugin.json
└── skills/
└── hello/
└── SKILL.md{
"name": "my-first-plugin",
"description": "A greeting plugin to learn the basics",
"version": "1.0.0",
"author": {
"name": "Your Name"
}
}---
description: Greet the user with a friendly message
disable-model-invocation: true
---
Greet the user warmly and ask how you can help them today.The plugins-reference doc states "If you include a manifest, name is the only required field." It explains that the skill folder name hello becomes the skill name, prefixed by the plugin name to give /my-first-plugin:hello.
Step 2 — claude plugin validate
$ claude plugin validate ./my-first-plugin
Validating plugin manifest: C:\Users\User\AppData\Local\Temp\longtail_B2\plugins\my-first-plugin\.claude-plugin\plugin.json
✔ Validation passedExit code was 0, and adding --strict changed nothing. With --json you get this:
{
"success": true,
"strict": false,
"target": "C:\\Users\\User\\AppData\\Local\\Temp\\longtail_B2\\plugins\\my-first-plugin\\.claude-plugin\\plugin.json",
"manifest": {
"file": "C:\\Users\\User\\AppData\\Local\\Temp\\longtail_B2\\plugins\\my-first-plugin\\.claude-plugin\\plugin.json",
"type": "plugin",
"errors": [],
"warnings": [],
"notes": []
},
"contents": []
}Step 3 — validating deliberately broken plugins
We built a few more plugins for validation. Most change exactly one thing from the working plugin; noname-plugin has only "description": "no name field" in its plugin.json.
| Folder | What changed | validate result | Exit code |
|---|---|---|---|
| typo-plugin | description misspelled as descripton | ⚠ Found 2 warnings → ✔ Validation passed with warnings | 0 |
typo-plugin + --strict | same | ✘ Validation failed (--strict treats warnings as errors) | 1 |
| badtype-plugin | "keywords": "greeting" (a string where an array belongs) | ❯ keywords: Invalid input → ✘ Validation failed | 1 |
| noname-plugin | only description, no name | ❯ name: Invalid input → ✘ Validation failed | 1 |
| wrongdir-plugin | skills/ placed inside .claude-plugin/ | ✔ Validation passed | 0 |
| badskill-plugin (first pass) | description: [unclosed in SKILL.md | ✔ Validation passed | 0 |
| badskill-plugin (second pass) | SKILL.md frontmatter broken further, plus agents/broken.md | ✘ Validation failed | 1 |
| no-such-dir | a path that does not exist | ❯ file: File not found: ... → ✘ Validation failed | 1 |
The typo warnings read as follows.
❯ descripton: Unknown field 'descripton' — did you mean 'description'? Claude Code ignores unrecognized fields at load time, so this field has no effect.
❯ description: No description provided. Adding a description helps users understand what your plugin doesFor badskill-plugin's second pass, the SKILL.md frontmatter had description: "unterminated with the quote left open, plus a misindented line and a tab line. The result:
Validating skill: C:\...\badskill-plugin\skills\hello\SKILL.md
✘ Found 1 error:
❯ frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected EOF. At runtime this skill loads with empty metadata (all frontmatter fields silently dropped).
Validating agent: C:\...\badskill-plugin\agents\broken.md
⚠ Found 1 warning:
❯ description: No description in frontmatter. A description helps users and Claude understand when to use this agent.Three things stood out.
- wrongdir-plugin passed. The docs flag that layout as a "Common mistake", yet
validatealone does not catch it. Step 4 below catches it withdetails. - The first pass's
description: [unclosedwas not flagged as an error. Nor wasname: [oopsinagents/broken.md, which produced only a warning about a missing description rather than a parse error. We could not determine why an unclosed bracket passes. - A nonexistent path exited 1. The plugins-reference doc says "2 when the validation run itself fails, such as when the path you pass is unreadable", but here the missing path was treated as a validation failure (1), not 2.
The docs (plugin-marketplaces) also describe validating a skills folder on its own. claude plugin validate ./badskill-plugin/skills printed Validating components in: ... followed by the same frontmatter error, and exited 1.
Step 4 — did --plugin-dir actually load it?
The docs' test procedure is to open a chat session with claude --plugin-dir ./my-first-plugin. We skipped the chat session and passed --plugin-dir alongside plugin list and plugin details instead.
claude --plugin-dir ./my-first-plugin plugin list | grep -n -A4 'my-first-plugin'300: ❯ my-first-plugin@inline
301- Version: 1.0.0
302: Path: C:\Users\User\AppData\Local\Temp\longtail_B2\plugins\my-first-plugin
303- Status: ✔ loadedWith --json it reported "id": "my-first-plugin@inline", "scope": "session", "enabled": true. Running claude plugin list --json without --plugin-dir returned zero hits for my-first-plugin.
$ claude --plugin-dir ./my-first-plugin plugin details my-first-plugin
my-first-plugin 1.0.0
Description: A greeting plugin to learn the basics
Source: my-first-plugin@inline
Component inventory
Skills (1) hello
Agents (0)
Hooks (0)
MCP servers (0)
LSP servers (0)
Projected token cost
Always-on: ~27 tok added to every sessionRunning claude plugin details my-first-plugin without --plugin-dir gave `Plugin "my-first-plugin" not found. Run claude plugin list to see installed plugins, or pass --plugin-dir <path> to load one from disk.` and exit code 1.
We put the broken plugins through the same treatment.
| Folder | plugin list (with --plugin-dir) | plugin details |
|---|---|---|
| wrongdir-plugin | (not checked individually) | Skills (0), Always-on: ~0 tok |
| badtype-plugin | ✘ Failed to load plugin: Plugin badtype-plugin has an invalid manifest file at ... Validation errors: keywords: Invalid input | not found, exit code 1 |
| badskill-plugin (second pass) | (from the whole-folder load) ✔ loaded | Skills (1) hello, Agents (1) broken |
wrongdir-plugin passed validate and loaded, but carried zero skills. badskill-plugin kept its skill in the list despite the frontmatter parse error. Going by the wording of the validate error, that skill loads with its frontmatter values dropped.
Step 5 — a whole folder of plugins at once
The help text says that pointing --plugin-dir at a parent folder of plugins loads each child. The plugins doc notes this feature requires v2.1.265 or later.
claude --plugin-dir . plugin list | grep -n 'inline'300: ❯ badskill-plugin@inline
305: ❯ my-first-plugin@inline
310: ❯ typo-plugin@inline
315: ❯ wrongdir-plugin@inline
320: ❯ inline[1]: ✘ Failed to load plugin: Plugin badtype-plugin has an invalid manifest file at ...\badtype-plugin\.claude-plugin\plugin.json. Validation errors: keywords: Invalid input
322: ❯ inline[3]: ✘ Failed to load plugin: Plugin noname-plugin has an invalid manifest file at ...\noname-plugin\.claude-plugin\plugin.json. Validation errors: name: Invalid inputThe two that were errors (exit code 1) under validate failed to load, while typo-plugin, which had only warnings, loaded fine. That matches the docs' "A plugin with only unrecognized-field warnings still passes validation and loads at runtime."
Results
| Check | Command | Result |
|---|---|---|
| Valid plugin | claude plugin validate ./my-first-plugin | ✔ Validation passed, exit code 0 |
| Manifest with errors | validate (keywords type, missing name) | ✘ Validation failed, exit code 1 |
| Manifest with warnings only | validate / validate --strict | 0 / 1 |
Layout mistake (.claude-plugin/skills) | validate → details | passes → Skills (0) |
| Load check | claude --plugin-dir <folder> plugin list | @inline, Status: ✔ loaded |
| Load failure check | same | ✘ Failed to load plugin: ... |
| Traces in user settings | string counts in ~/.claude.json, ~/.claude/settings.json, installed_plugins.json | 0 everywhere |
What didn't work
Our first attempt at making validate catch a frontmatter error (description: [unclosed) was not caught at all. Only the second file, with an unterminated quote, produced YAML frontmatter failed to parse.
What we did not check
- Actually invoking
/my-first-plugin:helloin a chat session,/reload-plugins, or the Errors tab of the/pluginscreen. We judged--plugin-dirloading solely fromplugin listanddetails. - Passing a
.zipfile to--plugin-dir, and--plugin-url. - Creating with
claude plugin init, installing withinstall, ormarketplacemanagement (we only read--helpforinit). - How plugins with hooks, MCP servers, or agents behave.
- Cases that produce exit code 2 (an unreadable path, and so on).
- How
plugin detailscomputes its token numbers.