Files
orca/src/main/codex/codex-hook-definition.ts
T
Neil d62328aa4d fix(codex): remove redundant Windows hook launcher for Unicode profiles (#20952)
* fix(codex): reuse the Windows hook shell for Unicode profile paths

* test(codex): register Unicode hook tests in Windows CI

* test(codex): pin trust hash replacement during Windows upgrade

* test(codex): retry transient Windows teardown locks
2026-09-16 00:30:52 -07:00

117 lines
4.2 KiB
TypeScript

import { join } from 'node:path'
import {
getSharedManagedScriptPath,
buildWindowsHookPowerShellCommand,
wrapPosixHookCommand,
WINDOWS_CMD_SAFE_PATH,
writeHooksJson,
type HookDefinition
} from '../agent-hooks/installer-utils'
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from '../agent-hooks/hook-stdin-contract'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
import { CODEX_HOOK_EVENT_LABEL, getCodexManagedScriptFileName } from './codex-hook-identity'
import { getManagedScript } from './codex-hook-script'
import type { CodexEventLabel } from './config-toml-trust'
// Why: Pre/PostToolUse feed the live in-flight-tool readout; PermissionRequest exits with no decision so Codex still shows its approval UI while Orca flips the pane to waiting.
export const CODEX_EVENTS = [
'SessionStart',
'UserPromptSubmit',
'PreToolUse',
'PermissionRequest',
'PostToolUse',
'SubagentStart',
'SubagentStop',
'Stop'
] as const
export function getConfigPath(runtimeHomePath: string = getOrcaManagedCodexHomePath()): string {
return join(runtimeHomePath, 'hooks.json')
}
export function writeCodexHooksJson(
configPath: string,
hooks: Record<string, HookDefinition[]>
): void {
// Why: Codex rejects unknown top-level hooks.json fields, so plugin bookkeeping like `_managed` must not survive Orca's rewrite.
writeHooksJson(configPath, { hooks })
}
export function getCodexConfigTomlPath(
runtimeHomePath: string = getOrcaManagedCodexHomePath()
): string {
return join(runtimeHomePath, 'config.toml')
}
// Why: managed-event subset of the shared label map; full mapping lives in codex-hook-identity.ts so promotion can't drift.
export const CODEX_EVENT_LABEL: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel> = {
SessionStart: CODEX_HOOK_EVENT_LABEL.SessionStart!,
UserPromptSubmit: CODEX_HOOK_EVENT_LABEL.UserPromptSubmit!,
PreToolUse: CODEX_HOOK_EVENT_LABEL.PreToolUse!,
PermissionRequest: CODEX_HOOK_EVENT_LABEL.PermissionRequest!,
PostToolUse: CODEX_HOOK_EVENT_LABEL.PostToolUse!,
SubagentStart: CODEX_HOOK_EVENT_LABEL.SubagentStart!,
SubagentStop: CODEX_HOOK_EVENT_LABEL.SubagentStop!,
Stop: CODEX_HOOK_EVENT_LABEL.Stop!
}
export const CODEX_MANAGED_EVENT_LABELS = new Set<CodexEventLabel>(
CODEX_EVENTS.map((eventName) => CODEX_EVENT_LABEL[eventName])
)
export const CODEX_PLUGIN_ONLY_HOOK_PLACEHOLDERS = [
'${CLAUDE_PLUGIN_ROOT}',
'${CLAUDE_PLUGIN_DATA}',
'${PLUGIN_ROOT}',
'${PLUGIN_DATA}'
] as const
export function getManagedScriptPath(): string {
return getSharedManagedScriptPath(getCodexManagedScriptFileName())
}
export function getManagedCommand(scriptPath: string): string {
if (process.platform !== 'win32') {
return wrapPosixHookCommand(scriptPath)
}
// Codex's default native Windows hook host is PowerShell; reuse it to avoid a second interpreter.
return WINDOWS_CMD_SAFE_PATH.test(scriptPath)
? scriptPath
: buildWindowsHookPowerShellCommand(scriptPath)
}
export type CodexManagedHookInstallMaterial = {
events: readonly (typeof CODEX_EVENTS)[number][]
eventLabel: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel>
scriptPath: string
command: string
script: string
}
// Why: the real-home installer must byte-match the managed lane's events,
// command, and script, or trust signatures diverge between the two homes.
export function getCodexManagedHookInstallMaterial(): CodexManagedHookInstallMaterial {
const scriptPath = getManagedScriptPath()
return {
events: CODEX_EVENTS,
eventLabel: CODEX_EVENT_LABEL,
scriptPath,
command: getManagedCommand(scriptPath),
script: getManagedScript()
}
}
export function wrapReadablePosixHookCommand(scriptPath: string): string {
const quoted = `'${scriptPath.replaceAll("'", "'\\''")}'`
// Why: WSL hooks are written from Windows over UNC where the exec bit is unreliable; a missing script must still own stdin.
return `if [ -f ${quoted} ] && [ -r ${quoted} ]; then /bin/sh ${quoted}; else ${POSIX_HOOK_STDIN_DRAIN_COMMAND}; fi`
}
export function getSystemConfigPath(): string {
return join(getSystemCodexHomePath(), 'hooks.json')
}
export function getSystemCodexConfigTomlPath(): string {
return join(getSystemCodexHomePath(), 'config.toml')
}