fix(hooks): use ${CLAUDE_PLUGIN_ROOT} in skill-registered hook commands (narrow refile of #968/#970)#1141
Open
CermakM wants to merge 1 commit intogarrytan:mainfrom
Open
fix(hooks): use ${CLAUDE_PLUGIN_ROOT} in skill-registered hook commands (narrow refile of #968/#970)#1141CermakM wants to merge 1 commit intogarrytan:mainfrom
CermakM wants to merge 1 commit intogarrytan:mainfrom
Conversation
Replaces ${CLAUDE_SKILL_DIR} with ${CLAUDE_PLUGIN_ROOT} in the
PreToolUse hook commands of careful, freeze, guard, and investigate.
Only the 8 hook-command strings are changed — body-level ${CLAUDE_SKILL_DIR}
uses (e.g. investigate's FREEZE_AVAILABLE probe and ship's skill-body cat
commands) are left alone because they render inside skill content, where
CLAUDE_SKILL_DIR is a documented, supported substitution.
Why the change is needed:
Claude Code does not substitute or export ${CLAUDE_SKILL_DIR} for hooks
declared in a skill's frontmatter — only ${CLAUDE_PLUGIN_ROOT} is
available in that context (the Skills doc lists CLAUDE_SKILL_DIR under
string substitutions for skill content / bash injection, not hooks, and
the Hooks doc's environment-variable list omits it entirely). With the
variable undefined at hook-execution time, bash expands it to empty, so
every Bash/Edit/Write tool call while careful/freeze/guard/investigate
is active errors with paths like `/../freeze/bin/check-freeze.sh: No
such file or directory`. The hooks are non-blocking, so tool calls
still succeed — but the destructive-command and freeze-boundary checks
the hooks exist to enforce never actually run for any user.
This is the same fix proposed in garrytan#968 and garrytan#970, both of which were
closed without technical engagement. This PR narrows the change from
those (skill-body uses of CLAUDE_SKILL_DIR are preserved) and includes
independent verification of the root cause.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces
${CLAUDE_SKILL_DIR}with${CLAUDE_PLUGIN_ROOT}in only the 8 frontmatter hookcommand:strings acrosscareful,freeze,guard, andinvestigate. Body-level${CLAUDE_SKILL_DIR}uses (e.g.investigate'sFREEZE_AVAILABLEprobe at line 85,ship's skill-bodycatcommands inscripts/resolvers/review.ts) are preserved — those render inside skill content where${CLAUDE_SKILL_DIR}is a documented, supported substitution.This is a narrower, independently-verified refile of #968 and #970, which were closed with the rationale that gstack "consistently uses CLAUDE_SKILL_DIR". The point this PR hopes to surface cleanly: consistency within hook commands is already broken today, not preserved — Claude Code silently expands
${CLAUDE_SKILL_DIR}to empty in that specific context, and the hooks have been inoperative for every user running/careful,/freeze,/guard, or/investigate.Scope
careful/SKILL.md.tmplcommand:linefreeze/SKILL.md.tmplcommand:linesguard/SKILL.md.tmplcommand:linesinvestigate/SKILL.md.tmplcommand:linesSKILL.mdfiles viabun run gen:skill-docs --host all16 insertions, 16 deletions across 8 files.
Deliberately unchanged (body-level, these work):
investigate/SKILL.md.tmpl:85—FREEZE_AVAILABLEprobe (inside skill body bash block)scripts/resolvers/review.ts:949— renders intoshipbody contenttest/fixtures/golden/*-ship-*.md— golden fixtures for body contentIndependent reproduction
Diagnosed from scratch while debugging a user-reported
PreToolUse:Bash hook errorwith/guardactive. Instrumentedguard/SKILL.mdwith a probe logging every hook-fire's runtime environment:Three hook fires, three identical log lines:
claude_subst=[]— Claude's registration-time substitution yielded emptyshell_set=(empty) — bash's${VAR+yes}test confirms the variable is unset, not empty-stringplugin_root=[...]— same hook, same invocation:${CLAUDE_PLUGIN_ROOT}resolves correctlyEnvironment: Claude Code 2.x native binary on macOS (arm64), gstack installed via
./setupinto~/.claude/skills/gstack.This matches #961's disassembly evidence exactly. The Skills doc (available-string-substitutions) lists
CLAUDE_SKILL_DIRfor skill content; the Hooks doc's environment-variable section omits it entirely and listsCLAUDE_PLUGIN_ROOTas the skill-root anchor for hook contexts.User-visible consequence
Because hooks are non-blocking, tool calls still succeed — the bug presents as transcript noise rather than a hard failure. But the noise is real (one error per Bash/Edit/Write while any affected skill is active), and more consequentially: the destructive-command and freeze-boundary checks these hooks are meant to enforce have never run for any user. A developer running
/carefulbelievesrm -rfis gated; in reality the gate short-circuits before the check script is invoked.Why this narrower scope than #968/#970
The owner's close comment on #968 and #970 read: "the codebase consistently uses CLAUDE_SKILL_DIR, which is the correct variable. These PRs propose switching to CLAUDE_PLUGIN_ROOT but that's not what we use."
This PR takes that seriously.
${CLAUDE_SKILL_DIR}is the correct variable for skill-content substitutions, and those uses are preserved. The change is surgically limited to frontmatter hookcommand:strings, where${CLAUDE_SKILL_DIR}is not available — a distinction between two Claude Code substitution contexts, not a global rename.Testing (per CONTRIBUTING.md)
bun testagainstfix/skill-dir-to-plugin-root(79e67bd) shows identical pass/fail counts toupstream/main(656df0e) on the same commit base. Pre-existing environmental failures (gstack-diff-scopesuite,WorktreeManagersuite,detectBaseBranch) reproduce identically on both branches and are unrelated to this change.bun test test/hook-scripts.test.ts→ 32 pass / 0 fail (the suite that directly coverscheck-careful.sh/check-freeze.sh).bun run skill:check→ all 10 host outputs (Claude Code, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes, GBrain) report fresh.bun run gen:skill-docs --host allproduces only the 4 expected SKILL.md regenerations; no collateral diff.check-freeze.sh/check-careful.shreceive correct paths and execute.References
${CLAUDE_SKILL_DIR}but Claude Code only exposes${CLAUDE_PLUGIN_ROOT}— all skill-registered hooks silently broken #961 (closed) — original well-researched report with Claude Code binary-level evidenceHappy to rebase if main has moved. Happy to drop the scope further or split differently if that helps.
🤖 Co-authored-by: Claude Opus 4.7