[BUG] ${CLAUDE_SKILL_DIR} not substituted in SKILL.md frontmatter `allowed-tools` patterns

Resolved 💬 1 comment Opened May 1, 2026 by AaronValentino Closed May 4, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The Available string substitutions docs list ${CLAUDE_SKILL_DIR} as available for skills:

${CLAUDE_SKILL_DIR} — The directory containing the skill's SKILL.md file. For plugin skills, this is the skill's subdirectory within the plugin, not the plugin root. Use this in bash injection commands to reference scripts or files bundled with the skill, regardless of the current working directory.

However, ${CLAUDE_SKILL_DIR} is not substituted when used in allowed-tools patterns in SKILL.md frontmatter. The literal ${CLAUDE_SKILL_DIR} string is kept in the permission pattern, so it never matches the actual command (which has the resolved absolute path), and the user is re-prompted on every invocation.

${CLAUDE_PLUGIN_ROOT} in the same field works correctly — patterns containing it auto-allow without prompting. So the substitution gap is specific to ${CLAUDE_SKILL_DIR}.

This is distinct from #36135, which covers the same variable in hooks: frontmatter. Same root cause (substitution scope gap), different field, different symptom (re-prompt loop vs. silent shell failure).

What Should Happen?

${CLAUDE_SKILL_DIR} should be substituted in allowed-tools patterns the same way ${CLAUDE_PLUGIN_ROOT} is — and the same way ${CLAUDE_SKILL_DIR} itself is in skill body prose and !-prefixed context-injection commands.

Steps to Reproduce

  1. Create a plugin skill with two bundled scripts:

``
my-plugin/
├── .claude-plugin/plugin.json
└── skills/my-skill/
├── SKILL.md
└── scripts/
├── via-plugin-root.sh # chmod +x; echo "plugin-root"
└── via-skill-dir.sh # chmod +x; echo "skill-dir"
``

  1. In SKILL.md frontmatter, declare one allowed-tools pattern per variable, each scoped to its own script so there's no overlap:

~~~yaml
---
name: my-skill
description: Repro for \${CLAUDE_SKILL_DIR} substitution gap in allowed-tools
user-invocable: true
allowed-tools:

  • Bash(${CLAUDE_PLUGIN_ROOT}/skills/my-skill/scripts/via-plugin-root.sh)
  • Bash(${CLAUDE_SKILL_DIR}/scripts/via-skill-dir.sh)

---

Run both bundled scripts.

``bash
${CLAUDE_PLUGIN_ROOT}/skills/my-skill/scripts/via-plugin-root.sh
${CLAUDE_SKILL_DIR}/scripts/via-skill-dir.sh
``
~~~

  1. Install the plugin and invoke /my-skill.
  1. Observe:
  • The first script (via-plugin-root.sh) executes silently — its allowed-tools pattern matches.
  • The second script (via-skill-dir.sh) triggers a permission prompt every time, even though its allowed-tools pattern is structurally identical apart from the variable used.

Expected: Both scripts auto-allow.
Actual: Only the ${CLAUDE_PLUGIN_ROOT} pattern auto-allows; the ${CLAUDE_SKILL_DIR} pattern is treated as a literal string in the permission engine and never matches the resolved command path.

Why This Matters

${CLAUDE_SKILL_DIR} is the documented, ergonomic way to reference a skill's bundled files without hardcoding the skill name. Forcing skill authors to fall back to ${CLAUDE_PLUGIN_ROOT}/skills/<skill-name>/scripts/... in allowed-tools while using ${CLAUDE_SKILL_DIR} everywhere else creates an inconsistent authoring experience and is easy to get wrong (the skill silently re-prompts users on every call).

Related Issues

  • #36135 — same variable, same scope-gap pattern, in hooks: frontmatter (still open)
  • #9354 — same shape with ${CLAUDE_PLUGIN_ROOT} in command markdown
  • #30578 — closed; docs were missing ${CLAUDE_SKILL_DIR} from the substitutions table

Claude Code Version

2.1.121

Platform

Anthropic API

Operating System

macOS 26.3.1

Terminal/Shell

VS Code integrated terminal

Is this a regression?

I don't know — first observed in 2.1.121.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗