How Claude Code Plugins Work: Anatomy of plugin.json and the skills/commands/hooks Layout

hero

Building your own Claude Code plugin sounds intimidating until you realize the whole thing is just a directory with a JSON file and a handful of Markdown files. Once I understood the three-folder structure, everything clicked. This post is for developers who want to extend Claude Code with their own reusable behaviors and don't want to spend two hours guessing at folder names.

overall plugin structure flow


The Problem: Claude Code Extensions Have No Obvious Entry Point

When I first looked at Claude Code's plugin system, I expected something like VS Code — a package.json with a contributes block. What I found was leaner but less documented. There's no official scaffold command. There's no init that spits out a working template. You're staring at an empty directory wondering what goes where.

The first thing I tried was dropping a Markdown file directly into ~/.claude/ and hoping Claude would pick it up as a skill. It didn't. Then I tried naming a folder plugin/ (singular). Also nothing. The actual requirement is a specific subdirectory under ~/.claude/plugins/ with an exact filename — plugin.json — at the root.

Here's the error you get (silently, in the debug log) when the structure is wrong:

[plugin loader] skipping ~/.claude/plugins/my-thing — no plugin.json found

No UI feedback. Just silence. That cost me 30 minutes.

failed load vs successful load


The Fix: Minimal Working Plugin in Three Files

The smallest plugin that actually loads is two files:

~/.claude/plugins/my-plugin/
├── plugin.json
└── skills/
    └── main.md

plugin.json

{
  "name": "my-plugin",
  "description": "What this plugin does, one sentence",
  "author": "seunghyeon",
  "version": "0.1.0",
  "entrypoint": "skills/main.md"
}

Every field matters. Claude Code reads name for deduplication, description for surfacing the skill in context, and entrypoint to know which file to load first. Missing author doesn't break loading, but missing entrypoint does — you'll get another silent skip.

skills/main.md

This is a plain Markdown file with a system-style instruction block. Think of it as a persistent prompt fragment that Claude injects whenever the skill is invoked.

## my-plugin

When the user asks to [trigger phrase], do the following:

1. Step one
2. Step two
3. Return result in this format: ...

To verify it loaded, run:

claude --list-plugins

Expected output:

Loaded plugins:
  my-plugin (v0.1.0) — What this plugin does, one sentence

If your plugin name doesn't appear here, something in plugin.json is malformed. Run claude --debug and grep for your plugin name to find the exact parse error.

minimal plugin verified


Section: The Full Three-Folder Layout

Once the minimal version works, you can add commands/ and hooks/ for more control.

~/.claude/plugins/my-plugin/
├── plugin.json
├── skills/
│   ├── main.md          ← auto-invoked skill definition
│   └── helper.md        ← secondary skill, referenced by main
├── commands/
│   └── mycommand.md     ← maps to /mycommand in the Claude CLI
└── hooks/
    └── on-stop.md       ← fires on a specific Claude lifecycle event

skills/

Files here define behaviors Claude can invoke automatically based on context matching. You can have multiple skill files; they're all loaded, and Claude picks which one applies based on the trigger conditions you describe in the Markdown. Reference a secondary skill from main.md using a relative path hint in the front matter (if your version supports it) or just describe the delegation in prose — Claude handles the routing.

commands/

Each .md file in commands/ maps directly to a slash command. A file named refactor.md becomes /refactor in the CLI. The file contains the behavior description for what Claude should do when that command is invoked.

## /mycommand

When invoked, take the current file context and do X.
Respond with a summary in bullet format.

Test it with:

/mycommand

If nothing happens, confirm the file is named exactly as you want the command (no spaces — use hyphens).

hooks/

Hooks fire on lifecycle events. The filename maps to the event name:

Filename Fires when
on-stop.md Claude finishes a response
on-start.md Claude begins processing a turn
on-tool-call.md Any tool is about to be called
on-error.md An error occurs in the run loop

A hook file looks identical to a skill file — it's Markdown with instructions. The difference is invocation: hooks are event-driven, not triggered by user input.

## on-stop hook

After every response, check if any TODO comments were added to the edited files.
If found, append them to TASKS.md in the project root.

three folder event routing


Variations and Gotchas

Plugin name collisions. If two plugins share the same name in plugin.json, Claude Code loads the first one it finds alphabetically and silently drops the second. Prefix your plugin names: sh-my-plugin instead of my-plugin.

Entrypoint must be relative. The entrypoint field in plugin.json is relative to the plugin root, not to ~/.claude/. "entrypoint": "skills/main.md" is correct. "entrypoint": "~/.claude/plugins/my-plugin/skills/main.md" will fail.

Command name conflicts with built-ins. If you name a command file help.md, it won't override /help. Built-in commands take priority. Check claude --list-commands to see what's already taken before naming your files.

Hot reload doesn't exist (yet). As of the time I'm writing this, changes to plugin files don't take effect until you restart Claude Code. There's no watch mode. Keep a second terminal open with claude --debug so you can restart fast and see load errors immediately.

Mac vs Linux path. On macOS, ~/.claude/ resolves to /Users/yourname/.claude/. On Linux it's /home/yourname/.claude/. On both, the plugins/ subdirectory must exist before you create your plugin folder — Claude Code doesn't create it automatically on first run in all versions.

# Run this once to make sure the directory exists
mkdir -p ~/.claude/plugins

Docker. If you're running Claude Code inside a container, mount the plugins directory explicitly:

volumes:
  - ~/.claude/plugins:/root/.claude/plugins:ro

The :ro flag is fine — Claude Code only reads these files at startup.


Closing

The plugin system is simpler than it looks: one JSON file declares the plugin, and three folders (skills/, commands/, hooks/) cover every extension point Claude Code exposes. Start with plugin.json + skills/main.md, verify with --list-plugins, then add commands/ and hooks/ only when you need them.

Next up in this series: how to pass structured data between a hook and a skill using shared context, and how to version your plugins so updates don't silently break running sessions.


🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Build
💌 Subscribe: Follow on X or grab the RSS

댓글