fix(claude): make managed hook paths portable (STA-3348) (#13442)

* fix(claude): make managed hook paths portable

* perf(claude): keep portable hooks shell-native

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-08-11 14:03:16 -07:00
committed by GitHub
co-authored by Brennan Benson
parent d6e1d84235
commit f226fcfc4b
9 changed files with 164 additions and 98 deletions
@@ -5,6 +5,7 @@ import {
accessSync,
chmodSync,
constants as fsConstants,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
@@ -311,43 +312,37 @@ async function verifyForwarding(scripts, home, payload) {
}
}
// Why: rewrite from the path embedded in the installed command, not a
// reconstructed join(home, ...). That way missing/failing-script cases cannot
// silently re-run the real script if the install layout changes.
function rewriteLauncherScriptPath(command, nextPath) {
const match = /if \[ -f '([^']+)'/.exec(command)
if (!match) {
throw new Error('Installed launcher command did not reference a quoted script path')
}
return command.replaceAll(match[1], nextPath)
}
async function verifyInstalledLauncher(home, payload) {
const settingsPath = join(home, '.claude', 'settings.json')
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'))
const command = findStrings(settings).find(
(value) => value.includes('claude-hook.sh') && value.includes('if [ -f ')
)
if (!command || !command.includes('] && [ -r ') || !command.includes('else cat >/dev/null')) {
if (
!command ||
!command.includes('"$HOME/.orca/agent-hooks/claude-hook.sh"') ||
!command.includes('] && [ -r ') ||
!command.includes('else { command -p cat')
) {
throw new Error('Electron did not install the guarded Claude launcher')
}
const scratch = mkdtempSync(join(tmpdir(), 'orca-hook-launcher-'))
try {
const missingPath = join(scratch, 'missing-hook.sh')
const missingResult = await runShell(
rewriteLauncherScriptPath(command, missingPath),
command,
payload,
withoutOrcaEnvironment({ HOME: home })
withoutOrcaEnvironment({ HOME: scratch })
)
assertSuccessfulWrite(missingResult, 'installed missing-script launcher')
const failingPath = join(scratch, 'failing-hook.sh')
const failingPath = join(scratch, '.orca', 'agent-hooks', 'claude-hook.sh')
mkdirSync(join(scratch, '.orca', 'agent-hooks'), { recursive: true })
writeFileSync(failingPath, '#!/bin/sh\ncat >/dev/null\nexit 7\n', 'utf8')
chmodSync(failingPath, 0o755)
const failingResult = await runShell(
rewriteLauncherScriptPath(command, failingPath),
command,
payload,
withoutOrcaEnvironment({ HOME: home })
withoutOrcaEnvironment({ HOME: scratch })
)
if (failingResult.exitCode !== 7 || failingResult.stdinErrors.length > 0) {
throw new Error('Installed launcher did not preserve a running script failure')
+1
View File
@@ -13,6 +13,7 @@
"../src/main/agent-hooks/managed-agent-hook-controls.ts",
"../src/main/agent-hooks/managed-agent-hook-registry.ts",
"../src/main/agent-hooks/managed-hook-script-refresh.ts",
"../src/main/agent-hooks/runtime-home-hook-command.ts",
"../src/main/amp/hook-service.ts",
"../src/main/antigravity/hook-service.ts",
"../src/main/claude/hook-settings.ts",
+77 -18
View File
@@ -24,7 +24,6 @@ import {
removeManagedCommands,
wrapPosixHookCommand,
wrapWindowsCmdHookCommand,
wrapWindowsGitBashHookCommand,
readHooksJsonWithRaw,
wrapWindowsHookCommand,
writeManagedScript,
@@ -32,6 +31,7 @@ import {
type HooksConfig
} from './installer-utils'
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from './hook-stdin-contract'
import { wrapRuntimeHomeHookCommand } from './runtime-home-hook-command'
let tmpDir: string
let configPath: string
@@ -648,28 +648,87 @@ describe('wrapWindowsCmdHookCommand', () => {
})
})
describe('wrapWindowsGitBashHookCommand', () => {
it('guards the forward-slash fast path and drains when missing', () => {
expect(
wrapWindowsGitBashHookCommand('C:\\Users\\alice\\.orca\\agent-hooks\\claude-hook.cmd')
).toBe(
`if [ -f 'C:/Users/alice/.orca/agent-hooks/claude-hook.cmd' ]; then 'C:/Users/alice/.orca/agent-hooks/claude-hook.cmd'; else ${POSIX_HOOK_STDIN_DRAIN_COMMAND}; fi`
describe('wrapRuntimeHomeHookCommand', () => {
it('selects the runtime platform variant under HOME', () => {
const command = wrapRuntimeHomeHookCommand('claude-hook')
expect(command).toContain('case "${OSTYPE-}" in msys*|cygwin*|win32*)')
expect(command).toContain('case "$HOME" in *\\&*|*\\^*|*\\(*|*\\)*|*\\;*|*,*|*=*|*%*|*\\!*)')
expect(command).not.toContain('uname')
expect(command).toContain('"$HOME/.orca/agent-hooks/claude-hook.cmd"')
expect(command).toContain('/bin/sh "$HOME/.orca/agent-hooks/claude-hook.sh"')
expect(command).not.toMatch(/[A-Z]:[\\/]|\/Users\/|\/home\//)
})
it('rejects a script base name that could inject shell syntax', () => {
expect(() => wrapRuntimeHomeHookCommand('claude-hook; echo injected')).toThrow(
'Invalid managed script base name'
)
})
it('falls back to the encoded launcher when bash would split the path', () => {
const scriptPath = 'C:\\Users\\Jane Doe\\.orca\\agent-hooks\\claude-hook.cmd'
const command = wrapWindowsGitBashHookCommand(scriptPath)
expect(command).toMatch(qualifiedWindowsPowerShellCommand)
expect(decodeWindowsHookCommand(command)).toBe(expectedDecodedWindowsHookCommand(scriptPath))
it('executes the destination HOME script for the current runtime', () => {
const sourceHome = join(tmpDir, 'source profile')
const destinationHome = join(tmpDir, "destination $HOME ' & profile")
const sourceScriptDir = join(sourceHome, '.orca', 'agent-hooks')
const destinationScriptDir = join(destinationHome, '.orca', 'agent-hooks')
mkdirSync(sourceScriptDir, { recursive: true })
mkdirSync(destinationScriptDir, { recursive: true })
const windowsExitCode = process.platform === 'win32' ? 7 : 9
const posixExitCode = process.platform === 'win32' ? 9 : 7
writeFileSync(
join(destinationScriptDir, 'claude-hook.cmd'),
`@echo off\r\nexit /b ${windowsExitCode}\r\n`,
'utf-8'
)
writeFileSync(
join(destinationScriptDir, 'claude-hook.sh'),
`#!/bin/sh\nexit ${posixExitCode}\n`,
'utf-8'
)
writeFileSync(join(sourceScriptDir, 'claude-hook.cmd'), '@echo off\r\nexit /b 9\r\n', 'utf-8')
writeFileSync(join(sourceScriptDir, 'claude-hook.sh'), '#!/bin/sh\nexit 9\n', 'utf-8')
chmodSync(join(destinationScriptDir, 'claude-hook.sh'), 0o755)
const shell =
process.platform === 'win32'
? join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Git', 'bin', 'bash.exe')
: '/bin/sh'
const result = spawnSync(shell, ['-c', wrapRuntimeHomeHookCommand('claude-hook')], {
env: {
...process.env,
HOME: destinationHome.replaceAll('\\', '/'),
USERPROFILE: destinationHome
}
})
expect(result.error).toBeUndefined()
expect(result.status, result.stderr.toString()).toBe(7)
})
it('falls back to the encoded launcher when bash metacharacters are present', () => {
const scriptPath = 'C:\\Users\\alice & bob\\.orca\\agent-hooks\\claude-hook.cmd'
const command = wrapWindowsGitBashHookCommand(scriptPath)
expect(command).toMatch(qualifiedWindowsPowerShellCommand)
expect(command).not.toContain('& bob')
expect(decodeWindowsHookCommand(command)).toBe(expectedDecodedWindowsHookCommand(scriptPath))
it.skipIf(process.platform !== 'win32')('keeps common Windows profiles on the fast path', () => {
const destinationHome = join(tmpDir, 'destination 国際 profile')
const scriptDir = join(destinationHome, '.orca', 'agent-hooks')
mkdirSync(scriptDir, { recursive: true })
writeFileSync(join(scriptDir, 'claude-hook.cmd'), '@echo off\r\nexit /b 7\r\n', 'utf-8')
const gitBash = join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Git', 'bin', 'bash.exe')
const result = spawnSync(gitBash, ['-c', wrapRuntimeHomeHookCommand('claude-hook')], {
env: { ...process.env, HOME: destinationHome.replaceAll('\\', '/') }
})
expect(result.error).toBeUndefined()
expect(result.status, result.stderr.toString()).toBe(7)
})
it('drains stdin when HOME is unavailable', () => {
const command = `unset HOME; ${wrapRuntimeHomeHookCommand('claude-hook')}`
const shell =
process.platform === 'win32'
? join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Git', 'bin', 'bash.exe')
: '/bin/sh'
const result = spawnSync(shell, ['-c', command], { input: Buffer.alloc(1_000_000, 'x') })
expect(result.error).toBeUndefined()
expect(result.status).toBe(0)
})
})
-10
View File
@@ -152,16 +152,6 @@ export function wrapWindowsCmdHookCommand(scriptPath: string): string {
return WINDOWS_CMD_SAFE_PATH.test(scriptPath) ? scriptPath : wrapWindowsHookCommand(scriptPath)
}
export const WINDOWS_GIT_BASH_SAFE_PATH = /^[A-Za-z0-9_.:/~-]+$/
export function wrapWindowsGitBashHookCommand(scriptPath: string): string {
const bashPath = scriptPath.replaceAll('\\', '/')
// Why: Claude's Git Bash runner can execute a forward-slash .cmd directly; unsafe paths stay encoded.
return WINDOWS_GIT_BASH_SAFE_PATH.test(bashPath)
? `if [ -f ${quotePosixShellString(bashPath)} ]; then ${quotePosixShellString(bashPath)}; else ${POSIX_HOOK_STDIN_DRAIN_COMMAND}; fi`
: wrapWindowsHookCommand(scriptPath)
}
/**
* Extra form lines inserted before the final `payload@-` line (each should end with ` ^`).
* Used by Grok to attach `grokHome` without fragile string replace on the shared template.
@@ -79,12 +79,9 @@ import { GeminiHookService } from '../gemini/hook-service'
import { GrokHookService } from '../grok/hook-service'
import { KimiHookService } from '../kimi/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import {
wrapPosixHookCommand,
wrapWindowsGitBashHookCommand,
wrapWindowsHookCommand
} from './installer-utils'
import { wrapPosixHookCommand, wrapWindowsHookCommand } from './installer-utils'
import { POSIX_HOOK_STDIN_READER } from './hook-stdin-contract'
import { wrapRuntimeHomeHookCommand } from './runtime-home-hook-command'
import { createAgentHookMemorySftp } from './agent-hook-memory-sftp.test-fixture'
const REMOTE_HOME = '/home/dev'
@@ -387,9 +384,9 @@ describe('Windows managed hook stdin structure', () => {
args: ['/d', '/c', wrapWindowsHookCommand(missingScript)]
},
{
name: 'Git Bash fast path',
name: 'portable Git Bash launcher',
executable: gitBash,
args: ['-lc', wrapWindowsGitBashHookCommand(missingScript)]
args: ['-lc', wrapRuntimeHomeHookCommand('missing-orca-hook')]
}
]
for (const launcher of launcherCases) {
@@ -0,0 +1,22 @@
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from './hook-stdin-contract'
const MANAGED_SCRIPT_BASE_NAME = /^[A-Za-z0-9_-]+$/
const WINDOWS_GIT_BASH_RUNTIME_HOME_UNSAFE = '*\\&*|*\\^*|*\\(*|*\\)*|*\\;*|*,*|*=*|*%*|*\\!*'
export function wrapRuntimeHomeHookCommand(scriptBaseName: string): string {
if (!MANAGED_SCRIPT_BASE_NAME.test(scriptBaseName)) {
throw new Error(`Invalid managed script base name: ${scriptBaseName}`)
}
const windowsScript = `"$HOME/.orca/agent-hooks/${scriptBaseName}.cmd"`
const posixScript = `"$HOME/.orca/agent-hooks/${scriptBaseName}.sh"`
const drain = POSIX_HOOK_STDIN_DRAIN_COMMAND
const powershell = '"$SYSTEMROOT/System32/WindowsPowerShell/v1.0/powershell.exe"'
const powershellCommand = `$homePath = $env:HOME -replace '^/([A-Za-z])/', '$1:/'; $scriptPath = Join-Path $homePath '.orca\\agent-hooks\\${scriptBaseName}.cmd'; if (Test-Path -LiteralPath $scriptPath -PathType Leaf) { & $scriptPath; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null; exit 0`
const encodedCommand = Buffer.from(powershellCommand, 'utf16le').toString('base64')
const powershellInvocation = `${powershell} -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`
const encodedWindowsBranch = `if [ -f ${powershell} ]; then ${powershellInvocation}; else ${drain}; fi`
const windowsBranch = `if [ -f ${windowsScript} ]; then case "$HOME" in ${WINDOWS_GIT_BASH_RUNTIME_HOME_UNSAFE}) ${encodedWindowsBranch} ;; *) ${windowsScript} ;; esac; else ${drain}; fi`
const posixBranch = `if [ -f ${posixScript} ] && [ -r ${posixScript} ] && [ -x ${posixScript} ]; then /bin/sh ${posixScript}; else ${drain}; fi`
// Why: OSTYPE is shell-owned, so platform selection adds no process to every hook invocation.
return `if [ -z "\${HOME-}" ]; then ${drain}; else case "\${OSTYPE-}" in msys*|cygwin*|win32*) ${windowsBranch} ;; *) ${posixBranch} ;; esac; fi`
}
+31 -18
View File
@@ -34,15 +34,14 @@ function hasManagedCommand(hook: TestHook, matcher: (command: string | undefined
}
describe('getWindowsManagedLifecycleHook', () => {
it('keeps cmd metacharacters out of the headless client command line', () => {
it('resolves the managed script from the runtime Windows profile', () => {
const scriptPath = 'C:\\Users\\%name%\\a^b&c\\.orca\\agent-hooks\\claude-hook.cmd'
const hook = getWindowsManagedLifecycleHook(scriptPath)
const encodedCommand = hook.args?.at(-1)
expect(hook.args?.[0]).toBe('--headless')
expect(hook.args?.[1]).toMatch(/\\WindowsPowerShell\\v1\.0\\powershell\.exe$/i)
expect(hook.args?.[1]).toMatch(/\\System32\\cmd\.exe$/i)
expect(hook.args?.at(-1)).toBe('%USERPROFILE%\\.orca\\agent-hooks\\claude-hook.cmd')
expect(hook.args).not.toContain(scriptPath)
expect(Buffer.from(encodedCommand!, 'base64').toString('utf16le')).toContain(scriptPath)
})
})
@@ -185,6 +184,10 @@ describe('ClaudeHookService.install', () => {
expect(legacyHooks.map((hook: TestHook) => hook.command)).toContain(
'/usr/local/bin/user-hook'
)
const managedHook = legacyHooks.find((hook: TestHook) =>
hasManagedCommand(hook, isClaudeManagedCommand)
)
expect(JSON.stringify(managedHook)).not.toContain(tmpHome.replaceAll('\\', '/'))
expect(
legacyHooks.some((hook: TestHook) => hasManagedCommand(hook, isClaudeManagedCommand))
).toBe(true)
@@ -216,7 +219,13 @@ describe('ClaudeHookService.install', () => {
readFileSync(join(tmpHome, '.claude', 'settings.json'), 'utf-8')
) as { statusLine?: { type: string; command: string } }
expect(settings.statusLine?.type).toBe('command')
expect(settings.statusLine?.command).toContain('claude-statusline')
expect(settings.statusLine?.command).toContain(
'"$HOME/.orca/agent-hooks/claude-statusline.cmd"'
)
expect(settings.statusLine?.command).toContain(
'"$HOME/.orca/agent-hooks/claude-statusline.sh"'
)
expect(settings.statusLine?.command).not.toContain(tmpHome.replaceAll('\\', '/'))
const script = readFileSync(
join(tmpHome, '.orca', 'agent-hooks', STATUSLINE_SCRIPT_FILE_NAME),
@@ -330,7 +339,7 @@ describe('ClaudeHookService.install', () => {
})
it.skipIf(process.platform !== 'win32')(
'runs managed hooks through headless exec form and preserves profile-path spaces',
'runs portable managed hooks through headless exec form',
() => {
const tmpHome = mkdtempSync(join(tmpdir(), 'orca claude home with spaces '))
vi.stubEnv('HOME', tmpHome)
@@ -344,15 +353,22 @@ describe('ClaudeHookService.install', () => {
const system32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
const scriptPath = join(tmpHome, '.orca', 'agent-hooks', CLAUDE_SCRIPT_FILE_NAME)
const runtimeScriptPath = join(
'%USERPROFILE%',
'.orca',
'agent-hooks',
CLAUDE_SCRIPT_FILE_NAME
)
for (const eventName of ['UserPromptSubmit', 'Stop', 'StopFailure']) {
const hook = settings.hooks[eventName]?.[0]?.hooks?.[0]
expect(hook).toEqual({
type: 'command',
command: join(system32, 'conhost.exe'),
args: ['--headless', join(system32, 'cmd.exe'), '/d', '/c', scriptPath],
args: ['--headless', join(system32, 'cmd.exe'), '/d', '/c', runtimeScriptPath],
timeout: 10
})
expect(hook.args).not.toContain(scriptPath)
}
} finally {
vi.unstubAllEnvs()
@@ -395,11 +411,7 @@ describe('ClaudeHookService.installRemote', () => {
const settings = fs.files.get('/home/dev/.claude/settings.json')
expect(settings).toBeTruthy()
const parsed = JSON.parse(settings!)
// Why: every load-bearing event must be present and point at the
// remote-shaped script path with the guarded launcher applied. Drift in
// any of these is a real bug — Claude
// Code rejects unknown shapes silently and the agent-hooks pipeline
// goes dark.
// Why: Claude silently rejects drifted hook shapes, so assert every load-bearing event.
for (const event of [
// Why: SessionStart is the only signal a resumed/idle session ever emits;
// without it the sidebar row waits for the first prompt (STA-3386).
@@ -417,8 +429,8 @@ describe('ClaudeHookService.installRemote', () => {
]) {
expect(parsed.hooks[event]).toBeTruthy()
const cmd = parsed.hooks[event][0].hooks[0].command as string
expect(cmd).toContain('/home/dev/.orca/agent-hooks/claude-hook.sh')
expect(cmd).toMatch(/^if \[ -f /)
expect(cmd).toContain('"$HOME/.orca/agent-hooks/claude-hook.sh"')
expect(cmd).not.toContain('/home/dev/.orca/agent-hooks/claude-hook.sh')
}
// Managed script body
const script = fs.files.get('/home/dev/.orca/agent-hooks/claude-hook.sh')
@@ -510,9 +522,9 @@ describe('OpenClaudeHookService-compatible install', () => {
for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure']) {
const command = parsed.hooks[event][0].hooks[0].command as string
expect(isOpenClaudeManagedCommand(command)).toBe(true)
if (process.platform !== 'win32') {
expect(command).toMatch(/^if \[ -f /)
}
expect(command).toContain('"$HOME/.orca/agent-hooks/openclaude-hook.cmd"')
expect(command).toContain('"$HOME/.orca/agent-hooks/openclaude-hook.sh"')
expect(command).not.toContain(tmpHome.replaceAll('\\', '/'))
}
expect(
readFileSync(join(tmpHome, '.orca', 'agent-hooks', OPENCLAUDE_SCRIPT_FILE_NAME), 'utf-8')
@@ -541,7 +553,8 @@ describe('OpenClaudeHookService-compatible install', () => {
})
const parsed = JSON.parse(fs.files.get('/home/dev/.openclaude/settings.json')!)
const command = parsed.hooks.StopFailure[0].hooks[0].command as string
expect(command).toContain('/home/dev/.orca/agent-hooks/openclaude-hook.sh')
expect(command).toContain('"$HOME/.orca/agent-hooks/openclaude-hook.sh"')
expect(command).not.toContain('/home/dev/.orca/agent-hooks/openclaude-hook.sh')
expect(fs.files.get('/home/dev/.orca/agent-hooks/openclaude-hook.sh')).toContain('/hook/claude')
})
})
+1 -1
View File
@@ -262,7 +262,7 @@ export class ClaudeHookService {
}
}
// Why: the POSIX wrapper is identical regardless of where the script lands; only the path differs.
// Why: settings resolve HOME at runtime while SFTP still targets the discovered remote home.
const hook = buildManagedCommandHook(getRemoteManagedCommand(remoteScriptPath))
const nextConfig = applyManagedHooks(config, hook, remoteScriptFileName)
+15 -26
View File
@@ -1,5 +1,5 @@
import { homedir } from 'node:os'
import { join, win32 } from 'node:path'
import { basename, extname, join, win32 } from 'node:path'
import {
buildManagedCommandHook,
createManagedCommandMatcher,
@@ -7,13 +7,11 @@ import {
isPlainObject,
MANAGED_HOOK_TIMEOUT_SECONDS,
removeManagedCommands,
wrapPosixHookCommand,
wrapWindowsGitBashHookCommand,
wrapWindowsHookCommand,
type HookCommandConfig,
type HookDefinition,
type HooksConfig
} from '../agent-hooks/installer-utils'
import { wrapRuntimeHomeHookCommand } from '../agent-hooks/runtime-home-hook-command'
export type ClaudeCompatibleHookSettings = {
configDirName: '.claude' | '.openclaude'
@@ -111,9 +109,11 @@ export function getRemoteConfigPath(remoteHome: string, settings = CLAUDE_HOOK_S
}
export function getManagedCommand(scriptPath: string): string {
return process.platform === 'win32'
? wrapWindowsGitBashHookCommand(scriptPath)
: wrapPosixHookCommand(scriptPath)
const scriptFileName = basename(scriptPath)
const extension = extname(scriptFileName)
return wrapRuntimeHomeHookCommand(
extension ? scriptFileName.slice(0, -extension.length) : scriptFileName
)
}
export function getManagedLifecycleHook(
@@ -126,30 +126,19 @@ export function getManagedLifecycleHook(
return getWindowsManagedLifecycleHook(scriptPath)
}
const WINDOWS_CMD_ARGUMENT_META = /[%!^&|<>()"\r\n]/
export function getWindowsManagedLifecycleHook(scriptPath: string): HookCommandConfig {
const system32 = win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
let clientArgs = [win32.join(system32, 'cmd.exe'), '/d', '/c', scriptPath]
if (WINDOWS_CMD_ARGUMENT_META.test(scriptPath)) {
const encodedCommand = wrapWindowsHookCommand(scriptPath).match(/ -EncodedCommand (\S+)$/)?.[1]
if (!encodedCommand) {
throw new Error('Failed to encode managed Claude hook path')
}
clientArgs = [
win32.join(system32, 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-EncodedCommand',
encodedCommand
]
}
const runtimeScriptPath = win32.join(
'%USERPROFILE%',
'.orca',
'agent-hooks',
win32.basename(scriptPath)
)
// Why: Claude's Windows shell form opens Git Bash consoles; exec form hosts the client in a windowless console.
return {
type: 'command',
command: win32.join(system32, 'conhost.exe'),
args: ['--headless', ...clientArgs],
args: ['--headless', win32.join(system32, 'cmd.exe'), '/d', '/c', runtimeScriptPath],
timeout: MANAGED_HOOK_TIMEOUT_SECONDS
}
}
@@ -165,7 +154,7 @@ export function hasSameManagedHookInvocation(
}
export function getRemoteManagedCommand(scriptPath: string): string {
return wrapPosixHookCommand(scriptPath)
return getManagedCommand(scriptPath)
}
export function applyManagedHooks(