diff --git a/bin/install.js b/bin/install.js index 5650ceb9a..710270024 100755 --- a/bin/install.js +++ b/bin/install.js @@ -5856,6 +5856,35 @@ function install(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`); console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`); + // Copy hook files that are referenced in config.toml (#2153) + // The main hook-copy block is gated to non-Codex runtimes, but Codex registers + // gsd-check-update.js in config.toml — the file must physically exist. + const codexHooksSrc = path.join(src, 'hooks', 'dist'); + if (fs.existsSync(codexHooksSrc)) { + const codexHooksDest = path.join(targetDir, 'hooks'); + fs.mkdirSync(codexHooksDest, { recursive: true }); + const configDirReplacement = getConfigDirFromHome(runtime, isGlobal); + for (const entry of fs.readdirSync(codexHooksSrc)) { + const srcFile = path.join(codexHooksSrc, entry); + if (!fs.statSync(srcFile).isFile()) continue; + const destFile = path.join(codexHooksDest, entry); + if (entry.endsWith('.js')) { + let content = fs.readFileSync(srcFile, 'utf8'); + content = content.replace(/'\.claude'/g, configDirReplacement); + content = content.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`); + content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version); + fs.writeFileSync(destFile, content); + try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ } + } else { + fs.copyFileSync(srcFile, destFile); + if (entry.endsWith('.sh')) { + try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ } + } + } + } + console.log(` ${green}✓${reset} Installed hooks`); + } + // Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag const configPath = path.join(targetDir, 'config.toml'); try { diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index b51037428..f02db36f5 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -830,6 +830,22 @@ describe('Codex install hook configuration (e2e)', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); + test('Codex install copies hook file that is referenced in config.toml (#2153)', () => { + // Regression test: Codex install writes gsd-check-update hook reference into + // config.toml but must also copy the hook file to ~/$CODEX_HOME/hooks/ + runCodexInstall(codexHome); + + const configContent = readCodexConfig(codexHome); + // config.toml must reference the hook + assert.ok(configContent.includes('gsd-check-update.js'), 'config.toml references gsd-check-update.js'); + // The hook file must physically exist at the referenced path + const hookFile = path.join(codexHome, 'hooks', 'gsd-check-update.js'); + assert.ok( + fs.existsSync(hookFile), + `gsd-check-update.js must exist at ${hookFile} — config.toml references it but file was not installed` + ); + }); + test('fresh CODEX_HOME enables codex_hooks without draft root defaults', () => { runCodexInstall(codexHome);