diff --git a/.agents/skills/skill-host-compat/SKILL.md b/.agents/skills/skill-host-compat/SKILL.md index 6597dad54..d790a56ed 100644 --- a/.agents/skills/skill-host-compat/SKILL.md +++ b/.agents/skills/skill-host-compat/SKILL.md @@ -26,7 +26,9 @@ sees `.agents/skills/`. This script is the cross-tree gate: 1. Claude-only substitutions in executable `bash` / `sh` / `zsh` fences (`$CLAUDE_SESSION_ID`, `$CLAUDE_SKILL_DIR`, `$CLAUDE_PLUGIN_ROOT`, - `$ARGUMENTS`) fail. Codex leaves those tokens as literal text. + `$CLAUDE_PROJECT_DIR`, `$ARGUMENTS`) fail. Codex leaves those tokens + as literal text. In a Codex copy, rewrite `$CLAUDE_PROJECT_DIR` to + `$(pwd)` or an explicit path. 2. A Codex copy may only use `name`, `description`, `metadata`, `license`, and `allowed-tools`. `origin: ECC` may stay on the canonical `skills/` copy and must be stripped on the Codex copy. @@ -75,6 +77,7 @@ forbidden pattern: echo "$CLAUDE_SESSION_ID" ls "$CLAUDE_SKILL_DIR" node "$CLAUDE_PLUGIN_ROOT/scripts/setup.js" $ARGUMENTS +cd "$CLAUDE_PROJECT_DIR" ``` Do not add `version:` or `origin:` to a Codex copy under `.agents/skills/`. @@ -86,7 +89,9 @@ every `skills/` entry to have a Cursor copy. - Keep canonical `origin: ECC` if the CONTRIBUTING template calls for it. - Drop `origin`, `version`, and `argument-hint` on the Codex copy. - Show host-portable commands in bash fences (`npx`, `ecc`, or a resolved - root). Mention Claude env vars in prose, not in executable fences. + root). Rewrite `$CLAUDE_PROJECT_DIR` in Codex copies to `$(pwd)` or an + explicit path. Mention other Claude env vars in prose, not in executable + fences. - Run this script before `npm test` when the change is a new skill. - Use `--inventory` to see checklist warning volume without failing the tree. diff --git a/commands/skill-host-compat.md b/commands/skill-host-compat.md index f30604a24..c9ebab928 100644 --- a/commands/skill-host-compat.md +++ b/commands/skill-host-compat.md @@ -36,5 +36,5 @@ node "$ECC_ROOT/scripts/ci/validate-skill-host-compat.js" --inventory 1. Run `scripts/ci/validate-skill-host-compat.js` from the ECC root 2. Show ERROR and WARN lines to the user -3. For substitution errors, move Claude env vars out of bash fences +3. For substitution errors, move Claude env vars out of bash fences. Rewrite `$CLAUDE_PROJECT_DIR` to `$(pwd)` or an explicit path 4. For Codex-key errors, drop `origin` / `version` / `argument-hint` on the copy diff --git a/scripts/ci/validate-skill-host-compat.js b/scripts/ci/validate-skill-host-compat.js index 751b361cc..8dfc3a980 100644 --- a/scripts/ci/validate-skill-host-compat.js +++ b/scripts/ci/validate-skill-host-compat.js @@ -5,7 +5,9 @@ * Checks: * 1. Claude-only substitutions in executable bash/sh/zsh fences * (${CLAUDE_SESSION_ID}, ${CLAUDE_SKILL_DIR}, ${CLAUDE_PLUGIN_ROOT}, - * $ARGUMENTS). Codex and a bare shell leave those tokens literal. + * ${CLAUDE_PROJECT_DIR}, $ARGUMENTS). Codex and a bare shell leave + * those tokens literal. Rewrite ${CLAUDE_PROJECT_DIR} in Codex copies + * to $(pwd) or an explicit path. * 2. Codex-copy frontmatter allowlist on .agents/skills//SKILL.md * (name, description, metadata, license, allowed-tools). origin/version * may remain on the canonical skills/ copy. @@ -47,6 +49,8 @@ const CLAUDE_SUBSTITUTION_PATTERNS = [ { token: '$CLAUDE_SKILL_DIR', regex: /\$CLAUDE_SKILL_DIR\b/g }, { token: '${CLAUDE_PLUGIN_ROOT}', regex: /\$\{CLAUDE_PLUGIN_ROOT\}/g }, { token: '$CLAUDE_PLUGIN_ROOT', regex: /\$CLAUDE_PLUGIN_ROOT\b/g }, + { token: '${CLAUDE_PROJECT_DIR}', regex: /\$\{CLAUDE_PROJECT_DIR\}/g }, + { token: '$CLAUDE_PROJECT_DIR', regex: /\$CLAUDE_PROJECT_DIR\b/g }, { token: '$ARGUMENTS', regex: /\$ARGUMENTS\b|\$\{ARGUMENTS\}/g }, // Claude skill form ${1}. Bare $1 is handled in findSubstitutionTokens so // POSIX `VAR=$1` and Perl/awk `$1 < 80` are not treated as Claude args. @@ -231,6 +235,14 @@ function pushFinding(findings, finding) { findings.push(finding); } +function substitutionMessage(skillName, fenceLang, token) { + const base = `${relSkillMd(skillName)} - ${fenceLang} fence contains Claude-only substitution ${token}`; + if (token === '${CLAUDE_PROJECT_DIR}' || token === '$CLAUDE_PROJECT_DIR') { + return `${base}; rewrite to $(pwd) or an explicit path`; + } + return base; +} + function inspectCanonicalSkill(skillName, content, findings) { const { frontmatter, body } = stripFrontmatter(content); const values = frontmatter.present ? inspectFrontmatter(frontmatter.lines).values : {}; @@ -289,7 +301,7 @@ function inspectCanonicalSkill(skillName, content, findings) { code: 'substitution', skill: skillName, token, - message: `${relSkillMd(skillName)} - ${fence.lang} fence contains Claude-only substitution ${token}`, + message: substitutionMessage(skillName, fence.lang, token), }); } } diff --git a/skills/skill-host-compat/SKILL.md b/skills/skill-host-compat/SKILL.md index e8ad98059..6afc6aa78 100644 --- a/skills/skill-host-compat/SKILL.md +++ b/skills/skill-host-compat/SKILL.md @@ -27,7 +27,9 @@ sees `.agents/skills/`. This script is the cross-tree gate: 1. Claude-only substitutions in executable `bash` / `sh` / `zsh` fences (`${CLAUDE_SESSION_ID}`, `${CLAUDE_SKILL_DIR}`, `${CLAUDE_PLUGIN_ROOT}`, - `$ARGUMENTS`) fail. Codex leaves those tokens as literal text. + `${CLAUDE_PROJECT_DIR}`, `$ARGUMENTS`) fail. Codex leaves those tokens + as literal text. In a Codex copy, rewrite `${CLAUDE_PROJECT_DIR}` to + `$(pwd)` or an explicit path. 2. A Codex copy may only use `name`, `description`, `metadata`, `license`, and `allowed-tools`. `origin: ECC` may stay on the canonical `skills/` copy and must be stripped on the Codex copy. @@ -76,6 +78,7 @@ forbidden pattern: echo "${CLAUDE_SESSION_ID}" ls "${CLAUDE_SKILL_DIR}" node "${CLAUDE_PLUGIN_ROOT}/scripts/setup.js" $ARGUMENTS +cd "${CLAUDE_PROJECT_DIR}" ``` Do not add `version:` or `origin:` to a Codex copy under `.agents/skills/`. @@ -87,7 +90,9 @@ every `skills/` entry to have a Cursor copy. - Keep canonical `origin: ECC` if the CONTRIBUTING template calls for it. - Drop `origin`, `version`, and `argument-hint` on the Codex copy. - Show host-portable commands in bash fences (`npx`, `ecc`, or a resolved - root). Mention Claude env vars in prose, not in executable fences. + root). Rewrite `${CLAUDE_PROJECT_DIR}` in Codex copies to `$(pwd)` or an + explicit path. Mention other Claude env vars in prose, not in executable + fences. - Run this script before `npm test` when the change is a new skill. - Use `--inventory` to see checklist warning volume without failing the tree. diff --git a/tests/ci/validate-skill-host-compat.test.js b/tests/ci/validate-skill-host-compat.test.js index a97bc766d..df476bcda 100644 --- a/tests/ci/validate-skill-host-compat.test.js +++ b/tests/ci/validate-skill-host-compat.test.js @@ -150,6 +150,75 @@ check('fail: bash fence contains ${CLAUDE_SESSION_ID}', () => { } }); +check('fail: bash fence contains ${CLAUDE_PROJECT_DIR}', () => { + const root = createRoot(); + try { + writeSkill( + root, + 'proj', + `${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`bash\ncd "\${CLAUDE_PROJECT_DIR}"\n\`\`\`\n` + ); + const result = runValidator(root); + assert.strictEqual(result.status, 1); + assert.ok(result.stderr.includes('ERROR:')); + assert.ok(result.stderr.includes('proj')); + assert.ok(result.stderr.includes('${CLAUDE_PROJECT_DIR}')); + assert.ok(result.stderr.includes('rewrite to $(pwd) or an explicit path')); + } finally { + cleanup(root); + } +}); + +check('fail: $CLAUDE_PROJECT_DIR used as a substitution in a bash fence', () => { + const root = createRoot(); + try { + writeSkill( + root, + 'proj', + `${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`sh\ncd "$CLAUDE_PROJECT_DIR"\n\`\`\`\n` + ); + const result = runValidator(root); + assert.strictEqual(result.status, 1); + assert.ok(result.stderr.includes('$CLAUDE_PROJECT_DIR')); + assert.ok(result.stderr.includes('rewrite to $(pwd) or an explicit path')); + } finally { + cleanup(root); + } +}); + +check('bash fence $(pwd) is accepted in place of CLAUDE_PROJECT_DIR', () => { + const root = createRoot(); + try { + writeSkill( + root, + 'proj', + `${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`bash\ncd "$(pwd)"\n\`\`\`\n` + ); + const result = runValidator(root); + assert.strictEqual(result.status, 0, result.stderr); + assert.ok(!result.stderr.includes('ERROR:')); + assert.ok(!result.stderr.includes('CLAUDE_PROJECT_DIR')); + } finally { + cleanup(root); + } +}); + +check('prose mention of CLAUDE_PROJECT_DIR does not fail', () => { + const root = createRoot(); + try { + writeSkill( + root, + 'prose', + `${checklistBody('Prose')}\nThe project root is \`CLAUDE_PROJECT_DIR\`, then cwd.\n` + ); + const result = runValidator(root); + assert.strictEqual(result.status, 0, result.stderr); + assert.ok(!result.stderr.includes('ERROR:')); + } finally { + cleanup(root); + } +}); + check('fail: $ARGUMENTS used as a substitution in a bash fence', () => { const root = createRoot(); try {