Packaging as a plugin
This page covers Claude Code plugin packaging from the karpathy-wiki shape: minimal.claude-plugin/plugin.json, skills in skills/<name>/SKILL.md, optional sibling sub-directories, the symlink install pattern, and the ${CLAUDE_PLUGIN_ROOT} substitution gotcha.
Cross-platform packaging (Codex, Grok Build, OpenCode, Gemini CLI, Cursor) is summarized below and covered in the cross-platform pages. This page is Claude Code-specific.
The minimal plugin shape
A Claude Code plugin is a directory of components in standard locations. The manifest is optional: without it, Claude Code loads the components it finds and takes the plugin name from the marketplace entry or the directory name (Plugins reference, read 2026-09-29).Plugin manifest: .claude-plugin/plugin.json
A small manifest, following the shape of karpathy-wiki’s plugin.json at d8107e7:
name is the only required field. author is an object with a required name and optional email and url. A string author fails validation: Claude Code 2.1.284 then refuses to load the plugin, and Grok Build 1.0.44 installs it with no Skills, while Codex 0.159.0 accepts it (loader case study). version, description, repository, license and keywords are optional; claude plugin validate warns when version, description or author is missing, and --strict turns those warnings into failures.
The name is the namespace prefix for your plugin’s skills (Claude Code uses plugin-name:skill-name for namespacing). Pick a name that will not collide with other plugins.
The version follows semantic versioning. Note: a description-string change in your skills can break implicit triggering (the description is the activation contract); a description change is conceptually a major-version shift even if the body is unchanged. See Evolution.
Where skills live
Per Claude Code’s docs, and as karpathy-wiki’sskills/ does: plugin skills live at skills/<skill-name>/SKILL.md relative to the plugin root.
Optional sibling sub-directories under each skill:
scripts/. Helper scripts the SKILL.md invokes. The agent-skills spec convention (Layer 1).references/. Heavy reference docs loaded on demand. One level deep from SKILL.md; never nested. See Line budget.assets/. Data files, fixtures, images. Loaded as needed.
external_directory permission and Gemini CLI’s activation consent are two such gates (see the cross-platform pages).
Install paths
Claude Code recognizes plugins via:- Plugin marketplace. Add one with
claude plugin marketplace add <url-path-or-github-repo>, then install withclaude plugin install <name>or<name>@<marketplace>(Claude Code 2.1.284--help, 2026-09-29). - Local clone + symlink. The pattern karpathy-wiki uses for development:
git clonethe plugin repo, thenln -s <plugin-skill-dir> ~/.claude/skills/<name>to make the skill available without going through the plugin system. - Direct copy.
cp -r <plugin-skill-dir> ~/.claude/skills/<name>. Less common; loses the link to upstream.
enterprise > personal > project; plugin skills load alongside all of themA skill at
~/.claude/skills/wiki/ (personal) wins over .claude/skills/wiki/ (project). A plugin skill is namespaced as /plugin-name:wiki, so it never collides: a personal wiki and a plugin’s wiki both load. During symlink development with the plugin also installed, the model sees both copies.
The ${CLAUDE_PLUGIN_ROOT} gotcha
Source: REVIEWER G3; the Plugins reference: where each variable resolves, read 2026-09-29.
Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} with the plugin’s installed path in plugin.json and hooks.json fields, MCP and LSP server configs, and the Markdown body of a skill, command or agent that the plugin provides. Hook, MCP and LSP processes also get it as an environment variable.
The variable is NOT in the environment of commands Claude runs through the Bash tool, and a skill loaded from outside the plugin gets no substitution. A SKILL.md that says:
No such file or directory when the same skill loads as a personal skill (the symlink-install case) or on Codex, OpenCode or Gemini CLI, which do not substitute it. Grok Build substitutes it in plugin skills (skill.rs at 97f190f). The literal string ${CLAUDE_PLUGIN_ROOT} reaches Bash, expands to the empty string (Bash sees an unset variable), and the command becomes bash /scripts/foo.sh, which does not exist.
The failure is silent during plugin install; it surfaces only when someone installs the skill another way and invokes it.
Three workarounds:
- Use
${CLAUDE_SKILL_DIR}instead. Claude Code substitutes it with the skill’s own directory in the body of any skill, plugin or not (Skills docs). Grok Build substitutes it too (skill.rsat97f190f); Codex, OpenCode and Gemini CLI leave it literal, so pair it with workaround 3 for those hosts. - Use a relative path.
bash scripts/foo.shworks if the skill’s working directory is set to the skill’s base directory. Some skills docd "$(dirname "$0")"first; some rely on Claude Code settingpwdcorrectly. - Compute the path explicitly. From a SKILL.md prose preamble: “All script paths below are relative to this skill’s base directory (shown at the top of the skill as
Base directory for this skill: ...).cdinto that directory before invoking any script, or prefix each script with the absolute base path.” This is karpathy-wiki’s chosen workaround.
Cross-platform manifests (brief)
Other harnesses use different manifest shapes:- Codex:
.codex-plugin/plugin.json, listed in a marketplace’s.agents/plugins/marketplace.json; installed withcodex plugin marketplace addandcodex plugin add(Codex 0.159.0). The per-skillagents/openai.yamlsidecar is separate. See Codex. - Grok Build: an optional
plugin.jsonnext toskills/,hooks/hooks.jsonand.mcp.json; marketplaces index plugins in.grok-plugin/marketplace.jsonand Grok also accepts.claude-plugin/. Installed withgrok plugin install <name> --trust(Grok 1.0.44). See Other harnesses. - OpenCode: a JavaScript or TypeScript module in
.opencode/plugins/or~/.config/opencode/plugins/, or an npm package listed underplugininopencode.json. (OpenCode 1.18.33). See Other harnesses. - Gemini CLI:
~/.gemini/extensions/<name>/gemini-extension.json; an extension bundlesGEMINI.md,commands/*.toml,skills/,hooks/hooks.jsonandagents/(v0.61.0). See Gemini CLI. - Cursor (last checked 2026-04):
.cursor-plugin/plugin.jsondeclares every artifact path explicitly.
Sources
- Claude Code docs, read 2026-09-29: Plugins reference (manifest fields, variable substitution), Skills (name resolution,
${CLAUDE_SKILL_DIR}); Claude Code 2.1.284claude plugin --help. REVIEWERG3 (${CLAUDE_PLUGIN_ROOT}gotcha; the three workarounds).- karpathy-wiki
plugin.jsonatd8107e7(the manifest shape used as exemplar). - Cross-platform manifests: sources on each cross-platform page, checked 2026-09-29 except Cursor (
LANDSCAPE1.4, 2026-04).