anthropics / anthropics/claude-plugins-official
docs(plugin-dev): hook-command examples teach unquoted ${CLAUDE_PLUGIN_ROOT}; bundled validate-hook-schema.sh green-lights it and errors out on the plugin hooks.json shape
- Dominant language
- Python
- Stars
- 36.3k
- Forks
- 4.1k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 539
Description
## Summary
Two defects with one root cause: the `plugin-dev` skills teach an unquoted `${CLAUDE_PLUGIN_ROOT}` inside hook `command` strings, and the validator bundled with those same skills cannot catch it.
Unquoted, the shell word-splits the expansion. On any surface whose plugin root contains a space — notably Claude Desktop, where it lives under `~/Library/Application Support/…` — bash receives a truncated path (`/Users//Library/Application`) and **the hook exits 127 without ever running**. It blocks nothing and reports nothing. The CLI is unaffected because its plugin root has no space, which is why this survives undetected.
We hit this at scale in our own plugin trees: **6,125 silent hook failures** across our transcripts shared this exact signature (exit 127 / `claude-desktop` / `Library/Application:`) — 4,467 PreToolUse, 160 PostToolUse, 1,498 UserPromptSubmit. Among them, four deterministic *safety* guards (blocking direct pushes to protected branches, enforcing prod read-only, blocking PR merges) each failed to execute 82 times. Within an affected session it failed on every prompt — deterministic, not intermittent, and completely invisible.
## 1. The docs teach the unquoted form, with no counterexample
**27 occurrences in JSON `"command":` position across 11 files**, all in `plugin-dev` (skills `hook-development` 11, `plugin-structure` 10, `mcp-integration` 6). Prose mentions of the variable are fine and excluded from that count.
The most damaging one is the canonical example a reader copies — `skills/hook-development/SKILL.md`, in the block introduced as *"This is the **plugin-specific format**"* / *"**Example:**"*:
```json
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
```
Others in the same file: lines 49, 251, 336, 374, 616, e.g.
```json
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
```
**There is no correctly-quoted hook-command example anywhere in the docs** — so a reader has nothing to copy but the broken form. Two things in the same repo already show the convention is known:
- `plugins/ralph-loop/hooks/hooks.json` ships it **correctly quoted**: `"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh\""` — your own shipped runtime artifact contradicts your own docs.
- `plugins/plugin-dev/skills/plugin-structure/SKILL.md:300` already writes `source "${CLAUDE_PLUGIN_ROOT}/lib/common.sh"` — quoted, just never applied to hook commands.
**Fix:** quote the path in all 27 command-position examples — `bash "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"` — and state the reason once (word-splitting on roots containing spaces; single quotes are *not* an alternative, they suppress expansion entirely).
## 2. `validate-hook-schema.sh` green-lights the bug, and cannot read the documented shape
`skills/hook-development/scripts/validate-hook-schema.sh` looks like it would catch this. It does not.
**2a. It has no quoting check.** Its only assertion about `command` content warns on hardcoded absolute paths (line 112). Given a file in the root-level event shape it understands, containing an unquoted variable, it reports success:
```console
$ cat rootshape.json
{ "PreToolUse": [ { "matcher": "Bash", "hooks": [
{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" } ] } ] }
$ bash validate-hook-schema.sh rootshape.json
Validating individual hooks...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ All checks passed!
$ echo $?
0
```
**2b. Against a plugin `hooks.json` it validates zero hooks and exits 5.** It assumes event names at the JSON root, so the documented `{"description": …, "hooks": {…}}` plugin shape makes it emit "Unknown event type" per root key and then die on a `jq` type error:
```console
$ bash validate-hook-schema.sh plugins//hooks/hooks.json
Checking root structure...
⚠️ Unknown event type: description
⚠️ Unknown event type: hooks
✅ Root structure valid
Validating individual hooks...
jq: error (at plugins//hooks/hooks.json:60): Cannot index string with number
$ echo $?
5
```
Note it prints `✅ Root structure valid` immediately after failing to recognise either root key. It also hard-fails on the `**Example:**` block printed 44 lines earlier in its own skill (`SKILL.md` lines 84-99), extracted verbatim.
**Fix:** accept both root shapes, and add the quoting assertion — reject an unquoted `${CLAUDE_PLUGIN_ROOT}` in a `command`, and reject single quotes around it too.
## Why this is worth fixing at the docs/validator layer
There are already ~20 issues in this repo reporting the *runtime* symptom in individual plugins (e.g. #1524, on unquoted `${CLAUDE_PLUGIN_ROOT}` in `cockroachdb/hooks/hooks.json`). Each is a duplicate of a defect the docs keep re-seeding: the guidance teaches the broken form and the bundled validator certifies it. Fixing the examples and the validator closes the source rather than the instances.
For reference, the check we ended up writing after being bitten: it asserts every `${CLAUDE_PLUGIN_ROOT}` in a hook `command` is double-quoted (rejecting single quotes), that each referenced script exists *and is git-tracked*, and that dispatcher fan-out targets resolve. All of these are silent failures — a hook that does not run blocks nothing and reports nothing — so they need to be mechanical, not documented.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with plugins/plugin-dev/skills/hook-development/SKILL.md and its scripts/validate-hook-schema.sh, then compare the documented plugin hooks.json shape with the root-level event shape and the correctly quoted plugins/ralph-loop/hooks/hooks.json example. Run the validator against both sample shapes. Done means all 27 command examples quote the variable, the reason is documented, and the validator accepts both shapes while rejecting unsafe quoting.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bash
- Domain
- documentation, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100