Fix Codex Windows hook fallback · Entire

Fix Codex Windows hook fallback

a4d1ec9→main

peyton-alt·2w ago·4 files·+287 added/-20 removed

Sessions

5ac1e71750f3View transcript

[?
Fix Codex Windows Hook FallbackCodex·1 step](/content/gh/entireio/cli/session/019f0998-b0f5-7202-9db6-34d41fd9be38#timeline-5ac1e71750f3/index.html)

Changes

4

3 unmodified lines

4
5
6
7
8
9
10
11
12
13
14
15
16
11 unmodified lines

28
29
30
31
32
33
34
35
36
37
38
39
55 unmodified lines

95
96
97
98
99
89
100
101
102
103
104
105
95
96
97
106
107
108
109
110
111
112
102
103
113
114
115
116
106
107
117
118
119
120
110
111
121
122
123
124
114
115
125
126
127
128
129
47 unmodified lines

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
124 unmodified lines

338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353

3 unmodified lines

"context"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"
    "time"

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/jsonutil"
11 unmodified lines

`go run "$(git rev-parse --show-toplevel)"/cmd/entire/main.go `,
}

var (
    codexHookCommandOS                 = runtime.GOOS
    codexProductionHookWrapperWorks    = defaultCodexProductionHookWrapperWorks
    codexHookWrapperCompatibilityProbe = `sh -c 'exit 0'`
)

// InstallHooks installs Codex hooks in .codex/hooks.json.
func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool) (int, error) {
    repoRoot, err := paths.WorktreeRoot(ctx)
55 unmodified lines

cmdPrefix = "entire hooks codex "
    }
    sessionStartCmd := cmdPrefix + "session-start"
    useWindowsProductionHooks := shouldUseWindowsProductionCodexHooks(ctx, localDev)
    if !localDev {
        sessionStartCmd = agent.WrapProductionJSONWarningHookCommand(sessionStartCmd, agent.WarningFormatSingleLine)
        sessionStartCmd = wrapCodexSessionStartHookCommand(sessionStartCmd, useWindowsProductionHooks)
    }
    userPromptSubmitCmd := cmdPrefix + "user-prompt-submit"
    stopCmd := cmdPrefix + "stop"
    postToolUseCmd := cmdPrefix + "post-tool-use"
    if !localDev {
        userPromptSubmitCmd = agent.WrapProductionSilentHookCommand(userPromptSubmitCmd)
        stopCmd = agent.WrapProductionSilentHookCommand(stopCmd)
        postToolUseCmd = agent.WrapProductionSilentHookCommand(postToolUseCmd)
        userPromptSubmitCmd = wrapCodexSilentHookCommand(userPromptSubmitCmd, useWindowsProductionHooks)
        stopCmd = wrapCodexSilentHookCommand(stopCmd, useWindowsProductionHooks)
        postToolUseCmd = wrapCodexSilentHookCommand(postToolUseCmd, useWindowsProductionHooks)
    }

count := 0

if !hookCommandExists(sessionStart, sessionStartCmd) {
        sessionStart = addHook(sessionStart, sessionStartCmd)
    if updated, changed := syncHookCommand(sessionStart, sessionStartCmd); changed {
        sessionStart = updated
        count++
    }
    if !hookCommandExists(userPromptSubmit, userPromptSubmitCmd) {
        userPromptSubmit = addHook(userPromptSubmit, userPromptSubmitCmd)
    if updated, changed := syncHookCommand(userPromptSubmit, userPromptSubmitCmd); changed {
        userPromptSubmit = updated
        count++
    }
    if !hookCommandExists(stop, stopCmd) {
        stop = addHook(stop, stopCmd)
    if updated, changed := syncHookCommand(stop, stopCmd); changed {
        stop = updated
        count++
    }
    if !hookCommandExists(postToolUse, postToolUseCmd) {
        postToolUse = addHook(postToolUse, postToolUseCmd)
    if updated, changed := syncHookCommand(postToolUse, postToolUseCmd); changed {
        postToolUse = updated
        count++
    }

47 unmodified lines

return count, nil
}

func shouldUseWindowsProductionCodexHooks(ctx context.Context, localDev bool) bool {
    if localDev || codexHookCommandOS != "windows" {
        return false
    }
    return !codexProductionHookWrapperWorks(ctx, codexHookWrapperCompatibilityProbe)
}

func wrapCodexSessionStartHookCommand(command string, useWindows bool) string {
    if useWindows {
        return agent.WrapWindowsProductionJSONWarningHookCommand(command, agent.WarningFormatSingleLine)
    }
    return agent.WrapProductionJSONWarningHookCommand(command, agent.WarningFormatSingleLine)
}

func wrapCodexSilentHookCommand(command string, useWindows bool) string {
    if useWindows {
        return agent.WrapWindowsProductionSilentHookCommand(command)
    }
    return agent.WrapProductionSilentHookCommand(command)
}

func defaultCodexProductionHookWrapperWorks(ctx context.Context, command string) bool {
    probeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

cmd := exec.CommandContext(probeCtx, "cmd.exe", "/d", "/s", "/c", command)
    cmd.Stdout = io.Discard
    cmd.Stderr = io.Discard
    return cmd.Run() == nil
}

// UninstallHooks removes Entire hooks from Codex hooks.json.
func (c *CodexAgent) UninstallHooks(ctx context.Context) error {
    repoRoot, err := paths.WorktreeRoot(ctx)
124 unmodified lines

return false
}

func syncHookCommand(groups []MatcherGroup, command string) ([]MatcherGroup, bool) {
    if hookCommandExists(groups, command) {
        return groups, false
    }
    if hasEntireHook(groups) {
        groups = removeEntireHooks(groups)
    }
    return addHook(groups, command), true
}

func addHook(groups []MatcherGroup, command string) []MatcherGroup {
    entry := HookEntry{
        Type:    "command",

Mcmd/entire/cli/agent/codex/hooks.go+64/-12


51 unmodified lines

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
262 unmodified lines

400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421

51 unmodified lines

require.Contains(t, string(projectData), "[features]")
}

func TestInstallHooks_WindowsWrapperProbeSuccessKeepsWrappedCommands(t *testing.T) {
    tempDir := setupTestEnv(t)
    withCodexHookEnvironment(t, "windows", true)

ag := &CodexAgent{}
    count, err := ag.InstallHooks(context.Background(), false, false)
    require.NoError(t, err)
    require.Equal(t, 4, count)

hooksPath := filepath.Join(tempDir, ".codex", HooksFileName)
    data, err := os.ReadFile(hooksPath)
    require.NoError(t, err)

var hooksFile HooksFile
    require.NoError(t, json.Unmarshal(data, &hooksFile))

assertHookCommand(t, hooksFile.Hooks.SessionStart, agentpkg.WrapProductionJSONWarningHookCommand("entire hooks codex session-start", agentpkg.WarningFormatSingleLine), "SessionStart")
    assertHookCommand(t, hooksFile.Hooks.UserPromptSubmit, agentpkg.WrapProductionSilentHookCommand("entire hooks codex user-prompt-submit"), "UserPromptSubmit")
    assertHookCommand(t, hooksFile.Hooks.Stop, agentpkg.WrapProductionSilentHookCommand("entire hooks codex stop"), "Stop")
    assertHookCommand(t, hooksFile.Hooks.PostToolUse, agentpkg.WrapProductionSilentHookCommand("entire hooks codex post-tool-use"), "PostToolUse")
}

func TestInstallHooks_WindowsWrapperProbeFailureUsesWindowsCommands(t *testing.T) {
    tempDir := setupTestEnv(t)
    withCodexHookEnvironment(t, "windows", false)

ag := &CodexAgent{}
    count, err := ag.InstallHooks(context.Background(), false, false)
    require.NoError(t, err)
    require.Equal(t, 4, count)

hooksPath := filepath.Join(tempDir, ".codex", HooksFileName)
    data, err := os.ReadFile(hooksPath)
    require.NoError(t, err)

var hooksFile HooksFile
    require.NoError(t, json.Unmarshal(data, &hooksFile))

assertHookCommand(t, hooksFile.Hooks.SessionStart, agentpkg.WrapWindowsProductionJSONWarningHookCommand("entire hooks codex session-start", agentpkg.WarningFormatSingleLine), "SessionStart")
    assertHookCommand(t, hooksFile.Hooks.UserPromptSubmit, agentpkg.WrapWindowsProductionSilentHookCommand("entire hooks codex user-prompt-submit"), "UserPromptSubmit")
    assertHookCommand(t, hooksFile.Hooks.Stop, agentpkg.WrapWindowsProductionSilentHookCommand("entire hooks codex stop"), "Stop")
    assertHookCommand(t, hooksFile.Hooks.PostToolUse, agentpkg.WrapWindowsProductionSilentHookCommand("entire hooks codex post-tool-use"), "PostToolUse")
    require.NotContains(t, string(data), "sh -c")
    require.NotContains(t, string(data), "command -v entire")
    require.Contains(t, string(data), "where.exe entire")
}

func TestInstallHooks_WindowsWrapperProbeFailureMigratesToWindowsCommands(t *testing.T) {
    tempDir := setupTestEnv(t)
    wrapperWorks := true
    withCodexHookEnvironmentFunc(t, "windows", func(context.Context, string) bool {
        return wrapperWorks
    })

ag := &CodexAgent{}
    count, err := ag.InstallHooks(context.Background(), false, false)
    require.NoError(t, err)
    require.Equal(t, 4, count)

wrapperWorks = false
    count, err = ag.InstallHooks(context.Background(), false, false)
    require.NoError(t, err)
    require.Equal(t, 4, count)

hooksPath := filepath.Join(tempDir, ".codex", HooksFileName)
    data, err := os.ReadFile(hooksPath)
    require.NoError(t, err)

var hooksFile HooksFile
    require.NoError(t, json.Unmarshal(data, &hooksFile))

func TestInstallHooks_Idempotent(t *testing.T) {
    setupTestEnv(t)

262 unmodified lines

}

t.Errorf("%s: expected hook command not found: %s", label, expectedCmd)
}

func withCodexHookEnvironment(t *testing.T, goos string, wrapperWorks bool) {
    t.Helper()
    withCodexHookEnvironmentFunc(t, goos, func(context.Context, string) bool {
        return wrapperWorks
    })
}

func withCodexHookEnvironmentFunc(t *testing.T, goos string, wrapperWorks func(context.Context, string) bool) {
    t.Helper()
    oldGOOS := codexHookCommandOS
    oldProbe := codexProductionHookWrapperWorks
    codexHookCommandOS = goos
    codexProductionHookWrapperWorks = wrapperWorks
    t.Cleanup(func() {
        codexHookCommandOS = oldGOOS
        codexProductionHookWrapperWorks = oldProbe
    })
}