Fix slow Codex hook statuses on Windows (#6398)

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-25 19:24:04 -07:00 committed by GitHub
parent ef477199af
commit fa3fd3ae02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 125 additions and 76 deletions

View File

@ -345,12 +345,9 @@ describe('wrapWindowsHookCommand', () => {
return Buffer.from(encodedCommand!, 'base64').toString('utf16le')
}
it('invokes the .cmd through an encoded PowerShell command', () => {
it('uses the script path directly when no cmd escaping is needed', () => {
const command = wrapWindowsHookCommand('C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd')
expect(command).toMatch(/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/)
expect(decodeWindowsHookCommand(command)).toBe(
"& 'C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd'; exit $LASTEXITCODE"
)
expect(command).toBe('C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd')
})
// Why: a user profile path like `C:\Users\Jane Doe` is the regression from
@ -358,6 +355,7 @@ describe('wrapWindowsHookCommand', () => {
// the whole path inside the encoded command so shells do not split it.
it('preserves spaces in the script path (user profile with space case)', () => {
const cmd = wrapWindowsHookCommand('C:\\Users\\Jorge Silva\\.orca\\agent-hooks\\codex-hook.cmd')
expect(cmd).toMatch(/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/)
expect(decodeWindowsHookCommand(cmd)).toBe(
"& 'C:\\Users\\Jorge Silva\\.orca\\agent-hooks\\codex-hook.cmd'; exit $LASTEXITCODE"
)
@ -390,15 +388,17 @@ describe('wrapWindowsHookCommand', () => {
})
describe('buildWindowsAgentHookPostCommand', () => {
it('forces UTF-8 for redirected hook stdin and POST bodies', () => {
it('posts hook stdin through bounded curl without spawning PowerShell', () => {
const command = buildWindowsAgentHookPostCommand('codex')
expect(command).toContain('[Console]::InputEncoding=$utf8')
expect(command).toContain('[Console]::OutputEncoding=$utf8')
expect(command).toContain('$bodyBytes=$utf8.GetBytes($body)')
expect(command).toContain("-ContentType 'application/json; charset=utf-8'")
expect(command).toContain('-TimeoutSec 2')
expect(command).toContain('curl.exe -sS -X POST')
expect(command).toContain('--connect-timeout 0.5 --max-time 1.5')
expect(command).toContain('-H "Content-Type: application/x-www-form-urlencoded"')
expect(command).toContain('-H "X-Orca-Agent-Hook-Token: %ORCA_AGENT_HOOK_TOKEN%"')
expect(command).toContain('--data-urlencode "paneKey=%ORCA_PANE_KEY%"')
expect(command).toContain('--data-urlencode "payload@-"')
expect(command).toContain('/hook/codex')
expect(command).not.toContain("'Content-Type'='application/json'")
expect(command).not.toContain('powershell')
expect(command).not.toContain('Invoke-WebRequest')
})
})

View File

@ -147,23 +147,33 @@ function quotePowerShellString(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
// Why: Windows splits a raw hook command on whitespace, so a user profile path
// like `C:\Users\Jane Doe` makes the agent try to execute `C:\Users\Jane` and
// fail with exit code 1. Keep the script path inside an encoded PowerShell
// command so cmd.exe never gets a chance to expand legal path characters like
// `%` or `^` before the .cmd is invoked. #6078.
export function wrapWindowsHookCommand(scriptPath: string): string {
if (!/[ \t&()^|<>%!"]/u.test(scriptPath)) {
return scriptPath
}
// Why: Windows splits raw hook commands on whitespace, and cmd.exe expands
// `%`/`^`; use PowerShell only for paths that need that compatibility shim.
const command = `& ${quotePowerShellString(scriptPath)}; exit $LASTEXITCODE`
const encodedCommand = Buffer.from(command, 'utf16le').toString('base64')
return `powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`
}
export function buildWindowsAgentHookPostCommand(source: AgentHookSource): string {
// Why: Windows PowerShell 5.1 defaults redirected stdin/request bodies to the
// active code page. Hook payloads are UTF-8 JSON, so force UTF-8 on both read
// and POST or CJK prompts arrive in Orca as literal question marks. Timeout
// caps best-effort hook posts if the local listener stalls.
return `powershell -NoProfile -ExecutionPolicy Bypass -Command "$utf8=[System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$utf8; [Console]::OutputEncoding=$utf8; $inputData=[Console]::In.ReadToEnd(); if ([string]::IsNullOrWhiteSpace($inputData)) { exit 0 }; try { $body=@{ paneKey=$env:ORCA_PANE_KEY; launchToken=$env:ORCA_AGENT_LAUNCH_TOKEN; tabId=$env:ORCA_TAB_ID; worktreeId=$env:ORCA_WORKTREE_ID; env=$env:ORCA_AGENT_HOOK_ENV; version=$env:ORCA_AGENT_HOOK_VERSION; payload=($inputData | ConvertFrom-Json) } | ConvertTo-Json -Depth 100 -Compress; $bodyBytes=$utf8.GetBytes($body); Invoke-WebRequest -UseBasicParsing -Method Post -Uri ('http://127.0.0.1:' + $env:ORCA_AGENT_HOOK_PORT + '/hook/${source}') -ContentType 'application/json; charset=utf-8' -Headers @{ 'X-Orca-Agent-Hook-Token'=$env:ORCA_AGENT_HOOK_TOKEN } -Body $bodyBytes -TimeoutSec 2 | Out-Null } catch {}"`
// Why: Codex runs these hooks inline on every turn. PowerShell startup alone
// makes trusted Windows hooks visibly slow, so mirror the POSIX curl path.
return [
`curl.exe -sS -X POST "http://127.0.0.1:%ORCA_AGENT_HOOK_PORT%/hook/${source}" ^`,
' --connect-timeout 0.5 --max-time 1.5 ^',
' -H "Content-Type: application/x-www-form-urlencoded" ^',
' -H "X-Orca-Agent-Hook-Token: %ORCA_AGENT_HOOK_TOKEN%" ^',
' --data-urlencode "paneKey=%ORCA_PANE_KEY%" ^',
' --data-urlencode "tabId=%ORCA_TAB_ID%" ^',
' --data-urlencode "launchToken=%ORCA_AGENT_LAUNCH_TOKEN%" ^',
' --data-urlencode "worktreeId=%ORCA_WORKTREE_ID%" ^',
' --data-urlencode "env=%ORCA_AGENT_HOOK_ENV%" ^',
' --data-urlencode "version=%ORCA_AGENT_HOOK_VERSION%" ^',
' --data-urlencode "payload@-" >nul 2>nul'
].join('\r\n')
}
export function removeManagedCommands(

View File

@ -111,6 +111,16 @@ function markHookTrustDisabled(toml: string, header: string): string {
return `${toml.slice(0, headerIndex)}${block.replace('enabled = true', 'enabled = false')}${toml.slice(blockEnd)}`
}
function localManagedCodexEvents(): string[] {
return process.platform === 'win32'
? ['PermissionRequest', 'PostToolUse', 'PreToolUse']
: ['PermissionRequest', 'PostToolUse', 'PreToolUse', 'SessionStart', 'Stop', 'UserPromptSubmit']
}
function localHasManagedCodexLifecycleHooks(): boolean {
return process.platform !== 'win32'
}
describe('CodexHookService', () => {
it('installs PermissionRequest with trust so Codex approval prompts reach Orca', () => {
const systemCodexHome = join(tmpHome, '.codex')
@ -130,16 +140,7 @@ describe('CodexHookService', () => {
hooks: Record<string, { hooks?: { command?: string }[] }[]>
}
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(
[
'PermissionRequest',
'PostToolUse',
'PreToolUse',
'SessionStart',
'Stop',
'UserPromptSubmit'
].sort()
)
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(localManagedCodexEvents())
expect(
isCodexManagedCommand(hooksConfig.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command)
).toBe(true)
@ -198,7 +199,7 @@ describe('CodexHookService', () => {
readFileSync(join(managedCodexHome, 'hooks.json'), 'utf-8')
) as { hooks: Record<string, { hooks?: { command?: string }[] }[]> }
for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
for (const eventName of localManagedCodexEvents()) {
const command = hooksConfig.hooks[eventName]?.[0]?.hooks?.[0]?.command
expect(command).toMatch(
/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/
@ -259,12 +260,12 @@ describe('CodexHookService', () => {
)
).toBe(true)
expect(
devHooks.hooks.Stop?.some((definition) =>
devHooks.hooks.PreToolUse?.some((definition) =>
isCodexManagedCommand(definition.hooks?.[0]?.command)
)
).toBe(true)
expect(
prodHooks.hooks.Stop?.some((definition) =>
prodHooks.hooks.PreToolUse?.some((definition) =>
isCodexManagedCommand(definition.hooks?.[0]?.command)
)
).toBe(true)
@ -334,13 +335,18 @@ describe('CodexHookService', () => {
{ matcher?: string; hooks?: { command?: string; statusMessage?: string }[] }[]
>
}
expect(runtimeHooks.hooks.Stop?.[1]?.matcher).toBe('*')
expect(runtimeHooks.hooks.Stop?.[1]?.hooks?.[0]?.command).toBe('user-hook')
expect(runtimeHooks.hooks.Stop?.[1]?.hooks?.[0]?.statusMessage).toBe('Running user hook')
const userStopIndex = localHasManagedCodexLifecycleHooks() ? 1 : 0
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.matcher).toBe('*')
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.hooks?.[0]?.command).toBe('user-hook')
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.hooks?.[0]?.statusMessage).toBe(
'Running user hook'
)
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:${userStopIndex}:0`))
if (localHasManagedCodexLifecycleHooks()) {
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
}
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
})
@ -440,7 +446,9 @@ describe('CodexHookService', () => {
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:2:0`))
if (localHasManagedCodexLifecycleHooks()) {
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:2:0`))
}
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:1:0`))
})
@ -515,7 +523,9 @@ describe('CodexHookService', () => {
) ?? []
expect(stopCommands).toContain(userCommand)
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(true)
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(
localHasManagedCodexLifecycleHooks()
)
expect(runtimeHooks.hooks.PreCompact).toBeUndefined()
for (const command of pluginCommands) {
expect(runtimeHooksText).not.toContain(command)
@ -523,7 +533,9 @@ describe('CodexHookService', () => {
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
if (localHasManagedCodexLifecycleHooks()) {
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
}
for (const command of pluginCommands) {
expect(runtimeToml).not.toContain(command)
}
@ -621,7 +633,10 @@ describe('CodexHookService', () => {
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
const managedHooksPath = join(managedCodexHome, 'hooks.json')
const runtimeUserTrustHeader = hookTrustHeader(`${managedHooksPath}:stop:1:0`)
const runtimeUserStopIndex = localHasManagedCodexLifecycleHooks() ? 1 : 0
const runtimeUserTrustHeader = hookTrustHeader(
`${managedHooksPath}:stop:${runtimeUserStopIndex}:0`
)
expect(readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')).toContain(
runtimeUserTrustHeader
)
@ -631,7 +646,11 @@ describe('CodexHookService', () => {
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(runtimeToml).not.toContain(runtimeUserTrustHeader)
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
if (localHasManagedCodexLifecycleHooks()) {
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
} else {
expect(runtimeToml).not.toContain(':stop:0:0')
}
})
it('refreshes mirrored system user hooks when the system hooks file changes', () => {
@ -1079,7 +1098,9 @@ describe('CodexHookService', () => {
(definition) => definition.hooks?.map((hook) => hook.command ?? '') ?? []
) ?? []
expect(stopCommands).toContain(userCommand)
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(true)
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(
localHasManagedCodexLifecycleHooks()
)
expect(
isCodexManagedCommand(runtimeHooks.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command)
).toBe(true)
@ -1200,33 +1221,35 @@ describe('CodexHookService', () => {
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
const managedHooksPath = join(managedCodexHome, 'hooks.json')
const runtimeTomlPath = join(managedCodexHome, 'config.toml')
const canonicalSessionStartHeader = hookTrustHeader(`${managedHooksPath}:session_start:0:0`)
const legacySessionStartHeader = `[hooks.state."${escapeTomlBasicString(
`${realpathSync.native(managedHooksPath).replace(/\\/g, '/')}:session_start:0:0`
const canonicalPermissionHeader = hookTrustHeader(
`${managedHooksPath}:permission_request:0:0`
)
const legacyPermissionHeader = `[hooks.state."${escapeTomlBasicString(
`${realpathSync.native(managedHooksPath).replace(/\\/g, '/')}:permission_request:0:0`
)}"]`
const installedToml = readFileSync(runtimeTomlPath, 'utf-8')
expect(installedToml).toContain(canonicalSessionStartHeader)
expect(installedToml).toContain(canonicalPermissionHeader)
writeFileSync(
runtimeTomlPath,
installedToml.replace(canonicalSessionStartHeader, legacySessionStartHeader),
installedToml.replace(canonicalPermissionHeader, legacyPermissionHeader),
'utf-8'
)
const legacyToml = readFileSync(runtimeTomlPath, 'utf-8')
expect(legacyToml).toContain(legacySessionStartHeader)
expect(legacyToml).toContain(legacyPermissionHeader)
expect(service.getStatus().state).toBe('installed')
expect(service.install().state).toBe('installed')
const repairedToml = readFileSync(runtimeTomlPath, 'utf-8')
expect(repairedToml).not.toContain(legacySessionStartHeader)
expect(repairedToml).toContain(canonicalSessionStartHeader)
expect(repairedToml).not.toContain(legacyPermissionHeader)
expect(repairedToml).toContain(canonicalPermissionHeader)
expect(service.getStatus().state).toBe('installed')
}
)
it('repairs duplicate managed SessionStart trust tables on restart install', () => {
it('repairs duplicate managed PermissionRequest trust tables on restart install', () => {
const systemCodexHome = join(tmpHome, '.codex')
mkdirSync(systemCodexHome, { recursive: true })
writeFileSync(join(systemCodexHome, 'config.toml'), 'model = "system-model"\n', 'utf-8')
@ -1237,21 +1260,24 @@ describe('CodexHookService', () => {
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
const managedHooksPath = join(managedCodexHome, 'hooks.json')
const runtimeTomlPath = join(managedCodexHome, 'config.toml')
const sessionStartHeader = hookTrustHeader(`${managedHooksPath}:session_start:0:0`)
const permissionRequestHeader = hookTrustHeader(`${managedHooksPath}:permission_request:0:0`)
const installedToml = readFileSync(runtimeTomlPath, 'utf-8')
const sessionStartIndex = installedToml.indexOf(sessionStartHeader)
expect(sessionStartIndex).not.toBe(-1)
const permissionRequestIndex = installedToml.indexOf(permissionRequestHeader)
expect(permissionRequestIndex).not.toBe(-1)
const nextHeaderIndex = installedToml.indexOf(
'\n[',
sessionStartIndex + sessionStartHeader.length
permissionRequestIndex + permissionRequestHeader.length
)
const sessionStartBlock = installedToml
.slice(sessionStartIndex, nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex)
const permissionRequestBlock = installedToml
.slice(
permissionRequestIndex,
nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex
)
.trimEnd()
const staleDisabledBlock = sessionStartBlock
const staleDisabledBlock = permissionRequestBlock
.replace('enabled = true', 'enabled = false')
.replace(/trusted_hash = "[^"]+"/, 'trusted_hash = "sha256:STALE_DISABLED"')
const staleEnabledBlock = sessionStartBlock.replace(
const staleEnabledBlock = permissionRequestBlock.replace(
/trusted_hash = "[^"]+"/,
'trusted_hash = "sha256:STALE_ENABLED"'
)
@ -1259,20 +1285,20 @@ describe('CodexHookService', () => {
runtimeTomlPath,
`${installedToml.slice(
0,
sessionStartIndex
permissionRequestIndex
)}${staleDisabledBlock}\n\n${staleEnabledBlock}${installedToml.slice(
nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex
)}`,
'utf-8'
)
expect(readFileSync(runtimeTomlPath, 'utf-8').split(sessionStartHeader)).toHaveLength(3)
expect(readFileSync(runtimeTomlPath, 'utf-8').split(permissionRequestHeader)).toHaveLength(3)
// Why: preserving `enabled = false` is the repair contract; status can be
// partial because the user-disabled managed hook remains disabled.
expect(['installed', 'partial']).toContain(service.install().state)
const repairedToml = readFileSync(runtimeTomlPath, 'utf-8')
expect(repairedToml.split(sessionStartHeader)).toHaveLength(2)
expect(repairedToml.split(permissionRequestHeader)).toHaveLength(2)
expect(repairedToml).toContain('enabled = false')
expect(repairedToml).not.toContain('STALE_DISABLED')
expect(repairedToml).not.toContain('STALE_ENABLED')

View File

@ -59,6 +59,16 @@ const CODEX_EVENTS = [
'Stop'
] as const
const CODEX_WINDOWS_LOCAL_EVENTS = ['PreToolUse', 'PermissionRequest', 'PostToolUse'] as const
function getLocalCodexManagedEvents(): readonly (typeof CODEX_EVENTS)[number][] {
// Why: Codex renders synchronous lifecycle hooks as prominent TUI progress
// rows on Windows, where even fast hook processes leave multi-second stale
// screen fragments. Keep the tool/permission hooks Orca needs for live
// status and approvals, but avoid chat lifecycle hooks in local Windows PTYs.
return process.platform === 'win32' ? CODEX_WINDOWS_LOCAL_EVENTS : CODEX_EVENTS
}
function getConfigPath(): string {
return join(getOrcaManagedCodexHomePath(), 'hooks.json')
}
@ -86,9 +96,9 @@ const CODEX_EVENT_LABEL: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel>
Stop: 'stop'
}
const CODEX_MANAGED_EVENT_LABELS = new Set<CodexEventLabel>(
CODEX_EVENTS.map((eventName) => CODEX_EVENT_LABEL[eventName])
)
function getLocalCodexManagedEventLabels(): Set<CodexEventLabel> {
return new Set(getLocalCodexManagedEvents().map((eventName) => CODEX_EVENT_LABEL[eventName]))
}
const CODEX_HOOK_EVENT_LABEL: Record<string, CodexEventLabel> = {
...CODEX_EVENT_LABEL,
@ -450,7 +460,7 @@ function moveMirroredRuntimeUserTrustAfterManagedStatusHook(
entries: readonly MirroredRuntimeUserHookTrustEntry[]
): MirroredRuntimeUserHookTrustEntry[] {
return entries.map(({ entry, enabled }) => {
if (!CODEX_MANAGED_EVENT_LABELS.has(entry.eventLabel)) {
if (!getLocalCodexManagedEventLabels().has(entry.eventLabel)) {
return { entry, enabled }
}
return {
@ -655,9 +665,11 @@ function removeRuntimeManagedHookTrustEntries(configPath: string): void {
// recognize (and clean up) its own managed trust entries.
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
}
const expectedHash = computeTrustedHash(expectedEntry)
const legacyHash = computeTrustedHash({ ...expectedEntry, timeoutSec: undefined })
if (state.trustedHash !== expectedHash && state.trustedHash !== legacyHash) {
const recognizedHashes = new Set([
computeTrustedHash(expectedEntry),
computeTrustedHash({ ...expectedEntry, timeoutSec: undefined })
])
if (!state.trustedHash || !recognizedHashes.has(state.trustedHash)) {
continue
}
ourKeys.push(key)
@ -763,7 +775,7 @@ export class CodexHookService {
const trustMissing: string[] = []
const disabled: string[] = []
let presentCount = 0
for (const eventName of CODEX_EVENTS) {
for (const eventName of getLocalCodexManagedEvents()) {
const definitions = Array.isArray(config.hooks?.[eventName]) ? config.hooks![eventName]! : []
// Why: older installs appended this command, while current installs
// prepend it. Picking the last match keeps status repair conservative
@ -867,7 +879,8 @@ export class CodexHookService {
const command = getManagedCommand(scriptPath)
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand)
const nextHooks = hookPlan.hooks
const managedEvents = new Set<string>(CODEX_EVENTS)
const localManagedEvents = getLocalCodexManagedEvents()
const managedEvents = new Set<string>(localManagedEvents)
// Why: sweep managed entries out of events we no longer subscribe to
// (e.g., PreToolUse from a prior install). Without this, users who
@ -900,7 +913,7 @@ export class CodexHookService {
hookPlan.trustEntries
)
const trustEntries: CodexTrustEntry[] = mirroredUserTrustEntries.map(({ entry }) => entry)
for (const eventName of CODEX_EVENTS) {
for (const eventName of localManagedEvents) {
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
const cleaned = removeManagedCommands(current, isManagedCommand)
const definition: HookDefinition = {