fix(ci): flag CLAUDE_PROJECT_DIR in skill fences with Codex rewrite guidance

This commit is contained in:
Matt Van Horn
2026-09-25 06:44:12 -07:00
parent 9c45149bae
commit da5757b6ff
5 changed files with 98 additions and 7 deletions
+7 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+14 -2
View File
@@ -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/<name>/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),
});
}
}
+7 -2
View File
@@ -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.
@@ -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 {