The !`command` blocks in SKILL.md go through Claude Code's Bash permission checker, which rejects $() command substitution. Simplified the git log command, replaced dynamic verify script path with a Read tool instruction, and narrowed allowed-tools to specific command patterns. Also documented the constraints in CLAUDE.md for future skill development. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3.0 KiB
3.0 KiB
Custom Claude Skills
Reusable Claude Code skills shared across all projects via symlinks into ~/.claude/skills/.
Repository Structure
custom-claude-skills/
├── CLAUDE.md # This file
├── MEMORY.md # Learnings about skill development
├── FUTURE.md # Future skill ideas
├── README.md # Setup instructions and skill catalogue
├── scripts/
│ └── install.sh # Symlinks all skills into ~/.claude/skills/
└── skills/
└── reflect/
└── SKILL.md # Milestone reflection skill
Conventions
- Each skill lives in
skills/<skill-name>/SKILL.md - Skills should be project-agnostic where possible — use dynamic context injection (
!command``) to adapt to the current project - The
scripts/install.shscript creates symlinks from~/.claude/skills/<name>→ this repo'sskills/<name>/ - After adding a new skill, run
./scripts/install.shto register it - Skills that are only useful for one project should live in that project's
.claude/skills/instead
Skill Development Guidelines
- Inline by default — only use
context: forkif the skill genuinely doesn't need conversation history - Pre-fetch context with
!command`` injection to reduce tool calls during execution - Restrict tools with
allowed-toolsto the minimum needed — this reduces permission prompts - Use $ARGUMENTS for user input,
$0,$1etc. for positional args - Dynamic commands in
!command`` run at skill load time, not during Claude's execution
!command`` Gotchas
The !command`` syntax in SKILL.md runs shell commands at skill load time to pre-gather context. These commands go through Claude Code's Bash permission checker against the allowed-tools patterns. Key constraints:
- No
$()command substitution — the permission checker rejects commands containing$()even if the outer command matches an allowed pattern. This means you cannot nest subshells (e.g.,git log --since="$(git log ...)"will fail). - No complex shell pipelines relying on subshells — keep
!command`` commands simple and self-contained. If you need complex logic, have the skill instructions tell Claude to run it via tool calls instead. allowed-toolspatterns must match the command binary —Bash(git *)allows anygitsubcommand, butBash(cat *),Bash(sed *),Bash(ls *)must be listed separately if those commands appear in!command`` blocks.- Prefer specific tool patterns over broad ones —
Bash(git log *),Bash(git diff *)is safer thanBash(git *)to avoid accidentally allowing destructive git commands (e.g.,git reset,git push --force). - Fallback to tool instructions for dynamic paths — if a command needs
$ARGUMENTSto compute a file path (e.g.,cat scripts/verify-m${N}.sh), replace the!command`` with a plain-text instruction telling Claude to use the Read tool. This avoids$()substitution and is more reliable.