fix(team-init): emit PreToolUse deny in hookSpecificOutput shape

The check-gstack.sh hook generated by gstack-team-init printed
permissionDecision at the top level. Claude Code's PreToolUse decision
control only reads hookSpecificOutput.permissionDecision (with
hookEventName), so the deny was silently ignored and the required-mode
gate never actually blocked skill usage when gstack was missing.

Use the same hookSpecificOutput shape adopted for careful/freeze in
#1509, and add a test that executes the generated hook in both
scenarios (gstack missing -> structured deny; installed -> {}).
This commit is contained in:
Erick Heslan 2026-06-11 07:50:51 -03:00
parent a5833c413f
commit 6393079468
2 changed files with 24 additions and 1 deletions

View File

@ -127,7 +127,7 @@ Install it:
Then restart your AI coding tool.
MSG
echo '{"permissionDecision":"deny","message":"gstack is required but not installed. See stderr for install instructions."}'
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"gstack is required but not installed. See stderr for install instructions."}}'
exit 0
fi

View File

@ -240,6 +240,29 @@ describe('gstack-team-init', () => {
expect(stat.mode & 0o111).toBeGreaterThan(0);
});
test('required: hook emits PreToolUse deny in hookSpecificOutput shape when gstack is missing', () => {
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.sh');
// HOME without gstack → must deny via hookSpecificOutput (top-level
// permissionDecision is ignored by Claude Code's PreToolUse schema)
const fakeHome = path.join(tmpDir, 'fake-home');
fs.mkdirSync(fakeHome, { recursive: true });
const denied = run(`bash ${hookPath}`, { env: { HOME: fakeHome } });
expect(denied.exitCode).toBe(0);
const deniedJson = JSON.parse(denied.stdout.trim());
expect(deniedJson.hookSpecificOutput.hookEventName).toBe('PreToolUse');
expect(deniedJson.hookSpecificOutput.permissionDecision).toBe('deny');
expect(deniedJson.hookSpecificOutput.permissionDecisionReason).toContain('gstack is required');
// HOME with gstack installed → no-op {}
const homeWithGstack = path.join(tmpDir, 'home-with-gstack');
fs.mkdirSync(path.join(homeWithGstack, '.claude', 'skills', 'gstack', 'bin'), { recursive: true });
const allowed = run(`bash ${hookPath}`, { env: { HOME: homeWithGstack } });
expect(allowed.exitCode).toBe(0);
expect(JSON.parse(allowed.stdout.trim())).toEqual({});
});
test('required: creates project settings.json with PreToolUse hook', () => {
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const settingsPath = path.join(tmpDir, '.claude', 'settings.json');