Fix Codex hook path so .codex-directory installs run the detector

The committed .codex/hooks.json hardcoded .agents/skills/impeccable/scripts/
hook.mjs. On a .codex-directory install the skill payload lives at .codex/
skills/..., so the guarded command ([ ! -f X ] || node X) found no file and
silently no-opped, leaving the design detector dead for those users.

Derive the hook payload path from the emitting provider's own configDir rather
than hardcoding .agents:

- buildCodexHooksManifest(skillDir) now builds `${skillDir}/skills/impeccable/
  scripts/hook.mjs`; hooksJsonFor threads each provider's configDir through. The
  Codex provider (configDir .codex) emits .codex/skills; the root sync and the
  self-consistent dist/codex bundle both point at their own payload.
- CLI installer: project-scope hook rewriting now derives the provider's own
  project-relative path instead of preserving the bundle token. The Codex bundle
  ships a .codex/skills command, but the CLI lays the skill at .agents/skills, so
  the installed .codex/hooks.json is rewritten to .agents/skills (Claude keeps
  its ${CLAUDE_PROJECT_DIR} token; global installs keep the absolute rewrite).

Per-provider hook payload path after the fix:

  Emission                              hook path
  dist/codex/.codex/hooks.json          .codex/skills/impeccable/scripts/hook.mjs
  root .codex/hooks.json (build sync)   .codex/skills/impeccable/scripts/hook.mjs
  CLI .agents (codex) project install   .agents/skills/impeccable/scripts/hook.mjs
  CLI .agents (codex) global install    <home>/.agents/skills/.../hook.mjs (abs)
  .claude / .cursor                     unchanged

Tests: extended hook-build (codex-dir -> .codex/skills, agents-dir -> .agents/
skills) and skills-cli (bundle ships .codex/skills, install rewrites to .agents/
skills). Regenerated tracked .codex/hooks.json via build:release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-24 17:09:33 -07:00
co-authored by Claude Fable 5
parent bb57be4243
commit bcf354cd0c
7 changed files with 91 additions and 18 deletions
+1 -1
View File
@@ -400,7 +400,7 @@ function syncRootHookManifests(rootDir) {
const synced = [];
for (const config of Object.values(PROVIDERS)) {
if (!config.emitHooks) continue;
const manifest = hooksJsonFor(config.emitHooks);
const manifest = hooksJsonFor(config.emitHooks, { configDir: config.configDir });
if (!manifest) continue;
const rel = config.hooksManifestRel || path.join('hooks', 'hooks.json');
const dest = path.join(rootDir, config.configDir, rel);
+1 -1
View File
@@ -308,7 +308,7 @@ export function createTransformer(config) {
// `.codex/hooks.json`, and Cursor uses `.cursor/hooks.json`.
let hooksEmitted = false;
if (config.emitHooks) {
const manifest = hooksJsonFor(config.emitHooks);
const manifest = hooksJsonFor(config.emitHooks, { configDir });
if (manifest) {
const hooksRel = config.hooksManifestRel || path.join('hooks', 'hooks.json');
writeFile(path.join(providerDir, configDir, hooksRel), JSON.stringify(manifest, null, 2) + '\n');
+16 -6
View File
@@ -54,7 +54,13 @@ const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scr
const guardedNode = (hookPath) => `[ ! -f "${hookPath}" ] || node "${hookPath}"`;
const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
const CODEX_PROJECT_HOOK = '.agents/skills/impeccable/scripts/hook.mjs';
// Codex reads project hooks from `.codex/hooks.json`, but the skill payload the
// hook invokes lives under the install's own skills dir: a `.codex`-directory
// install keeps it at `.codex/skills/...`, while a `.agents` (Codex repo-skills)
// install keeps it at `.agents/skills/...`. Derive the path from the install dir
// so each generated manifest points at its own payload rather than a hardcoded
// `.agents` — otherwise the guarded hook silently no-ops on `.codex` installs.
const codexProjectHook = (skillDir) => `${skillDir}/skills/impeccable/scripts/hook.mjs`;
const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs';
const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs';
// Grok project hooks are relative to the git/workspace root. Claude tool names
@@ -134,7 +140,11 @@ export function buildCodexPluginHooksManifest() {
};
}
export function buildCodexHooksManifest() {
// `skillDir` is the install's own dot-directory (a provider's configDir), so the
// emitted command points at that install's payload. Defaults to `.codex` for the
// Codex provider, whose self-consistent bundle keeps the skill at `.codex/skills`.
export function buildCodexHooksManifest(skillDir = '.codex') {
const hookPath = codexProjectHook(skillDir);
return {
hooks: {
PostToolUse: [
@@ -143,14 +153,14 @@ export function buildCodexHooksManifest() {
hooks: [
{
type: 'command',
command: guardedNode(CODEX_PROJECT_HOOK),
command: guardedNode(hookPath),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(guardedNode(CODEX_PROJECT_HOOK))],
Stop: [stopEntry(guardedNode(hookPath))],
},
};
}
@@ -222,12 +232,12 @@ export function buildGrokHooksManifest() {
};
}
export function hooksJsonFor(provider) {
export function hooksJsonFor(provider, options = {}) {
switch (provider) {
case 'claude':
return buildClaudeSettingsManifest();
case 'codex':
return buildCodexHooksManifest();
return buildCodexHooksManifest(options.configDir || '.codex');
case 'cursor':
return buildCursorHooksManifest();
case 'github':