
Meta Command Creator
- 62 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
meta-command-creator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- meta-command-creator
- AI & Agent Building
- AI-coding skill
Meta Command Creator by the numbers
- 62 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,310 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill meta-command-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Custom Slash Command Creator
Overview
Claude Code slash commands are markdown files with optional YAML frontmatter that create reusable /command workflows. Commands and skills are unified: .claude/commands/ files and .claude/skills/ directories both create slash commands. Skills are the recommended approach since they support additional features like supporting files, but single-file commands still work.
When to use: Repeatable prompts, team workflow standardization, guardrailed operations (deploy, commit), multi-phase tasks, dynamic context injection with bash output.
When NOT to use: Complex capabilities needing multiple files and scripts (use a full skill directory instead), one-off prompts, built-in commands that already exist (/compact, /help, /init).
Quick Reference
| Feature | Syntax / Location | Key Points |
|---|---|---|
| Project command | .claude/commands/name.md | Shows "(project)" in /help |
| Project skill | .claude/skills/name/SKILL.md | Recommended over commands, supports extra files |
| Personal command | ~/.claude/commands/name.md | Available across all projects |
| Personal skill | ~/.claude/skills/name/SKILL.md | Available across all projects |
| Plugin command | <plugin>/skills/name/SKILL.md | Namespaced as plugin-name:skill-name |
| Subdirectory commands | .claude/commands/git/commit.md | Shows "(project:git)" in /help |
| All arguments | $ARGUMENTS | Entire argument string |
| Positional arguments | $0, $1, $2 or $ARGUMENTS[0] | Zero-based index |
| Bash injection | ` !git status ` | Runs before prompt is sent, output replaces placeholder |
| File reference | @src/file.ts | Inlines file contents into prompt |
| Extended thinking | Include "ultrathink" in content | Triggers deeper reasoning mode |
| Session ID | ${CLAUDE_SESSION_ID} | Current session identifier for logging |
| Frontmatter description | description: What it does | Required for /help listing and Skill tool |
| Tool restrictions | allowed-tools: Read, Grep, Glob | Limits tools without per-use approval |
| Manual-only invocation | disable-model-invocation: true | Prevents Claude from auto-triggering |
| Hide from menu | user-invocable: false | Background knowledge only Claude loads |
| Argument hint | argument-hint: [issue-number] | Shown in autocomplete |
| Model override | model: claude-sonnet-4-20250514 | Specific model for this command |
| Subagent execution | context: fork | Runs in isolated context |
| Subagent type | agent: Explore | Built-in or custom agent when context: fork |
Priority Order
When commands share the same name across levels, higher-priority locations win:
1. Enterprise (managed settings) 2. Personal (~/.claude/) 3. Project (.claude/)
Plugin commands use namespacing (plugin:name) so they never conflict. If a skill and a command share the same name, the skill takes precedence.
Common Mistakes
| Mistake | Impact | Correct Pattern |
|---|---|---|
Adding name field in command files | Ignored for .md commands | Name is inferred from filename |
Missing description frontmatter | Invisible in /help and Skill tool | Always include description |
Using category or tags fields | Silently ignored | Use subdirectories for organization |
$ARGUMENTS without handling empty case | Unexpected behavior | Check if empty and provide default |
` !bash without allowed-tools` | Commands fail to execute | Add allowed-tools: Bash(...) to frontmatter |
| Expecting project to override personal | Personal wins over project | Personal commands take priority; rename to avoid conflicts |
Using context: fork without a task | Subagent has no actionable work | Only fork skills with explicit step-by-step instructions |
| Overriding built-in command names | Built-ins cannot be overridden | Use different names (/my-help not /help) |
Delegation
- Command pattern discovery: Use
Exploreagent to find existing commands in.claude/commands/or.claude/skills/ - Complex workflow skills: For multi-file capabilities, create a full skill directory with
SKILL.mdand supporting files
References
- Command anatomy: markdown structure, YAML frontmatter fields, file locations, and priority order
- Arguments and references: $ARGUMENTS, positional args, bash injection, and file references
- Templates: ready-to-use command templates for common patterns
Arguments and References
$ARGUMENTS Placeholder
When a user invokes /command some text here, the string some text here is available as $ARGUMENTS. If the command content does not include $ARGUMENTS, Claude Code appends ARGUMENTS: <value> to the end of the content.
Basic usage
---
description: Fix a GitHub issue
disable-model-invocation: true
---
Fix GitHub issue $ARGUMENTS following our coding standards.
## Steps
1. Read the issue description
2. Understand the requirements
3. Implement the fix
4. Write tests
5. Create a commitInvocation: /fix-issue 123 replaces $ARGUMENTS with 123.
Handling empty arguments
Always account for the case where no arguments are provided:
---
description: Build the project
argument-hint: [target]
---
Build target: $ARGUMENTS
## Steps
1. If no arguments provided, run the default build
2. Otherwise, build the specified target
3. Report resultsPositional Arguments
Access individual arguments by zero-based index using $ARGUMENTS[N] or the shorthand $N.
$ARGUMENTS[N] syntax
---
description: Migrate a component between frameworks
argument-hint: [component] [from] [to]
---
Migrate the $ARGUMENTS[0] component from $ARGUMENTS[1] to $ARGUMENTS[2].
Preserve all existing behavior and tests.Invocation: /migrate-component SearchBar React Vue
$ARGUMENTS[0]=SearchBar$ARGUMENTS[1]=React$ARGUMENTS[2]=Vue
$N shorthand
The same command using shorthand notation:
---
description: Migrate a component between frameworks
argument-hint: [component] [from] [to]
---
Migrate the $0 component from $1 to $2.
Preserve all existing behavior and tests.Two-argument pattern
---
description: Create a new file from template
argument-hint: [type] [name]
---
Create a $0 named $1
## Steps
1. Determine template based on type $0
2. Create file named $1
3. Apply project conventionsInvocation: /create component UserProfile
Bash Injection (! Prefix)
The ` !command ` syntax runs shell commands before the skill content is sent to Claude. The command output replaces the placeholder. This is preprocessing, not something Claude executes.
Requirements
- The
allowed-toolsfrontmatter MUST includeBash(...)when using!prefix - Commands run in the project root directory
- Output is inserted inline where the placeholder appears
Git context example
---
description: Create a conventional git commit
allowed-tools: Bash(git add:*), Bash(git commit:*), Bash(git status:*), Bash(git diff:*)
argument-hint: [optional message]
---
## Context
- Current status: !`git status`
- Current diff: !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -5`
## Steps
1. Analyze the changes shown above
2. Stage appropriate files
3. Create commit with conventional format: type(scope): description
## Format
Use these types: feat, fix, docs, style, refactor, perf, test, chorePR review example
---
description: Summarize changes in a pull request
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---
## Pull request context
- PR diff: !`gh pr diff`
- PR comments: !`gh pr view --comments`
- Changed files: !`gh pr diff --name-only`
## Your task
Summarize this pull request. Focus on:
1. What changed and why
2. Potential risks
3. Testing recommendationsMultiple dynamic sources
---
description: Review staged changes before commit
allowed-tools: Bash(git:*), Read, Grep, Glob
---
## Context
- Staged changes: !`git diff --cached`
- Modified files: !`git diff --cached --name-only`
- Current branch: !`git branch --show-current`
## Steps
1. Review each changed file
2. Check against project conventions
3. Look for common issues
4. Provide structured feedbackFile References (@ Prefix)
The @ prefix inlines file contents into the command prompt. Use it to give Claude direct access to specific files without requiring tool calls.
Basic usage
---
description: Review configuration files
---
Review the following configuration for issues:
- Package config: @package.json
- TypeScript config: @tsconfig.json
- ESLint config: @eslint.config.jsComparing files
---
description: Compare two implementations
argument-hint: [file1] [file2]
---
Compare @$0 with @$1 and summarize the differences.
Focus on:
1. Behavioral changes
2. Performance implications
3. Missing edge casesReferencing project context
---
description: Create a component following project patterns
argument-hint: [component-name]
---
Create a new component named $ARGUMENTS following the patterns in:
- Example component: @src/components/Button.tsx
- Shared types: @src/types/components.d.ts
## Steps
1. Follow the structure of the example component
2. Use shared types from the types file
3. Add appropriate props interfaceSession Variables
${CLAUDE_SESSION_ID}
The current session identifier. Useful for logging, session-specific files, or correlating output.
---
description: Log activity for this session
---
Log the following to logs/${CLAUDE_SESSION_ID}.log:
$ARGUMENTSExtended Thinking
Include the word "ultrathink" in skill content to trigger extended thinking mode for deeper reasoning:
---
description: Analyze architecture implications
disable-model-invocation: true
---
ultrathink
Analyze the architecture implications of $ARGUMENTS.
## Steps
1. Map current dependencies
2. Identify affected components
3. Evaluate trade-offs
4. Recommend approachCombining Patterns
Commands can combine multiple features for powerful workflows:
---
description: Debug a failing test
allowed-tools: Bash(pnpm test:*), Read, Grep, Glob
argument-hint: [test file or pattern]
---
## Context
- Test output: !`pnpm test $ARGUMENTS 2>&1 | tail -50`
- Test file: @$ARGUMENTS
## Steps
1. Analyze the test failure output above
2. Read the test file and implementation
3. Identify the root cause
4. Suggest a fix
## Output
Provide the root cause and a specific code fix.Command Anatomy
Markdown Structure
Every command is a markdown file with optional YAML frontmatter followed by instruction content. The filename (minus .md) becomes the command name.
---
description: Brief description shown in /help
allowed-tools: Read, Grep, Glob
argument-hint: [filename]
---
# Command Title
## Steps
1. First action
2. Second action
3. Final actionYAML Frontmatter Fields
All fields are optional. Only description is recommended so Claude knows when to use the command.
description
Brief text shown in /help and used by the Skill tool to decide when to invoke the command.
---
description: Create a conventional git commit with proper formatting
---Commands without description do not appear in /help and cannot be invoked by the Skill tool.
allowed-tools
Restricts which tools Claude can use without per-use approval when the command is active. Supports glob patterns for tool arguments.
---
allowed-tools: Bash(git add:*), Bash(git commit:*), Bash(git status:*), Read
---Common patterns:
---
allowed-tools: Read, Grep, Glob
------
allowed-tools: Bash(gh *), Read, Grep
---Required when using ` !bash ` injection syntax.
argument-hint
Hint displayed during autocomplete to indicate expected arguments.
---
argument-hint: [issue-number]
------
argument-hint: [type] [name]
---disable-model-invocation
Claude Code only — other agents ignore this field.
Prevents Claude from automatically loading this command. Use for commands with side effects (deploy, commit, send messages) where you want explicit control over timing.
---
disable-model-invocation: true
---When set to true, the description is not loaded into Claude context at all, reducing context cost to zero.
user-invocable
Claude Code only — other agents ignore this field.
Set to false to hide from the / menu. Use for reference/knowledge skills where /skill-name isn't a meaningful user action. Claude can still auto-load the skill when relevant.
---
user-invocable: false
---model
Override the model used when this command is active.
---
model: claude-sonnet-4-20250514
---context
Set to fork to run the command in an isolated subagent context. The command content becomes the prompt that drives the subagent. The subagent has no access to conversation history.
---
context: fork
---Only use context: fork with commands that have explicit task instructions. Guidelines-only content (like "use these conventions") produces no meaningful output in a subagent.
agent
Specifies which subagent type to use when context: fork is set. Options include built-in agents (Explore, Plan, general-purpose) or custom subagents from .claude/agents/.
---
context: fork
agent: Explore
---If omitted, defaults to general-purpose.
hooks
Hooks scoped to this command's lifecycle. See the Claude Code hooks documentation for configuration format.
---
hooks:
pre_tool_call:
- matcher: Bash
hooks:
- command: echo "About to run bash"
type: command
---Invocation control matrix (Claude Code)
These settings only affect Claude Code. Other agents treat all skills as default (both user and agent can invoke).
| Frontmatter | User can invoke | Claude can invoke | Context cost |
|---|---|---|---|
| (default) | Yes | Yes | Description always loaded |
disable-model-invocation: true | Yes | No | Zero (description not loaded) |
user-invocable: false | No | Yes | Description always loaded |
File Locations
Single-file commands
| Location | Invocation | Label in /help |
|---|---|---|
.claude/commands/name.md | /name | (project) |
.claude/commands/git/commit.md | /commit | (project:git) |
~/.claude/commands/name.md | /name | (user) |
Plugin commands/name.md | /name | (plugin-name) |
Skill directories
| Location | Invocation | Label in /help |
|---|---|---|
.claude/skills/name/SKILL.md | /name | (project) |
~/.claude/skills/name/SKILL.md | /name | (user) |
Plugin skills/name/SKILL.md | /name | (plugin-name) |
Skills are the recommended format. They support additional files (templates, scripts, reference docs) alongside SKILL.md.
Automatic discovery in monorepos
When editing files in subdirectories, Claude Code also discovers skills from nested .claude/skills/ directories. For example, editing a file in packages/frontend/ also loads skills from packages/frontend/.claude/skills/.
Priority Order
When commands share a name across levels:
1. Enterprise (managed settings) -- highest priority 2. Personal (~/.claude/) 3. Project (.claude/)
Plugin commands use namespacing (plugin-name:skill-name) and never conflict with other levels. If a skill directory and a command file share the same name, the skill takes precedence.
Organizing Commands with Subdirectories
Use subdirectories to categorize project commands:
.claude/commands/
├── git/
│ ├── commit.md # /commit -> "(project:git)"
│ ├── pr.md # /pr -> "(project:git)"
│ └── sync.md # /sync -> "(project:git)"
├── dev/
│ ├── feature.md # /feature -> "(project:dev)"
│ └── debug.md # /debug -> "(project:dev)"
└── docs/
└── update.md # /update -> "(project:docs)"The subdirectory name appears in parentheses in /help as (project:subdirectory).
Naming Conventions
| Pattern | Example | Use Case |
|---|---|---|
verb-noun | create-component | Action commands |
noun | commit | Well-known actions |
verb | review | Context-dependent |
Skill Tool Permissions
Claude invokes commands programmatically via the Skill tool. Control access through permission rules:
| Rule | Matches |
|---|---|
Skill(commit) | Only /commit with no args |
Skill(review-pr *) | /review-pr with any args |
Skill | Deny all skill invocations |
Character Budget
Skill descriptions consume a shared character budget (default: 15,000 characters). When exceeded, Claude sees fewer commands. Check with /context and increase via the SLASH_COMMAND_TOOL_CHAR_BUDGET environment variable.
Enabling Automatic Invocation
Reference commands in CLAUDE.md to encourage Claude to use them automatically:
When writing tests, run /write-unit-test to generate test files.
After fixing bugs, run /verify-fix to ensure the fix is complete.This works because Claude reads CLAUDE.md at the start of every session and treats its content as project instructions.
Built-in Commands
These commands are built-in and cannot be overridden:
| Command | Purpose |
|---|---|
/help | Get usage help |
/compact | Compact conversation |
/memory | Edit CLAUDE.md files |
/init | Initialize CLAUDE.md |
/permissions | View/update permissions |
/agents | Manage subagents |
/mcp | Manage MCP servers |
/context | View context usage |
Run /help in Claude Code for the full list.
Commands vs Skills vs Subagents
| Aspect | Single-file command | Skill directory | Subagent |
|---|---|---|---|
| Structure | One .md file | Directory with SKILL.md + extras | .claude/agents/name.md |
| Discovery | Explicit (/command) | Automatic or explicit | Delegated by Claude |
| Supporting files | No | Yes (scripts, templates, docs) | Can preload skills |
| Best for | Simple reusable prompts | Complex capabilities with resources | Isolated task delegation |
Command Templates
Basic Command
Minimal command with description and numbered steps.
---
description: What this command does
---
# Command Name
Brief explanation of purpose.
## Steps
1. First action
2. Second action
3. Final action
## Reference
- Use `cli-command` for specific purposeCommand with Arguments
Accept dynamic input with $ARGUMENTS and argument-hint.
---
description: Review a pull request by number
argument-hint: [PR number]
allowed-tools: Bash(gh:*), Read, Grep, Glob
---
Review pull request #$ARGUMENTS
## Steps
1. Fetch PR details with `gh pr view $ARGUMENTS`
2. Review changed files
3. Check for issues against project conventions
4. Provide structured feedback
## Output Format
Provide:
- Summary of changes
- List of issues found
- Verdict: APPROVE or REQUEST CHANGESUsage: /review-pr 123
Guardrailed Command
For commands that modify state. Includes safety constraints and explicit boundaries.
---
description: Deploy application to environment
disable-model-invocation: true
argument-hint: [environment]
allowed-tools: Bash(git:*), Bash(pnpm:*), Read
---
Deploy to $ARGUMENTS environment.
## Guardrails
- Do NOT deploy if tests fail
- Do NOT force-push or skip CI checks
- Do NOT modify environment variables without confirmation
- Only deploy from the main branch
## Steps
1. Verify current branch is main
2. Run full test suite
3. Build the application
4. Execute deployment to $ARGUMENTS
5. Verify deployment succeeded
## Rollback
If deployment fails:
1. Revert to previous deployment
2. Report the failure with error detailsUsage: /deploy staging
Git Commit Command
Uses bash injection for dynamic context.
---
description: Create a conventional git commit
disable-model-invocation: true
allowed-tools: Bash(git add:*), Bash(git commit:*), Bash(git status:*), Bash(git diff:*)
argument-hint: [optional message]
---
## Context
- Current status: !`git status`
- Current diff: !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -5`
## Steps
1. Analyze the changes shown above
2. Stage appropriate files (not unrelated changes)
3. Create commit with conventional format: type(scope): description
4. If $ARGUMENTS provided, use it as the commit message basis
## Format
Use conventional commit types: feat, fix, docs, style, refactor, perf, test, choreMulti-Phase Command
Complex workflows with distinct phases. Consider using context: fork for isolation.
---
description: Complete feature development workflow
disable-model-invocation: true
argument-hint: [feature name]
---
# Feature Development: $ARGUMENTS
## Phase 1: Planning
1. Understand the requirement for $ARGUMENTS
2. Identify affected files and components
3. Create implementation plan
## Phase 2: Implementation
1. Create or modify necessary files
2. Follow project conventions
3. Add appropriate tests
## Phase 3: Verification
1. Run type checking
2. Run linting
3. Run tests
4. Verify no regressions
## Phase 4: Documentation
1. Update relevant documentation
2. Add inline comments only where non-obvious
## Output
Provide a summary of all changes made across phases.Code Review Command
Combines bash injection with structured output.
---
description: Review staged changes before commit
allowed-tools: Bash(git:*), Read, Grep, Glob
---
## Context
- Staged changes: !`git diff --cached`
- Modified files: !`git diff --cached --name-only`
## Steps
1. Review each changed file for correctness
2. Check against project conventions
3. Look for common issues: missing error handling, type safety, edge cases
4. Provide structured feedback
## Output
For each file, report:
- Status: OK or NEEDS CHANGES
- Issues found (if any)
- Overall verdict: READY TO COMMIT or NEEDS CHANGESDebug Helper Command
Accepts error context and provides structured analysis.
---
description: Help debug an error message or failing code
argument-hint: [error message or file:line]
---
Debug: $ARGUMENTS
## Steps
1. Parse the error location from the arguments
2. Read the relevant file and surrounding context
3. Search for related code patterns
4. Identify potential root causes
5. Suggest specific fixes
## Output
Provide:
- Parsed error details
- Root cause analysis
- Specific code fix with explanationRead-Only Explorer
Restricts tools to prevent modifications.
---
description: Explore and explain code without making changes
allowed-tools: Read, Grep, Glob
argument-hint: [file or pattern]
---
Explore $ARGUMENTS without making any changes.
## Steps
1. Find relevant files matching $ARGUMENTS
2. Read and analyze the code
3. Trace data flow and dependencies
4. Explain the architecture
## Output
Provide:
- File structure overview
- Key components and their responsibilities
- Data flow diagram (ASCII)
- Notable patterns or concernsForked Research Command
Runs in an isolated subagent for focused exploration.
---
description: Research a topic in the codebase
context: fork
agent: Explore
argument-hint: [topic]
---
Research $ARGUMENTS thoroughly in this codebase.
## Steps
1. Find relevant files using Glob and Grep
2. Read and analyze the code
3. Trace usage patterns and dependencies
4. Map the architecture
## Output
Summarize findings with specific file references:
- Where the feature lives
- How it works
- Key dependencies
- Potential improvementsComponent Generator
Uses file references for pattern consistency.
---
description: Generate a new component following project patterns
disable-model-invocation: true
argument-hint: [component-name]
---
Create a new component named $ARGUMENTS.
## Reference Pattern
Use the existing component structure from the project.
## Steps
1. Determine the appropriate directory for the new component
2. Create the component file with proper TypeScript types
3. Create a test file following existing test patterns
4. Export from the nearest index file
5. Verify types and lint pass
## Conventions
- PascalCase for component names and files
- Props interface named `${ComponentName}Props`
- Colocate tests as `component-name.test.tsx`Best Practices
Clear Steps
Use bold labels for phases in numbered steps:
## Steps
1. **Analyze** - Examine the current state
2. **Plan** - Determine necessary changes
3. **Execute** - Make the changes
4. **Verify** - Confirm successInclude Reference Commands
Tell Claude which CLI tools to use:
## Reference
- Use `gh pr view` to inspect PR details
- Use `git diff` to see changes
- Use `pnpm test` to verify no regressionsProvide Output Format
Specify the expected structure of results:
## Output Format
\`\`\`markdown
## Result
**Status**: [Success/Failure]
**Changes Made**:
- [Change 1]
- [Change 2]
**Next Steps**:
- [Recommendation]
\`\`\`Use File References
Reference project files with @ for context:
Review the implementation considering:
- Main file: @src/utils/helpers.ts
- Tests: @src/utils/helpers.test.ts
- Types: @src/types/helpers.d.tsChecklist for New Commands
When creating a command, verify:
1. File location matches intended scope (project vs personal) 2. description frontmatter is present 3. Steps are numbered and actionable 4. $ARGUMENTS handled for empty case (if used) 5. allowed-tools included when using ` !bash injection 6. All code blocks have language specifiers 7. Guardrails section included for state-changing commands 8. disable-model-invocation: true` set for side-effect commands 9. Output format specified when structured output is expected
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.11"
# ///
"""
Validate Claude Code slash command structure against best practices.
Usage:
uv run scripts/validate-command.py <path> # Single file or directory
uv run scripts/validate-command.py <glob-pattern> # Multiple commands
uv run scripts/validate-command.py .claude/commands/ # All commands in directory
Examples:
uv run scripts/validate-command.py .claude/commands/my-command.md
uv run scripts/validate-command.py .claude/commands/
uv run scripts/validate-command.py ".claude/commands/**/*.md"
"""
import glob
import sys
from pathlib import Path
def parse_frontmatter(content: str) -> tuple[dict[str, str] | None, str | None]:
"""Parse YAML frontmatter from content."""
if not content.startswith("---"):
return None, "YAML frontmatter must start with --- on line 1"
lines = content.split("\n")
end_idx = None
for i, line in enumerate(lines[1:], 1):
if line.strip() == "---":
end_idx = i
break
if end_idx is None:
return None, "Invalid YAML frontmatter: missing closing ---"
frontmatter = {}
for line in lines[1:end_idx]:
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
key, _, value = line.partition(":")
frontmatter[key.strip()] = value.strip().strip('"').strip("'")
return frontmatter, None
def resolve_command_paths(path_arg: str) -> tuple[list[Path], str | None]:
"""
Resolve a path argument to a list of command .md files.
Handles:
- Direct file path: .claude/commands/my-command.md
- Directory path: .claude/commands/ (finds all .md files recursively)
- Glob pattern: .claude/commands/**/*.md
Returns:
Tuple of (list of Path objects, error message if any)
"""
path = Path(path_arg)
# Case 1: Direct file path
if path.is_file():
if not path.suffix == ".md":
return [], f"Expected .md file, got: {path.name}"
return [path], None
# Case 2: Directory - find all .md files recursively
if path.is_dir():
cmd_files = [f for f in path.rglob("*.md") if f.name != "README.md"]
if cmd_files:
return sorted(cmd_files), None
return [], f"No command .md files found in: {path}"
# Case 3: Glob pattern
if "*" in path_arg or "?" in path_arg:
matches = glob.glob(path_arg, recursive=True)
cmd_files = [Path(m) for m in matches if m.endswith(".md") and not m.endswith("README.md")]
if cmd_files:
return sorted(cmd_files), None
return [], f"No command files match pattern: {path_arg}"
# Path doesn't exist
return [], f"Path not found: {path_arg}"
def validate_command(path: Path) -> tuple[list[str], list[str]]:
"""Validate a command .md file."""
errors: list[str] = []
warnings: list[str] = []
try:
content = path.read_text()
except PermissionError:
return [f"Permission denied: {path}"], []
except OSError as e:
return [f"Cannot read file: {e}"], []
lines = content.split("\n")
# Frontmatter validation
frontmatter, fm_error = parse_frontmatter(content)
if fm_error:
errors.append(fm_error)
elif frontmatter:
# Required field
if "description" not in frontmatter:
errors.append("Missing required field: 'description'")
# Invalid fields check
invalid_fields = ["name", "category", "tags"]
for field in invalid_fields:
if field in frontmatter:
warnings.append(f"Invalid field '{field}' - will be ignored (name is inferred from filename)")
# Check if using bash execution without allowed-tools
if "!" in content and "`" in content:
if "allowed-tools" not in frontmatter:
warnings.append("Using !`command` syntax but missing 'allowed-tools' with Bash permission")
# Line count
line_count = len(lines)
if line_count > 500:
errors.append(f"Command file is {line_count} lines (max 500)")
elif line_count > 200:
warnings.append(f"Command file is {line_count} lines (consider splitting into smaller commands)")
# Code block language specifiers
in_code = False
for i, line in enumerate(lines, 1):
if line.strip().startswith("```"):
if not in_code and line.strip() == "```":
errors.append(f"Line {i}: Code block missing language specifier (MD040)")
in_code = not in_code
# Check for steps or instructions (## or ** format)
content_lower = content.lower()
has_steps = (
"## steps" in content_lower
or "## instructions" in content_lower
or "**steps**" in content_lower
or "**instructions**" in content_lower
)
if not has_steps:
warnings.append("Consider adding '## Steps' or '## Instructions' section")
# Check for $ARGUMENTS handling
if "$ARGUMENTS" in content or "$1" in content:
if "argument-hint" not in (frontmatter or {}):
warnings.append("Using $ARGUMENTS but missing 'argument-hint' in frontmatter")
return errors, warnings
def print_result(path: Path, errors: list[str], warnings: list[str], verbose: bool = True) -> None:
"""Print validation results for a single command."""
# Use relative path from .claude/commands for display
try:
rel_path = path.relative_to(Path(".claude/commands"))
cmd_name = str(rel_path.with_suffix(""))
except ValueError:
cmd_name = path.stem
if errors:
print(f"❌ {cmd_name}: FAILED")
if verbose:
for error in errors:
print(f" ✗ {error}")
elif warnings:
print(f"✓ {cmd_name}: valid (with {len(warnings)} warning(s))")
if verbose:
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(f"✓ {cmd_name}: passed")
def main() -> int:
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: uv run scripts/validate-command.py <path>")
print()
print("Accepts:")
print(" - File path: .claude/commands/my-command.md")
print(" - Directory: .claude/commands/")
print(" - Glob pattern: '.claude/commands/**/*.md'")
print()
print("Examples:")
print(" uv run scripts/validate-command.py .claude/commands/commit.md")
print(" uv run scripts/validate-command.py .claude/commands/")
return 1
path_arg = sys.argv[1]
cmd_paths, error = resolve_command_paths(path_arg)
if error:
print(f"❌ Error: {error}")
return 1
if not cmd_paths:
print("❌ No commands found to validate")
return 1
# Validate all commands
total_errors = 0
total_warnings = 0
failed_cmds = []
# Single command - verbose output
if len(cmd_paths) == 1:
path = cmd_paths[0]
errors, warnings = validate_command(path)
if errors:
print("❌ Command validation FAILED\n")
print("Errors:")
for error in errors:
print(f" ✗ {error}")
print()
if warnings:
print("Warnings:")
for warning in warnings:
print(f" ⚠ {warning}")
print()
if not errors and not warnings:
print("✓ Command validation passed")
elif not errors:
print("✓ Command valid (with warnings)")
return 1 if errors else 0
# Multiple commands - summary output
print(f"Validating {len(cmd_paths)} command(s)...\n")
for path in cmd_paths:
errors, warnings = validate_command(path)
total_errors += len(errors)
total_warnings += len(warnings)
if errors:
failed_cmds.append(path.stem)
print_result(path, errors, warnings, verbose=bool(errors))
# Summary
print()
if failed_cmds:
print(f"❌ {len(failed_cmds)} command(s) failed: {', '.join(failed_cmds)}")
else:
print(f"✓ All {len(cmd_paths)} command(s) passed")
if total_warnings:
print(f" {total_warnings} total warning(s)")
return 1 if failed_cmds else 0
if __name__ == "__main__":
sys.exit(main())