| name | write-skill |
| description | Use when creating a new skill or improving an existing one — applies best practices for structure, dynamic context, and safety |
| tags | ["personal"] |
Skill Author
Create or improve Claude Code skills (slash commands) following established patterns and avoiding known pitfalls.
Arguments
$ARGUMENTS - What the skill should do, or the name of an existing skill to improve
Context
- Existing skills: !
find .claude/skills -maxdepth 2 -name "SKILL.md" 2>/dev/null | head -30
- Project type: !
find . -maxdepth 1 \( -name go.mod -o -name Gemfile -o -name package.json -o -name Cargo.toml -o -name pyproject.toml \) 2>/dev/null | head -3
Instructions
Step 1: Understand the Request
Determine whether the user wants to:
- Create a new skill from a description
- Improve an existing skill (read it first, then apply changes)
If creating, ask the user what the skill should do if $ARGUMENTS is vague.
Step 2: Design the Skill
Plan the skill structure:
- Frontmatter (YAML between
--- fences)
- Context section (dynamic context via shell commands)
- Instructions (what Claude should do when the skill is invoked)
Step 3: Write the Skill File
Write the file to .claude/skills/<name>/SKILL.md.
Follow all rules below carefully.
Skill File Reference
Frontmatter
All fields are optional. Only description is strongly recommended.
---
name: skill-name
description: Use when <trigger situation> -- <what the skill does>
allowed-tools: Bash(git *), Bash(npm test)
---
| Field | Purpose |
|---|
name | Skill name, used for /name invocation. |
description | Drives auto-activation and / menu. MUST start with "Use when..." or "You MUST use this before/after..." to tell Claude's router when to fire. Noun-phrase descriptions ("Multi-agent debugging") will never auto-trigger. Max 1024 chars. |
allowed-tools | Tools Claude can use without asking permission. Glob patterns supported. |
context | Set to fork to run in an isolated subagent (no conversation history). |
agent | Subagent type when context: fork (e.g., Explore, Plan). |
model | Override model (e.g., sonnet for cheaper tasks). |
disable-model-invocation | true to prevent auto-loading (manual /name only). Use for destructive skills. |
user-invocable | false to hide from / menu (background knowledge only). |
Dynamic Context
Inject live data by writing an exclamation mark followed by a command wrapped in backticks. The command runs locally and its output replaces the placeholder before Claude sees the prompt.
In the examples below, !{command} represents the actual syntax (exclamation mark + backtick + command + backtick). Curly braces are used here to prevent this skill file from executing its own examples.
## Context
- Current branch: !{git branch --show-current}
- Git status: !{git status --short}
- Project type: !{ls -1 go.mod package.json Cargo.toml 2>/dev/null | head -3}
CRITICAL: No $() Command Substitution
Claude Code blocks $() inside dynamic context expressions for security reasons. This is the single most common authoring mistake.
# BROKEN -- $() is blocked
- Branch commits: !{DEFAULT=$(git symbolic-ref ...) && git log origin/$DEFAULT..HEAD}
# FIXED -- single commands with error suppression
- Branch commits: !{git log origin/HEAD..HEAD --oneline 2>/dev/null | head -50}
Alternatives to $():
- Pipe chains:
command1 | command2 | command3
- Error suppression:
command 2>/dev/null (empty output on failure is fine)
- Use
origin/HEAD: resolves to whatever the remote default branch is
- Separate context lines: split a complex command into multiple simpler lines
Output Size Management
Always bound output to avoid blowing up the context window:
# GOOD -- bounded output
- Files: !{find . -maxdepth 3 -name "*.go" 2>/dev/null | head -20}
- Commits: !{git log --oneline -10}
# BAD -- unbounded, could be massive
- Files: !{find . -name "*.go"}
- Log: !{git log}
Error Handling and Exit Codes
Use 2>/dev/null to suppress stderr, but it does not fix exit codes. When a command fails, the skill loader sees the non-zero exit code and treats the whole init expression as failed, the skill never loads, and the user sees Shell command failed for pattern .... Bare 2>/dev/null is not enough.
Pick one of two safe patterns:
Pattern A: pipe through | head -N -- silent fallback. In a pipeline the exit code is the last command's, so empty upstream output yields exit 0.
# BAD -- 2>/dev/null suppresses stderr but exit code is still non-zero
- Tag: !{git describe --tags --abbrev=0 2>/dev/null}
# GOOD -- pipe neutralizes exit code, empty output on failure is fine
- Tag: !{git describe --tags --abbrev=0 2>/dev/null | head -1}
Pattern B: || echo '<sentinel>' -- produces a readable placeholder so Claude knows the command failed, not just that the output is empty. Useful when the difference between "no value" and "command failed" matters semantically.
- Branch: !{git branch --show-current 2>/dev/null || echo '(not in a git repo)'}
- Repo root: !{git rev-parse --show-toplevel 2>/dev/null || echo '(not in a git repo)'}
|| and && are fine inside skill init expressions. They are sometimes flagged in Bash tool calls Claude makes later (a separate concern in the global Bash style rules), but the skill loader does not block them.
Guarding git commands against non-repo CWD
Skills are increasingly invoked from sandboxed sessions or scratch directories where the CWD is not a git work tree. Every standalone git call in skill init MUST tolerate that case -- otherwise the skill fails to load entirely.
- Standalone git commands (no pipe, no chain) -- use Pattern A or B above so the exit code is 0 and the output is meaningful.
- Piped git commands (
git ... | head, git ... | awk, etc.) -- already safe because the pipe absorbs the upstream exit code. Stderr will still leak unless you add 2>/dev/null to the git stage; do that.
- Chained commands (
git X && git Y) -- && propagates failure. Either rewrite as two separate context lines or wrap with Pattern B at the end of the chain.
The classic failure mode: !{git branch --show-current} outside a git repo emits fatal: not a git repository ... to stderr AND exits non-zero. The skill never loads. Patterns A and B both fix this.
String Substitutions
| Variable | Description |
|---|
$ARGUMENTS | All arguments the user passed after /skill-name |
$ARGUMENTS[0], $1 | First argument (0-indexed) |
Skill Body Best Practices
- Be concise. Only add context Claude does not already have.
- Use imperative instructions. Tell Claude what to do, not what the skill "can" do.
- Structure with numbered steps. Makes execution predictable and debuggable.
- Include abort conditions. Define when to stop (e.g., "if no changes, tell the user and stop").
- Set loop limits. If the skill loops (CI check/fix), cap retries (e.g., "if stuck 3+ times, stop and report").
- Avoid backticks in prose. The skill content passes through shell evaluation; inline code fences can break parsing.
- Avoid contractions. Words like "don't" or "can't" can break due to single-quote shell interpretation.
- Match specificity to fragility. Give high freedom for flexible tasks, exact commands for fragile operations.
- Use
disable-model-invocation: true for skills with side effects (deploy, merge, send).
Skill Location Convention
All skills live in .claude/skills/<name>/SKILL.md inside the project. Never put skills in global ~/.claude/skills/.
Step 4: Validate
After writing, check the skill for:
Report the validation results and the file path of the new/updated skill.