Build a Plugin Skill from Scratch
A skill is a self-contained capability you add to Claude Code through a plugin. It has a trigger (usually a slash command), a definition (what it does and how), and optional hooks (code that runs before or after tool calls). This tutorial walks through building one from scratch.
We will build a skill called changelog that generates a changelog entry from recent git commits.
Step 1: Project Setup
Skills live inside a plugin directory. If you already have a plugin, add the skill there. Otherwise, scaffold a new plugin:
my-plugin/
PLUGIN.md
skills/
changelog/
SKILL.md
hooks/
changelog-hook.js
The PLUGIN.md file declares the plugin metadata. The skills/ directory holds one folder per skill, each containing a SKILL.md. Hooks are optional JavaScript files that run at specific lifecycle points.
Step 2: Define the Skill
The SKILL.md file is the skill’s contract. It uses frontmatter for metadata and markdown for the instruction body.
---
name: changelog
description: Generate a changelog entry from recent git commits
triggers:
- /changelog
- /cl
args:
- name: since
description: Start date or tag (default: last tag)
required: false
- name: format
description: Output format (markdown | json)
default: markdown
---
## Instructions
1. Run `git log` to fetch commits since the specified
start point (default: most recent tag).
2. Group commits by type using conventional commit
prefixes: feat, fix, refactor, docs, chore.
3. For each group, list commits with their short hash
and message.
4. If format is "json", output a JSON object with
groups as keys and arrays of entries as values.
5. Write the result to `CHANGELOG-entry.md` in the
project root (or stdout for JSON format).
## Rules
- Skip merge commits
- Skip commits with "chore: release" messages
- Truncate commit messages longer than 80 characters
- Include the date range in the header
The triggers array defines how users invoke the skill. The args array describes accepted parameters with defaults. The body contains instructions Claude follows when the skill is activated.
Step 3: Add Hook Integration
Hooks let you run JavaScript before or after specific tool calls. For the changelog skill, we want to validate that the project has git history before attempting to generate a changelog.
Create hooks/changelog-hook.js:
export default {
name: "changelog-hook",
description: "Validates git state before changelog generation",
hooks: {
PreToolUse: {
match: {
tool: "Bash",
skill: "changelog"
},
handler: async (input, context) => {
const { command } = input;
if (command.includes("git log")) {
const tagCheck = await context.bash("git tag --list");
if (!tagCheck.stdout.trim()) {
return {
decision: "modify",
message: "No tags found. Using first commit as start point.",
updatedInput: {
...input,
command: command.replace(
/--since=[^\s]+/,
""
)
}
};
}
}
return { decision: "approve" };
}
},
PostToolUse: {
match: {
tool: "Write",
skill: "changelog"
},
handler: async (output, context) => {
if (output.filePath?.endsWith("CHANGELOG-entry.md")) {
const lineCount = output.content.split("\n").length;
context.log(
`Changelog generated: ${lineCount} lines`
);
}
return output;
}
}
}
};
The PreToolUse hook intercepts bash commands within the changelog skill and checks for git tags. If none exist, it modifies the command to use the full commit history instead of failing. The PostToolUse hook logs the result after the changelog file is written.
Step 4: Register the Plugin
Add your plugin to your Claude Code configuration so skills and hooks are discovered:
claude plugin add ./my-plugin
Verify the skill is registered:
claude skills list
You should see changelog in the output with its triggers and description.
Step 5: Test the Skill
Test with claude -p (prompt mode) to run the skill non-interactively:
claude -p "/changelog --since=v1.2.0"
For interactive testing, start a session and invoke the trigger:
claude
> /changelog --format json
To test the hook behavior specifically, run in a repository without tags:
cd /tmp && mkdir test-repo && cd test-repo
git init && git commit --allow-empty -m "feat: initial"
claude -p "/changelog"
The hook should log “No tags found” and still generate a changelog from the full history.
Step 6: Debug and Iterate
If the skill does not behave as expected, check these common issues:
- Trigger not recognized — verify the
triggersarray inSKILL.mdstarts with/ - Hook not firing — confirm the
match.skillvalue matches the skill name exactly - Arguments not passed — ensure
argsentries have correctnamefields matching what the trigger parser expects
Enable debug logging to see skill resolution and hook execution:
claude --debug
This prints which skill was matched, which hooks fired, and what modifications were applied.
What Next
Once your skill works locally, you can publish it to the Claude Code marketplace or share the plugin directory with your team. Skills compose well — you can build higher-level skills that invoke other skills, creating layered automation.
For more examples, browse the Brewcode plugin source. It contains 22 skills ranging from simple utilities to complex multi-step workflows. Each follows the same pattern: SKILL.md definition, optional hooks, clear instructions.