Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion careful/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
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'''
statusMessage: "Checking for destructive commands..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
Expand Down
2 changes: 1 addition & 1 deletion careful/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
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'''
statusMessage: "Checking for destructive commands..."
sensitive: true
---
Expand Down
4 changes: 2 additions & 2 deletions freeze/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
Expand Down
4 changes: 2 additions & 2 deletions freeze/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
sensitive: true
---
Expand Down
6 changes: 3 additions & 3 deletions guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
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'''
statusMessage: "Checking for destructive commands..."
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
Expand Down
6 changes: 3 additions & 3 deletions guard/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
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'''
statusMessage: "Checking for destructive commands..."
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
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'''
statusMessage: "Checking freeze boundary..."
sensitive: true
---
Expand Down
4 changes: 2 additions & 2 deletions investigate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
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'''
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'''
statusMessage: "Checking debug scope boundary..."
- matcher: "Write"
hooks:
- type: command
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'''
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'''
statusMessage: "Checking debug scope boundary..."
gbrain:
schema: 1
Expand Down
4 changes: 2 additions & 2 deletions investigate/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
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'''
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'''
statusMessage: "Checking debug scope boundary..."
- matcher: "Write"
hooks:
- type: command
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'''
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'''
statusMessage: "Checking debug scope boundary..."
gbrain:
schema: 1
Expand Down
55 changes: 54 additions & 1 deletion test/gen-skill-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,7 +1837,7 @@ describe('Codex generation (--host codex)', () => {
expect(frontmatter).toContain('YC Office Hours');
});

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

test('Claude hook commands do not depend on CLAUDE_SKILL_DIR', () => {
// #1871: Claude Code 2.1.162 does not populate CLAUDE_SKILL_DIR for
// skill-frontmatter PreToolUse hooks. If these commands use that variable,
// they expand to paths like /../freeze/bin/check-freeze.sh and fail before
// the safety hook can do its job.
const HOOK_SKILLS = ['careful', 'freeze', 'guard', 'investigate'];
for (const skillName of HOOK_SKILLS) {
const content = fs.readFileSync(path.join(ROOT, skillName, 'SKILL.md'), 'utf-8');
const fmEnd = content.indexOf('\n---', 4);
const frontmatter = content.slice(4, fmEnd);
expect(frontmatter).toContain('hooks:');
expect(frontmatter).not.toContain('CLAUDE_SKILL_DIR');
expect(frontmatter).not.toContain('CLAUDE_PLUGIN_ROOT');
expect(frontmatter).not.toContain('git rev-parse --show-toplevel');
expect(frontmatter).toContain('while :; do');
expect(frontmatter).toContain('$D/.claude/skills/gstack/');
expect(frontmatter).toContain('$HOME/.claude/skills/gstack/');
}
});

test('Claude hook commands resolve project-local install from nested non-git cwd', () => {
const { execFileSync } = require('child_process');
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-hook-local-'));
const projectRoot = path.join(tmpRoot, 'project');
const nestedCwd = path.join(projectRoot, 'nested', 'plain-dir');
const scriptPath = path.join(projectRoot, '.claude', 'skills', 'gstack', 'careful', 'bin', 'check-careful.sh');

fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
fs.mkdirSync(nestedCwd, { recursive: true });
fs.writeFileSync(scriptPath, '#!/usr/bin/env bash\nprintf local-careful');
fs.chmodSync(scriptPath, 0o755);

try {
const content = fs.readFileSync(path.join(ROOT, 'careful', 'SKILL.md'), 'utf-8');
const fmEnd = content.indexOf('\n---', 4);
const frontmatter = content.slice(4, fmEnd);
const commandLine = frontmatter.split('\n').find(line => line.trim().startsWith('command: '));
expect(commandLine).toBeTruthy();
let command = commandLine!.trim().slice('command: '.length);
expect(command.startsWith("'")).toBe(true);
command = command.slice(1, -1).replaceAll("''", "'");

const output = execFileSync('bash', ['-c', command], {
cwd: nestedCwd,
input: '{}',
encoding: 'utf8',
});
expect(output).toBe('local-careful');
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});

test('all Codex SKILL.md files have auto-generated header', () => {
for (const skill of CODEX_SKILLS) {
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
Expand Down