[BUG] `claude plugin validate` passes manifests whose hook commands reference files that don't exist

Open 💬 0 comments Opened Jul 15, 2026 by muloka

Summary

claude plugin validate does not check that a hook's command resolves to an existing file. A manifest referencing a script that isn't there validates clean and exits 0 — the hook then silently fails at runtime.

Repro

mkdir -p /tmp/vt/p/.claude-plugin /tmp/vt/p/scripts

cat > /tmp/vt/p/.claude-plugin/plugin.json <<'JSON'
{ "name": "vt", "description": "d", "version": "0.1.0",
  "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [
    { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/DOES-NOT-EXIST.sh" } ] } ] } }
JSON

claude plugin validate /tmp/vt/p

/tmp/vt/p/scripts/ is empty. DOES-NOT-EXIST.sh is not there.

Actual

⚠ Found 1 warning:

  ❯ author: No author information provided. Consider adding author details for plugin attribution

✔ Validation passed with warnings

Exit code 0. The dangling reference is never mentioned — the only warning is about an unrelated missing author field.

Expected

An error, or at minimum a warning, naming the unresolvable path. A hook whose command doesn't exist is not a working plugin, and this is exactly the class of thing a manifest validator is for: the reference is checkable statically, cheaply, at validate time.

Why this matters

The failure is silent in both directions. Validation is green, and at runtime the hook just doesn't fire — no error surfaces to the user, so a plugin whose safety hooks have quietly stopped registering looks identical to one that's working.

I hit this from the adjacent direction: while adding a version field to six plugins, a jq '{name, description, version, author}' template silently dropped the hooks block from four of them — including a permission-gateway plugin's registration of its own PreToolUse gate. claude plugin validate returned ✔ on all six. That specific case isn't a bug (a plugin with no hooks is legitimately valid, and the validator has no baseline to diff against), but chasing it surfaced the case above, which is: the reference is right there in the manifest and is never checked.

I was one green checkmark away from shipping a change that disabled every safety hook in a repo. What caught it was reading the diff by hand.

Scope

Verified against hook command fields. I have not checked whether the same gap applies to other path-bearing fields (mcpServers command/args, lspServers, commands/agents/skills directory overrides) — plausibly the same, but I'm only reporting what I ran.

Possibly related: #66987 (lspServers entries silently dropped — schema-valid, no diagnostic) is the same shape in a different field.

Environment

  • claude --version: 2.1.210
  • macOS (darwin 25.5.0)

View original on GitHub ↗