@@ -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