Skip to content

Commit 6354f01

Browse files
committed
fix: preserve Windows hook escaping across nested cmd
Entire-Checkpoint: 01KYT6043WEEAEXWYQ47ABE3B5
1 parent 5c216c4 commit 6354f01

2 files changed

Lines changed: 17 additions & 13 deletions

File tree

cmd/entire/cli/agent/hook_command.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func WrapWindowsProductionJSONWarningHookCommand(command string, format WarningF
104104

105105
return fmt.Sprintf(
106106
`cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)"`,
107-
escapeWindowsCMD(string(payload)),
107+
escapeWindowsCMDForNestedCommand(string(payload)),
108108
command,
109109
)
110110
}
@@ -114,7 +114,7 @@ func WrapWindowsProductionJSONWarningHookCommand(command string, format WarningF
114114
func WrapWindowsProductionPlainTextWarningHookCommand(command string, format WarningFormat) string {
115115
return fmt.Sprintf(
116116
`cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)"`,
117-
escapeWindowsCMD(windowsPlainTextWarning(format)),
117+
escapeWindowsCMDForNestedCommand(windowsPlainTextWarning(format)),
118118
command,
119119
)
120120
}
@@ -171,16 +171,13 @@ func hasManagedHookPrefix(command string, prefixes []string) bool {
171171
return false
172172
}
173173

174-
// escapeWindowsCMD caret-escapes the cmd.exe block metacharacters that would
175-
// otherwise terminate the `(echo …)` warning block or redirect its output.
174+
// escapeWindowsCMD caret-escapes one cmd.exe parsing layer's block
175+
// metacharacters so they cannot terminate the `(echo …)` warning block or
176+
// redirect its output.
176177
//
177-
// `%` is deliberately NOT escaped. These wrappers are a `cmd.exe /d /s /c`
178-
// command line, not a batch script, so batch's `%%` doubling does not apply
179-
// (it would print a literal `%%`), and caret-escaping `%` is not a thing cmd
180-
// recognizes — `^%` would leak the caret. On the command line a lone `%` is
181-
// literal and `%NAME%` only expands for a defined environment variable, so the
182-
// fixed, %-free warning constant is emitted verbatim. If the warning text ever
183-
// gains a `%NAME%` that collides with a real env var, revisit this.
178+
// `%` is deliberately NOT escaped because caret-escaping `%` is not something
179+
// cmd.exe recognizes — `^%` would leak the caret. The fixed warning constants
180+
// are %-free; if that changes, percent expansion needs separate handling.
184181
func escapeWindowsCMD(s string) string {
185182
replacer := strings.NewReplacer(
186183
`^`, `^^`,
@@ -195,6 +192,13 @@ func escapeWindowsCMD(s string) string {
195192
return replacer.Replace(s)
196193
}
197194

195+
// escapeWindowsCMDForNestedCommand preserves metacharacter escaping through
196+
// the hook runner's outer cmd.exe /c so the nested cmd.exe /d /s /c receives
197+
// one complete escape layer of its own.
198+
func escapeWindowsCMDForNestedCommand(s string) string {
199+
return escapeWindowsCMD(escapeWindowsCMD(s))
200+
}
201+
198202
func windowsPlainTextWarning(format WarningFormat) string {
199203
return strings.Join(strings.Fields(MissingEntireWarning(format)), " ")
200204
}

cmd/entire/cli/agent/hook_command_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ func TestWrapWindowsProductionJSONWarningHookCommand(t *testing.T) {
9090
if !strings.Contains(command, "where.exe entire") {
9191
t.Fatalf("windows wrapper missing PATH guard, got %q", command)
9292
}
93-
if !strings.Contains(command, "^\"systemMessage^\"") {
94-
t.Fatalf("windows wrapper missing escaped systemMessage JSON, got %q", command)
93+
if !strings.Contains(command, "^^^\"systemMessage^^^\"") {
94+
t.Fatalf("windows wrapper missing nested-shell escaped systemMessage JSON, got %q", command)
9595
}
9696
if !strings.Contains(command, "entire hooks codex session-start") {
9797
t.Fatalf("windows wrapper missing hook target, got %q", command)

0 commit comments

Comments
 (0)