claude-code - 💡(How to fix) Fix Skills: `!`shell`` substitution is single-pass — blocks hierarchical/composable skill authoring

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The !`<command>` preprocessing in SKILL.md files runs as a single pass over the original file. Output of one substitution is never re-scanned, so a shared fragment that itself contains !`...` placeholders cannot be composed into another skill. This breaks the most natural pattern for hierarchical skill composition.

Per the skills doc, the mechanism is described as a preprocessing step but the docs are silent on whether substitution is recursive. Empirically: it is not.

Root Cause

The !`<command>` preprocessing in SKILL.md files runs as a single pass over the original file. Output of one substitution is never re-scanned, so a shared fragment that itself contains !`...` placeholders cannot be composed into another skill. This breaks the most natural pattern for hierarchical skill composition.

Per the skills doc, the mechanism is described as a preprocessing step but the docs are silent on whether substitution is recursive. Empirically: it is not.

Code Example

---
name: recursion-test-base
description: level 0
---
LEVEL_0_START
!`tail -n +6 /etc/hostname`
LEVEL_0_END

---

---
name: recursion-test-combined
description: level 1 — cats base
---
LEVEL_1_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-base/SKILL.md`
LEVEL_1_END

---

---
name: recursion-test-top
description: level 2 — cats combined
---
LEVEL_2_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-combined/SKILL.md`
LEVEL_2_END

---

LEVEL_2_START
LEVEL_1_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-base/SKILL.md`
LEVEL_1_END
LEVEL_2_END
RAW_BUFFERClick to expand / collapse

Summary

The !`<command>` preprocessing in SKILL.md files runs as a single pass over the original file. Output of one substitution is never re-scanned, so a shared fragment that itself contains !`...` placeholders cannot be composed into another skill. This breaks the most natural pattern for hierarchical skill composition.

Per the skills doc, the mechanism is described as a preprocessing step but the docs are silent on whether substitution is recursive. Empirically: it is not.

Repro

Three skill files in ~/.claude/skills/:

recursion-test-base/SKILL.md

---
name: recursion-test-base
description: level 0
---
LEVEL_0_START
!`tail -n +6 /etc/hostname`
LEVEL_0_END

recursion-test-combined/SKILL.md

---
name: recursion-test-combined
description: level 1 — cats base
---
LEVEL_1_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-base/SKILL.md`
LEVEL_1_END

recursion-test-top/SKILL.md

---
name: recursion-test-top
description: level 2 — cats combined
---
LEVEL_2_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-combined/SKILL.md`
LEVEL_2_END

Invoke /recursion-test-top.

Expected

The model receives a fully resolved blob — the tail in combined's body re-runs after combined is inlined into top, and the tail in base's body re-runs after base is inlined into combined.

Actual

LEVEL_2_START
LEVEL_1_START
!`tail -n +6 /Users/me/.claude/skills/recursion-test-base/SKILL.md`
LEVEL_1_END
LEVEL_2_END

The outer tail ran. The cat'd content's own !`tail` came through as literal text to the model.

Use case: composable / hierarchical skill authoring

Skills are growing into a serious authoring surface, and the same authoring problems that drove #include, ES module imports, and Jinja {% include %} show up:

PatternWhy you want it
Shared preamble"Always read these 3 docs first" — one fragment included by 20 skills; one place to edit.
Role/persona libraryA personas/senior-reviewer.md fragment composed into every review-style skill.
Tool-context bundleA fragment that itself dynamically lists relevant files via !`git diff --stat`, included by any skill that needs current-branch context.
Layered domain skillslanguage/cpp.md extends language/base.md; project/njin-cpp.md extends language/cpp.md. Each layer adds rules.
Per-org policy fragmentOne canonical org-policy.md with dynamic content (e.g. !`cat ~/.config/org-version`), composed into every project's skills.

Today the only way to compose is to flatten everything into the bash command itself — fine for static cat, but it defeats the purpose for fragments that themselves want dynamic content.

Suggested behaviors (in order of preference)

  1. Recursively expand until fixed point, with a depth/iteration cap (e.g. 8) to bound runaway expansion. Cycles would be detectable via a visited-set of resolved fragments.
  2. Explicit opt-in syntax for "include this file and continue preprocessing it," e.g. !!`cat fragment.md` or @include fragment.md. Keeps single-pass default; opt-in to deeper resolution.
  3. At minimum, document the current single-pass behavior in the skills page so authors know the boundary up front and can choose to flatten in their bash command.

Related observations from the same investigation

  • !`...` only triggers when ! is at line start (or after whitespace). FOO=!bar`` is passed through as literal. This is not documented and is surprising — variables, assignments, and inline annotations all break silently. Worth either documenting or relaxing.
  • The kill-switch disableSkillShellExecution replaces with [shell command execution disabled by policy] — but a denied/failed-to-find command appears to pass through silently as literal. Failure mode is invisible to the author.

Environment

  • Claude Code: 2.1.143
  • OS: macOS (Darwin 25.2.0)
  • Shell: bash (default)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Skills: `!`shell`` substitution is single-pass — blocks hierarchical/composable skill authoring