cursor: replace stale-form hooks on wrapper migration · Entire

cursor: replace stale-form hooks on wrapper migration

4dd4586→main· ?
Karthik Rameshkumar·4d ago·2 files·+75 added/-29 removed

Self-review of the Windows fix found that cursor.InstallHooks appended a hook only when the exact command was absent, removing existing Entire hooks only under --force. Once the sh↔cmd.exe wrapper form can change between installs (the new UseWindowsProductionHooks behavior), a non-force reinstall after a probe-result flip left the stale sh-wrapped entry in place AND added the cmd.exe one — a dead duplicate that could double-fire (duplicate session-start/stop → duplicate checkpoints) if sh later worked at hook runtime.

Add syncEntireHook (mirroring codex's syncHookCommand): if the desired command is already present it's a no-op; otherwise any existing Entire hook of that type — in any wrapper form — is removed before appending, so a changed wrapper cleanly replaces the stale one without --force. Non-Entire entries are preserved. Add a migration regression test asserting no sh-based wrapper survives an sh→cmd.exe reinstall.

Sessions

01KXDHJ1CEY1DF00642NVQESRPView transcript

Changes

2

148 unmodified lines

149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
175 unmodified lines

343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363

148 unmodified lines

count := 0

// Add hooks if they don't exist
    if !hookCommandExists(sessionStart, sessionStartCmd) {
        sessionStart = append(sessionStart, CursorHookEntry{Command: sessionStartCmd})
        count++
    }
    if !hookCommandExists(sessionEnd, sessionEndCmd) {
        sessionEnd = append(sessionEnd, CursorHookEntry{Command: sessionEndCmd})
        count++
    }
    if !hookCommandExists(beforeSubmitPrompt, beforeSubmitPromptCmd) {
        beforeSubmitPrompt = append(beforeSubmitPrompt, CursorHookEntry{Command: beforeSubmitPromptCmd})
        count++
    }
    if !hookCommandExists(stop, stopCmd) {
        stop = append(stop, CursorHookEntry{Command: stopCmd})
        count++
    }
    if !hookCommandExists(preCompact, preCompactCmd) {
        preCompact = append(preCompact, CursorHookEntry{Command: preCompactCmd})
        count++
    }
    if !hookCommandExists(subagentStart, subagentStartCmd) {
        subagentStart = append(subagentStart, CursorHookEntry{Command: subagentStartCmd})
        count++
    }
    if !hookCommandExists(subagentStop, subagentEndCmd) {
        subagentStop = append(subagentStop, CursorHookEntry{Command: subagentEndCmd})
        count++
    }
    // Sync each hook to its desired command. syncEntireHook replaces any
    // stale-form Entire hook (e.g. an sh-wrapped entry from a previous install)
    // with the current command even without --force, so a wrapper-form change —
    // notably the sh↔cmd.exe migration driven by UseWindowsProductionHooks when
    // a Windows host gains or loses a working POSIX sh — cleanly replaces rather
    // than leaving a dead duplicate entry that could double-fire (issue #1424).
    sessionStart, count = syncEntireHook(sessionStart, sessionStartCmd, count)
    sessionEnd, count = syncEntireHook(sessionEnd, sessionEndCmd, count)
    beforeSubmitPrompt, count = syncEntireHook(beforeSubmitPrompt, beforeSubmitPromptCmd, count)
    stop, count = syncEntireHook(stop, stopCmd, count)
    preCompact, count = syncEntireHook(preCompact, preCompactCmd, count)
    subagentStart, count = syncEntireHook(subagentStart, subagentStartCmd, count)
    subagentStop, count = syncEntireHook(subagentStop, subagentEndCmd, count)

if count == 0 {
        return 0, nil
    }
175 unmodified lines

// Helper functions for hook management

// syncEntireHook ensures entries contains exactly the given Entire hook command
// for this hook type. If command is already present it is a no-op. Otherwise any
// existing Entire hook (in any wrapper form) is removed before appending command,
// so a changed wrapper form replaces the stale one rather than duplicating it.
// Non-Entire entries are preserved. count is incremented when a change is made.
func syncEntireHook(entries []CursorHookEntry, command string, count int) ([]CursorHookEntry, int) {
    if hookCommandExists(entries, command) {
        return entries, count
    }
    if hasEntireHook(entries) {
        entries = removeEntireHooks(entries)
    }
    return append(entries, CursorHookEntry{Command: command}), count + 1
}

func hookCommandExists(entries []CursorHookEntry, command string) bool {
    for _, entry := range entries {
        if entry.Command == command {