Give plugin agent files the plugin-root variable, not the skill token

A spawned agent never loads SKILL.md, so the <skill-base-dir> token
Setup defines is undefined in the one context that must act on it
(review finding). Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} inline
anywhere in plugin agent content, so the agents directory now gets its
own rewrite emitting the quoted variable form; the skill and reference
files keep the token, which the main thread's base-directory report
resolves.

Drafted with AI assistance, reviewed by a maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-28 01:08:16 +05:00
co-authored by Cursor
parent abf7773c3d
commit d28a9dc31c
4 changed files with 74 additions and 7 deletions
+29 -4
View File
@@ -44,6 +44,15 @@ const SETUP_FALLBACK_TEXT =
const SETUP_PLUGIN_TEXT =
'Every `node "<skill-base-dir>/scripts/..."` command in this skill and its references resolves against that base directory.';
// Agent files are subagent system prompts: a spawned agent never loads
// SKILL.md, so Setup's <skill-base-dir> token is undefined in the one
// context that must act on it (review finding). Claude Code substitutes
// ${CLAUDE_PLUGIN_ROOT} inline anywhere in plugin skill and agent content
// (code.claude.com/docs/en/plugins-reference), so agent instructions carry
// the variable form; where a harness leaves it unsubstituted, the variable
// still names the plugin install directory for the agent to locate.
export const PLUGIN_AGENT_SCRIPTS_PATH = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
/**
* Rewrite one markdown file's content for the plugin subtree. Pure, so the
* unit suite can pin every rewrite without a build.
@@ -64,6 +73,21 @@ export function rewritePluginMarkdown(content) {
.replace(/node <skill-base-dir>\/scripts\/([^\s`"]+)/g, 'node "<skill-base-dir>/scripts/$1"');
}
/**
* Rewrite one agent file's content for the plugin subtree. Same quoting
* discipline as the skill rewrite, but the path is the plugin-root
* variable rather than the skill-base-dir token SKILL.md defines,
* because no SKILL.md travels with a spawned agent.
*/
export function rewritePluginAgentMarkdown(content) {
return content
.replaceAll(CLAUDE_PROJECT_SCRIPTS_PATH, PLUGIN_AGENT_SCRIPTS_PATH)
.replace(
/node \$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/([^\s`"]+)/g,
'node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/$1"',
);
}
/**
* Fail the build when the copied SKILL.md no longer matches the rewrite.
* The fallback-sentence replacement keys on the exact Setup step 1 text; if
@@ -98,20 +122,21 @@ export function verifyPluginSkillRewrite(skillMdPath) {
}
/**
* Apply rewritePluginMarkdown to every .md file under dir, recursively.
* Apply a rewrite to every .md file under dir, recursively. Defaults to
* the skill rewrite; the agents directory passes rewritePluginAgentMarkdown.
* Script files are left alone: the only project-relative paths in them
* (hook-admin.mjs) install project-scoped hooks via ${CLAUDE_PROJECT_DIR},
* which is that command's actual job.
*/
export function rewritePluginMarkdownTree(dir) {
export function rewritePluginMarkdownTree(dir, rewrite = rewritePluginMarkdown) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
rewritePluginMarkdownTree(entryPath);
rewritePluginMarkdownTree(entryPath, rewrite);
} else if (entry.name.endsWith('.md')) {
const original = fs.readFileSync(entryPath, 'utf-8');
const rewritten = rewritePluginMarkdown(original);
const rewritten = rewrite(original);
if (rewritten !== original) fs.writeFileSync(entryPath, rewritten);
}
}