|
| 1 | +--- |
| 2 | +title: "Reference bundled skill files by tier: relative for reads, SKILL_DIR anchor for executed scripts" |
| 3 | +date: 2026-06-26 |
| 4 | +category: skill-design |
| 5 | +module: "skills (bundled-script invocation across harnesses)" |
| 6 | +problem_type: convention |
| 7 | +component: tooling |
| 8 | +severity: high |
| 9 | +applies_when: |
| 10 | + - Authoring or reviewing a skill that executes a bundled script via the Bash tool |
| 11 | + - A skill must work on more than one harness (Claude Code, Codex, Cursor, Gemini) |
| 12 | + - "Choosing between a bare relative path, ${CLAUDE_SKILL_DIR}, and a model-filled SKILL_DIR anchor" |
| 13 | + - Generalizing an empirical harness finding into a broad authoring rule |
| 14 | +tags: |
| 15 | + - bundled-scripts |
| 16 | + - path-resolution |
| 17 | + - skill-authoring |
| 18 | + - cross-harness |
| 19 | + - skill-dir |
| 20 | + - bash-tool |
| 21 | + - claude-skill-dir |
| 22 | + - empirical-validation |
| 23 | +related_components: |
| 24 | + - development_workflow |
| 25 | + - documentation |
| 26 | +--- |
| 27 | + |
| 28 | +# Reference bundled skill files by tier: relative for reads, SKILL_DIR anchor for executed scripts |
| 29 | + |
| 30 | +## Context |
| 31 | + |
| 32 | +Skills bundle files the agent uses at runtime: reference docs, schemas, and `scripts/`. Getting paths to those files to resolve correctly across harnesses (Claude Code, Codex, Cursor) has been a recurring bug class — see closed issues #764 (`ce-worktree`), #811 (`ce-code-review`), #898 (`ce-compound`), all "script path resolved against the project root, not the skill dir," plus the still-open #944 ("reconcile contradictory AGENTS.md guidance on bare relative paths") and #949 (a live prose-reference variant). |
| 33 | + |
| 34 | +The trigger for this learning was a wrong turn. An empirical finding — *the Bash tool's working directory is the user's project root, not the skill directory, on Claude Code, Codex, and Cursor* — was over-generalized into "bare relative paths like `bash scripts/x.sh` are broken, so anchor every invocation." That conclusion was codified and acted on before it was checked against the cross-tool skill spec or any independent implementation. It turned out to be wrong on the strong form: bare relative paths work fine in practice. The correction produced the tiered model below, and a transferable lesson about validating findings before codifying them. |
| 35 | + |
| 36 | +## Guidance |
| 37 | + |
| 38 | +Pick the reference form by **what the agent does with the file**, in three tiers. |
| 39 | + |
| 40 | +**Tier 1 — Read-time file references (the agent *reads* a co-located file into context, e.g. `references/*.md`).** Bare relative path from the skill root, no anchor — the skill loader resolves these against the skill directory on every major harness (the form AGENTS.md Tier 1 codifies). The line vs Tier 2: reading a reference *into context* is Tier 1; the moment the file is used in an *action the agent performs* (copy it, pass it as an argument, execute it) it becomes Tier 2 and takes the cue. Open caveat (#949): if a `Read references/X` is ever observed to resolve against the project CWD and miss on a target, treat that read as Tier 2 and add the "from this skill's directory" cue. |
| 41 | + |
| 42 | +``` |
| 43 | +Read `references/schema.yaml` and validate frontmatter against it. |
| 44 | +``` |
| 45 | + |
| 46 | +**Tier 2 — Prose pointers to a bundled file the agent acts on but does *not* execute** (a template to copy, a file to inspect). Bare relative path **plus an explicit "from this skill's directory" cue**, so the agent resolves it against the skill dir rather than the project CWD. |
| 47 | + |
| 48 | +``` |
| 49 | +Copy `scripts/hook.sh` from this skill's directory into `.claude/hooks/`. |
| 50 | +``` |
| 51 | + |
| 52 | +**Tier 3 — Executed shell commands** (fenced ```bash``` blocks *or* inline `bash …` / `python …` the agent runs through the Bash tool). Use the **model-filled `SKILL_DIR` anchor**, set inline in the same command (shell state does not persist between separate Bash-tool calls): |
| 53 | + |
| 54 | +```bash |
| 55 | +SKILL_DIR="<absolute path of the directory containing the SKILL.md you just read>" |
| 56 | +bash "$SKILL_DIR/scripts/measure.sh" "$ARG" |
| 57 | +``` |
| 58 | + |
| 59 | +This is the conservative **house default** for executed shell — not because bare relative "fails," but because it bakes resolution into the command so a fenced block copied verbatim into a Bash call cannot miss, regardless of harness or model version. |
| 60 | + |
| 61 | +Two adjacent rules: |
| 62 | + |
| 63 | +- **Avoid `${CLAUDE_SKILL_DIR}`.** It is a Claude-Code-only SKILL.md *content* substitution (not an env var) and is empty on every other host. A `${CLAUDE_SKILL_DIR}`-guarded call's `then` branch then silently never fires off-Claude — a genuine silent skip. This plugin ships as a *native* Codex plugin (the converter is not in that path), so a Claude-only mechanism is a footgun, not a neutral fallback. The model-filled `SKILL_DIR` anchor works on every host with no downside. (Narrow exception: behavior that is genuinely Claude-only and will never run elsewhere — essentially never, given the cross-host install model.) |
| 64 | +- **A script that needs its *own* directory** (to open a sibling file from inside the process) derives it from `BASH_SOURCE`, not `SKILL_DIR` — `SKILL_DIR` is the orchestrator's shell variable and is not exported to the child process. |
| 65 | + |
| 66 | +### Where this is codified |
| 67 | + |
| 68 | +`AGENTS.md` > "Platform-Specific Variables in Skills" codifies this three-tier model as the repo's authoring rule; this learning is the rationale and worked examples behind it. A few legacy `${CLAUDE_SKILL_DIR}` uses are still being migrated to the anchor (e.g. `ce-compound`'s `validate-frontmatter.py`), tracked by #944. Note `tests/skill-conventions.test.ts` enforces an existence guard only when a skill-dir *platform var* is used — it does not forbid the model-filled `SKILL_DIR` anchor (which uses no platform var), so anchor-based skills pass it. |
| 69 | + |
| 70 | +## Why This Matters |
| 71 | + |
| 72 | +There are two payoffs, and the second is the larger one. |
| 73 | + |
| 74 | +First-order: correctness. A bundled script that silently no-ops on a non-Claude host, or resolves the wrong path, fails quietly in a user's environment and is hard to catch in review. The tier rules remove that class of failure while keeping the simple cases simple. |
| 75 | + |
| 76 | +Second-order — the meta-lesson: **a single empirical finding is not an authoring rule until it is validated against the spec and independent implementations.** "The shell's CWD is the project root" is a true, narrow *harness fact*. It was inflated into "bare relative is broken" without checking the actual *resolution mechanism* — which is the agent, not the shell. The agentskills.io spec says it directly: relative script paths work because "the agent resolves these paths automatically." Conflating "where the shell starts" with "who resolves the path" produced guidance that diverged from the ecosystem and added unnecessary machinery. Distinguish the harness fact from the resolution mechanism, and confirm any broad rule against the cross-tool spec plus two or three real skills before codifying it. Vendor docs alone do not settle a cross-harness question — they are platform-centric by construction (e.g., Anthropic's Claude Code docs recommend `${CLAUDE_SKILL_DIR}`, which is exactly the non-portable form to avoid here). |
| 77 | + |
| 78 | +The evidence that corrected the over-generalization, for the record: the agentskills.io spec ships bare `bash scripts/validate.sh` as its canonical example; obra/superpowers' `brainstorming` skill runs bare `scripts/start-server.sh` with no anchor across four named platforms; its `subagent-driven-development` skill pairs a bare relative path with a "from this skill's directory" cue (Tier 2); mattpocock/skills sidesteps in-place execution entirely (copies a hook to `.claude/hooks/`, or ships a `.template.sh` referenced in prose); and `last30days` adopts the explicit `SKILL_DIR` anchor for its critical multi-host engine — *after* a path-resolution regression. The tiers reconcile all of these: relative is the ecosystem norm and works via agent resolution; the anchor is the determinism upgrade reserved for executed shell. |
| 79 | + |
| 80 | +## When to Apply |
| 81 | + |
| 82 | +- Whenever a SKILL.md or a reference file tells the agent to run a bundled script through the Bash tool (Tier 3). |
| 83 | +- When adding files under a skill's `scripts/` that are *executed* rather than *read*. |
| 84 | +- When reviewing or migrating a skill that uses `${CLAUDE_SKILL_DIR}` guards — check whether they silently no-op off-Claude and move executed-shell calls to the anchor. |
| 85 | +- When weighing "inline logic" vs. "bundled script" for portability — the tiers remove the old fear that bundled scripts are inherently fragile cross-harness; just give executed scripts Tier-3 treatment. |
| 86 | +- Do **not** apply the anchor to Tier 1 or Tier 2 references — it adds noise without benefit there. |
| 87 | + |
| 88 | +## Examples |
| 89 | + |
| 90 | +Tier 3 — executed script, before and after: |
| 91 | + |
| 92 | +```bash |
| 93 | +# Before: bare relative in a fenced block. Works via agent resolution, but a |
| 94 | +# verbatim-copied block resolves against the project root and misses. |
| 95 | +bash scripts/measure.sh "$TARGET" |
| 96 | +``` |
| 97 | + |
| 98 | +```bash |
| 99 | +# After: model-filled anchor, deterministic regardless of harness/model. |
| 100 | +SKILL_DIR="<absolute path of the directory containing the SKILL.md you just read>" |
| 101 | +bash "$SKILL_DIR/scripts/measure.sh" "$TARGET" |
| 102 | +``` |
| 103 | + |
| 104 | +Anti-pattern — the `${CLAUDE_SKILL_DIR}` existence guard (silently no-ops on Codex/Cursor): |
| 105 | + |
| 106 | +```bash |
| 107 | +# AVOID — `then` branch never fires off-Claude, where ${CLAUDE_SKILL_DIR} is unset. |
| 108 | +if [ -n "${CLAUDE_SKILL_DIR}" ] && [ -f "${CLAUDE_SKILL_DIR}/scripts/x.sh" ]; then |
| 109 | + bash "${CLAUDE_SKILL_DIR}/scripts/x.sh" |
| 110 | +else |
| 111 | + echo "script unavailable" |
| 112 | +fi |
| 113 | +``` |
| 114 | + |
| 115 | +Tier 1 — read-time reference, already correct, no anchor needed: |
| 116 | + |
| 117 | +``` |
| 118 | +Read `references/schema.yaml` and apply its field definitions. |
| 119 | +``` |
| 120 | + |
| 121 | +## Related |
| 122 | + |
| 123 | +- [`script-first-skill-architecture.md`](script-first-skill-architecture.md) — *whether* to offload work to a bundled script (token cost). This doc is the companion *how to invoke it* once you have. Low overlap, complementary. |
| 124 | +- [`pass-paths-not-content-to-subagents.md`](pass-paths-not-content-to-subagents.md) — path-passing for orchestrator->subagent dispatch; a different "paths" problem (token efficiency, not CWD resolution). |
| 125 | +- [`../best-practices/prefer-python-over-bash-for-pipeline-scripts.md`](../best-practices/prefer-python-over-bash-for-pipeline-scripts.md) — which language to write a bundled script in. |
| 126 | +- `AGENTS.md` > "Platform-Specific Variables in Skills" — codifies this three-tier model as the repo's authoring rule; this doc is its rationale and worked examples. Its Tier 1 (read-time references) and Tier 2 (prose pointers + cue) address the prose-reference bug class in #949. |
| 127 | +- Issues: #944 (open — audit/reconcile bundled-script invocation guidance; this learning informs it), #949 (open — live Tier-2 prose-reference miss), #943/#898/#811/#764 (closed — the Tier-3 origin bug class). |
0 commit comments