Three path forms, one that every client resolves
Issue #3822, steps 2 and 3. A SKILL.md body is loaded by Claude Code, by pi, by the Claude Skills API export and by the Cursor command wrappers. Only Claude Code expands a ${CLAUDE_*} placeholder; the others hand the model the literal string, silently. 425 same-skill references were on that form.
What the model receives
Pick a client and a form. The resolution row is measured, not assumed: Claude Code 2.1.251 with a throwaway plugin asked for three nonces from /tmp and from the skill dir (rel=1 sdir=1 cpr=1 both times); pi 0.84.4 loaded ork:verify and returned the placeholder unexpanded.
| form | Claude Code | pi | Agent Skills spec | Skills API export | use it for |
|---|---|---|---|---|---|
references/x.md | resolves (skill dir) | resolves | resolves | resolves | the skill's own files (405 refs after the codemod) |
${CLAUDE_SKILL_DIR}/… | expands | literal | literal | literal | exec shapes only, where the shell cwd is not the skill dir (19 refs) |
${CLAUDE_PLUGIN_ROOT}/… | expands | literal | literal | literal | cross-skill, shared/, frontmatter hooks: command: (72 refs, a plugin root is genuinely needed) |
Census, before and after
Counted on this tree with one regex over src/skills/*/SKILL.md, classified by what the path points at. The issue's census at cd2bfdc96 read 479 in 58 files (405 same-skill); the tree had drifted to 497 by ff0066fa1.
| shape | before | after | what happened |
|---|---|---|---|
| same-skill, read shape (Read(), link, backtick, prose) | 405 | 0 | bare relative <path> |
| same-skill, exec shape (bash, node, python3, scriptPath, H=) | 19 | 0 | ${CLAUDE_SKILL_DIR}/<path> (15 command tokens plus 4 scriptPath or assignment values inside fences) |
| same-skill, own SKILL.md load path for agents | 1 | 1 | kept: agents keep the plugin-root form (#3313); both gates exempt it |
| cross-skill | 22 | 22 | kept |
shared/ | 29 | 29 | kept |
frontmatter hooks: command: | 19 | 19 | kept (not body text) |
| other plugin-root paths | 2 | 2 | kept |
| total | 497 in 58 files | 73 in 22 files | 424 rewritten, 6 commits of at most 10 skills |
The rule, and the gates that now hold it
node scripts/codemod/skill-paths-relative.mjs [--dry-run] [--check] [--skills a,b] [--limit N]
Per occurrence of ${CLAUDE_PLUGIN_ROOT}/skills/<self>/<rest> inside that skill's own SKILL.md: a read shape becomes <rest>; an exec shape (command token, shell assignment, scriptPath, or any non-Read value inside a code fence) becomes ${CLAUDE_SKILL_DIR}/<rest>; a directory target gets a trailing slash; the skill's own SKILL.md is skipped. Frontmatter is never touched. Idempotent: --check exits 1 only if something is left.
tests/skills/structure/test-placeholder-validity.sh section 4 was a count while the codemod was pending; it fails now. scripts/eval/conformance-check.mjs C3c dropped its opt-in. scripts/check-skill-delta.mjs --json is byte-identical before and after (orphan detection keys on file stems, which a relative path still carries).