Skip to content

Commit 08f66bd

Browse files
fix: avoid CLAUDE_SKILL_DIR in hook frontmatter
1 parent cab774c commit 08f66bd

9 files changed

Lines changed: 70 additions & 17 deletions

File tree

careful/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ hooks:
1414
- matcher: "Bash"
1515
hooks:
1616
- type: command
17-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
17+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
1818
statusMessage: "Checking for destructive commands..."
1919
---
2020
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

careful/SKILL.md.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ hooks:
1919
- matcher: "Bash"
2020
hooks:
2121
- type: command
22-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
22+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2323
statusMessage: "Checking for destructive commands..."
2424
sensitive: true
2525
---

freeze/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ hooks:
1515
- matcher: "Edit"
1616
hooks:
1717
- type: command
18-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
18+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
1919
statusMessage: "Checking freeze boundary..."
2020
- matcher: "Write"
2121
hooks:
2222
- type: command
23-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
23+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2424
statusMessage: "Checking freeze boundary..."
2525
---
2626
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

freeze/SKILL.md.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ hooks:
2020
- matcher: "Edit"
2121
hooks:
2222
- type: command
23-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
23+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2424
statusMessage: "Checking freeze boundary..."
2525
- matcher: "Write"
2626
hooks:
2727
- type: command
28-
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
28+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2929
statusMessage: "Checking freeze boundary..."
3030
sensitive: true
3131
---

guard/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ hooks:
1515
- matcher: "Bash"
1616
hooks:
1717
- type: command
18-
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
18+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
1919
statusMessage: "Checking for destructive commands..."
2020
- matcher: "Edit"
2121
hooks:
2222
- type: command
23-
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
23+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2424
statusMessage: "Checking freeze boundary..."
2525
- matcher: "Write"
2626
hooks:
2727
- type: command
28-
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
28+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2929
statusMessage: "Checking freeze boundary..."
3030
---
3131
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

guard/SKILL.md.tmpl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ hooks:
2020
- matcher: "Bash"
2121
hooks:
2222
- type: command
23-
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
23+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/careful/bin/check-careful.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2424
statusMessage: "Checking for destructive commands..."
2525
- matcher: "Edit"
2626
hooks:
2727
- type: command
28-
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
28+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2929
statusMessage: "Checking freeze boundary..."
3030
- matcher: "Write"
3131
hooks:
3232
- type: command
33-
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
33+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
3434
statusMessage: "Checking freeze boundary..."
3535
sensitive: true
3636
---

investigate/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ hooks:
2323
- matcher: "Edit"
2424
hooks:
2525
- type: command
26-
command: 'bash -c ''S="${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"; [ -x "$S" ] || S="${CLAUDE_SKILL_DIR}/../gstack-freeze/bin/check-freeze.sh"; [ -x "$S" ] && bash "$S" || exit 0'''
26+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
2727
statusMessage: "Checking debug scope boundary..."
2828
- matcher: "Write"
2929
hooks:
3030
- type: command
31-
command: 'bash -c ''S="${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"; [ -x "$S" ] || S="${CLAUDE_SKILL_DIR}/../gstack-freeze/bin/check-freeze.sh"; [ -x "$S" ] && bash "$S" || exit 0'''
31+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
3232
statusMessage: "Checking debug scope boundary..."
3333
gbrain:
3434
schema: 1

investigate/SKILL.md.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ hooks:
3030
- matcher: "Edit"
3131
hooks:
3232
- type: command
33-
command: 'bash -c ''S="${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"; [ -x "$S" ] || S="${CLAUDE_SKILL_DIR}/../gstack-freeze/bin/check-freeze.sh"; [ -x "$S" ] && bash "$S" || exit 0'''
33+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
3434
statusMessage: "Checking debug scope boundary..."
3535
- matcher: "Write"
3636
hooks:
3737
- type: command
38-
command: 'bash -c ''S="${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"; [ -x "$S" ] || S="${CLAUDE_SKILL_DIR}/../gstack-freeze/bin/check-freeze.sh"; [ -x "$S" ] && bash "$S" || exit 0'''
38+
command: 'bash -c ''D="$PWD"; while :; do S="$D/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; [ "$D" = "/" ] && break; D="$(dirname "$D")"; done; S="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"; [ -x "$S" ] && exec bash "$S"; exit 0'''
3939
statusMessage: "Checking debug scope boundary..."
4040
gbrain:
4141
schema: 1

test/gen-skill-docs.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1837,7 +1837,7 @@ describe('Codex generation (--host codex)', () => {
18371837
expect(frontmatter).toContain('YC Office Hours');
18381838
});
18391839

1840-
test('hook skills have safety prose and no hooks: in frontmatter', () => {
1840+
test('Codex hook skills have safety prose and no hooks: in frontmatter', () => {
18411841
const HOOK_SKILLS = ['gstack-careful', 'gstack-freeze', 'gstack-guard'];
18421842
for (const skillName of HOOK_SKILLS) {
18431843
const content = fs.readFileSync(path.join(AGENTS_DIR, skillName, 'SKILL.md'), 'utf-8');
@@ -1850,6 +1850,59 @@ describe('Codex generation (--host codex)', () => {
18501850
}
18511851
});
18521852

1853+
test('Claude hook commands do not depend on CLAUDE_SKILL_DIR', () => {
1854+
// #1871: Claude Code 2.1.162 does not populate CLAUDE_SKILL_DIR for
1855+
// skill-frontmatter PreToolUse hooks. If these commands use that variable,
1856+
// they expand to paths like /../freeze/bin/check-freeze.sh and fail before
1857+
// the safety hook can do its job.
1858+
const HOOK_SKILLS = ['careful', 'freeze', 'guard', 'investigate'];
1859+
for (const skillName of HOOK_SKILLS) {
1860+
const content = fs.readFileSync(path.join(ROOT, skillName, 'SKILL.md'), 'utf-8');
1861+
const fmEnd = content.indexOf('\n---', 4);
1862+
const frontmatter = content.slice(4, fmEnd);
1863+
expect(frontmatter).toContain('hooks:');
1864+
expect(frontmatter).not.toContain('CLAUDE_SKILL_DIR');
1865+
expect(frontmatter).not.toContain('CLAUDE_PLUGIN_ROOT');
1866+
expect(frontmatter).not.toContain('git rev-parse --show-toplevel');
1867+
expect(frontmatter).toContain('while :; do');
1868+
expect(frontmatter).toContain('$D/.claude/skills/gstack/');
1869+
expect(frontmatter).toContain('$HOME/.claude/skills/gstack/');
1870+
}
1871+
});
1872+
1873+
test('Claude hook commands resolve project-local install from nested non-git cwd', () => {
1874+
const { execFileSync } = require('child_process');
1875+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-hook-local-'));
1876+
const projectRoot = path.join(tmpRoot, 'project');
1877+
const nestedCwd = path.join(projectRoot, 'nested', 'plain-dir');
1878+
const scriptPath = path.join(projectRoot, '.claude', 'skills', 'gstack', 'careful', 'bin', 'check-careful.sh');
1879+
1880+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
1881+
fs.mkdirSync(nestedCwd, { recursive: true });
1882+
fs.writeFileSync(scriptPath, '#!/usr/bin/env bash\nprintf local-careful');
1883+
fs.chmodSync(scriptPath, 0o755);
1884+
1885+
try {
1886+
const content = fs.readFileSync(path.join(ROOT, 'careful', 'SKILL.md'), 'utf-8');
1887+
const fmEnd = content.indexOf('\n---', 4);
1888+
const frontmatter = content.slice(4, fmEnd);
1889+
const commandLine = frontmatter.split('\n').find(line => line.trim().startsWith('command: '));
1890+
expect(commandLine).toBeTruthy();
1891+
let command = commandLine!.trim().slice('command: '.length);
1892+
expect(command.startsWith("'")).toBe(true);
1893+
command = command.slice(1, -1).replaceAll("''", "'");
1894+
1895+
const output = execFileSync('bash', ['-c', command], {
1896+
cwd: nestedCwd,
1897+
input: '{}',
1898+
encoding: 'utf8',
1899+
});
1900+
expect(output).toBe('local-careful');
1901+
} finally {
1902+
fs.rmSync(tmpRoot, { recursive: true, force: true });
1903+
}
1904+
});
1905+
18531906
test('all Codex SKILL.md files have auto-generated header', () => {
18541907
for (const skill of CODEX_SKILLS) {
18551908
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');

0 commit comments

Comments
 (0)