# validate-skill ## Purpose Validate Claude Code SKILL.md files against known restrictions that cause permission checker failures, sandbox errors, and other breakages. ## Usage ``` validate-skill [OPTIONS] ``` ### Arguments | Argument | Default | Description | |----------|---------|-------------| | `PATH` | `.` (current directory) | A SKILL.md file or a directory to scan recursively | ### Flags | Flag | Short | Description | |------|-------|-------------| | `--dryrun` | `-n` | List SKILL.md files that would be checked without validating | | `--help` | `-h` | Show usage information | ## Behaviour 1. **Discovery phase:** If `PATH` is a file, validate that one file. If `PATH` is a directory, find all files named `SKILL.md` recursively. 2. **Validation phase:** For each SKILL.md file, run these checks: **Frontmatter checks:** a. File starts with `---` on line 1 b. A closing `---` delimiter exists on a subsequent line c. `name:` field exists, contains only lowercase letters, numbers, and hyphens, max 64 characters d. `description:` field exists and is non-empty (multi-line `>` syntax counts as non-empty) e. Non-ASCII characters in frontmatter (em dashes, smart quotes, curly apostrophes, etc.) — these can silently break YAML parsing and prevent the skill from loading **Bang-command checks** (lines matching `!` followed by a backtick): e. No `${VAR}` syntax — must use `$VAR` instead f. No `$()` command substitution g. No `~/` paths (sandbox-fragile) h. No `../` relative paths (CWD-fragile) i. Warning (not error) for hardcoded `/home/` paths **allowed-tools coverage checks:** j. For each bang-command, the command binary (first word) must match a `Bash(binary *)` or `Bash(binary)` pattern in `allowed-tools` k. Warning for overly broad patterns like `Bash(git *)` l. Warning if bang-commands exist but no `allowed-tools` is declared 3. **Report phase:** For each file: - Print the file path - Print each issue with severity (`ERROR`/`WARN`), line number, and description - Print per-file summary: `N errors, M warnings` - At the end, print total summary across all files 4. **Exit code:** - `0` — no errors (warnings are OK) - `1` — at least one error found - `2` — usage error (bad arguments) ## Dryrun Behaviour When `--dryrun` is passed: - Perform the discovery phase only - Print each SKILL.md path, one per line - Prefix output with `[dryrun] Would validate N files:` - Exit code is always `0` ## Edge Cases | Scenario | Handling | |----------|----------| | No SKILL.md files found | Print "No SKILL.md files found in " and exit 0 | | File is not named SKILL.md (when given directly) | Validate it anyway (user explicitly chose it) | | No frontmatter at all | Report missing `---` delimiter error, skip field checks | | No bang-commands and no allowed-tools | Valid (like the linter skill) | | Bang-commands exist but no allowed-tools declared | Warning: commands won't be pre-authorised | | `$HOME` (without braces) in bang-command | No error — `$HOME` works fine, only `${HOME}` is rejected | | Multi-line `description: >` | Treat as non-empty | | Commands with `\|\| echo "fallback"` | Binary is the first word only | | Bang-commands inside markdown code blocks | Still checked (they execute regardless of markdown context) | ## Examples ### Clean file ``` ── skills/log/SKILL.md ── No issues found. Summary: 1 file checked, 0 errors, 0 warnings ``` ### File with issues ``` ── skills/distill/SKILL.md ── ERROR Line 18: Bang-command uses ${VAR} syntax (use $VAR instead) ERROR Line 22: Bang-command uses $() command substitution WARN Line 8: 'Bash(git *)' is overly broad — use specific subcommands 2 errors, 1 warning Summary: 1 file checked, 2 errors, 1 warning ``` ### Directory scan ``` ── skills/log/SKILL.md ── No issues found. ── skills/reflect/SKILL.md ── No issues found. ── skills/distill/SKILL.md ── ERROR Line 18: Bang-command uses ${VAR} syntax (use $VAR instead) 1 error, 0 warnings Summary: 3 files checked, 1 error, 0 warnings ``` ### Dryrun ``` [dryrun] Would validate 3 files: skills/log/SKILL.md skills/reflect/SKILL.md skills/distill/SKILL.md ```