fix: copy hook files to Codex install target (#2153) (#2166)

Codex install registered gsd-check-update.js in config.toml but never
copied the hook file to ~/.codex/hooks/. The hook-copy block in install()
was gated by !isCodex, leaving a broken reference on every fresh Codex
global install.

Adds a dedicated hook-copy step inside the isCodex branch that mirrors
the existing copy logic (template substitution, chmod). Adds a regression
test that verifies the hook file physically exists after install.

Closes #2153

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-12 15:52:57 -04:00
committed by GitHub
parent 4bf3b02bec
commit b896db6f91
2 changed files with 45 additions and 0 deletions

View File

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

View File

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