fix(ck): escape the skill dir for bash when SessionStart fills it in

Signed-off-by: Wen <jwcastillo@gmail.com>
(cherry picked from commit 00a03ab84f)
This commit is contained in:
Wen authored and haelyra committed 2026-10-20 18:01:42 -04:00
1 parent cf3408097c
commit 59868a9404
2 files changed
+29 -2

No files matched your search

+4 -1
View File
@@ -89,8 +89,11 @@ function main() {
// Load skill (always inject — now only ~50 lines)
// The host does not expand ${CLAUDE_SKILL_DIR} in hook output, so fill it in here.
// SKILL.md uses it inside double-quoted bash, so escape the characters bash
// still interprets there; the callback keeps `$&` etc. in the path literal.
const shellSkillDir = SKILL_DIR.replace(/[\\$`"]/g, '\\$&');
const skill = existsSync(SKILL_FILE)
? readFileSync(SKILL_FILE, 'utf8').replaceAll('${CLAUDE_SKILL_DIR}', SKILL_DIR)
? readFileSync(SKILL_FILE, 'utf8').replaceAll('${CLAUDE_SKILL_DIR}', () => shellSkillDir)
: '';
const projects = readJson(PROJECTS_FILE) || {};
+25 -1
View File
@@ -78,7 +78,31 @@ async function runTests() {
const { additionalContext } = JSON.parse(result.stdout);
assert.ok(additionalContext.includes('# ck — Context Keeper'), 'SKILL.md was not injected');
assert.ok(!additionalContext.includes('${CLAUDE_SKILL_DIR}'), 'skill dir placeholder left unexpanded');
assert.ok(additionalContext.includes(path.join(SKILL_DIR, 'commands')), 'skill dir not substituted');
// Escaped for double-quoted bash (Windows backslashes are doubled).
const shellCommandsDir = path.join(SKILL_DIR, 'commands').replace(/[\\$`"]/g, '\\$&');
assert.ok(additionalContext.includes(shellCommandsDir), 'skill dir not substituted');
} finally {
fs.rmSync(ctx.root, { recursive: true, force: true });
}
})) passed++; else failed++;
if (process.platform === 'win32') {
console.log(' - skipped shell-special skill dir test on Windows');
} else if (await test('hook-injected commands run from a skill dir with shell-special characters', async () => {
const ctx = tempEnv();
try {
const skillDir = path.join(ctx.root, 'skills $USER $& `x` "q"', 'ck');
fs.cpSync(SKILL_DIR, skillDir, { recursive: true });
const result = spawnSync(process.execPath, [path.join(skillDir, 'hooks', 'session-start.mjs')], {
cwd: ctx.project, env: ctx.env, input: '{}', encoding: 'utf8',
});
assert.strictEqual(result.status, 0, result.stderr);
const { additionalContext } = JSON.parse(result.stdout);
const listCommand = additionalContext.split('\n').find(line => line.includes('commands/list.mjs'));
assert.ok(listCommand, 'no list.mjs command injected');
const list = spawnSync('bash', ['-c', listCommand], { cwd: ctx.project, env: ctx.env, encoding: 'utf8' });
// No projects are registered, so list.mjs reaching its own message proves the path resolved.
assert.ok(list.stdout.includes('No projects registered'), `${listCommand}\n${list.stdout}${list.stderr}`);
} finally {
fs.rmSync(ctx.root, { recursive: true, force: true });
}