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.


formClaude CodepiAgent Skills specSkills API exportuse it for
references/x.mdresolves (skill dir)resolvesresolvesresolvesthe skill's own files (405 refs after the codemod)
${CLAUDE_SKILL_DIR}/…expandsliteralliteralliteralexec shapes only, where the shell cwd is not the skill dir (19 refs)
${CLAUDE_PLUGIN_ROOT}/…expandsliteralliteralliteralcross-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.


shapebeforeafterwhat happened
same-skill, read shape (Read(), link, backtick, prose)4050bare relative <path>
same-skill, exec shape (bash, node, python3, scriptPath, H=)190${CLAUDE_SKILL_DIR}/<path> (15 command tokens plus 4 scriptPath or assignment values inside fences)
same-skill, own SKILL.md load path for agents11kept: agents keep the plugin-root form (#3313); both gates exempt it
cross-skill2222kept
shared/2929kept
frontmatter hooks: command:1919kept (not body text)
other plugin-root paths22kept
total497 in 58 files73 in 22 files424 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).