Checks for common issues that silently break skills — non-ASCII frontmatter, $VAR in paths, uncovered binaries in allowed-tools, etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
4.2 KiB
Markdown
128 lines
4.2 KiB
Markdown
# 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] <PATH>
|
|
```
|
|
|
|
### 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 <path>" 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
|
|
```
|