Skip to content
6 changes: 4 additions & 2 deletions CLI/CMUXCLI+TmuxCompatSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ extension CMUXCLI {

var pieces: [String] = []
if let trimmedCwd, !trimmedCwd.isEmpty {
pieces.append("cd -- \(tmuxShellQuote(resolvePath(trimmedCwd)))")
let quotedCwd = tmuxShellQuote(resolvePath(trimmedCwd))
pieces.append("cd -- \(quotedCwd)")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if !commandText.isEmpty {
pieces.append(commandText)
Expand Down Expand Up @@ -202,7 +203,8 @@ extension CMUXCLI {
"rm -f -- \"$0\" 2>/dev/null || true"
]
if let cwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines), !cwd.isEmpty {
lines.append("cd -- \(tmuxShellQuote(resolvePath(cwd))) || exit $?")
let quotedCwd = tmuxShellQuote(resolvePath(cwd))
lines.append("cd -- \(quotedCwd) || exit $?")
}
Comment on lines 205 to 208

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The tmux-compat launcher script still hard-exits when cd fails, so a deleted saved directory will kill the session before the agent can run. The PR description explicitly lists "tmux compatibility startup scripts" as fixed, and every other startup-script path in this PR (SessionRestoredTerminalCommandStore, Codex Teams in cmux.swift) now uses the guarded form, but this one does not. Workspace.swift now also suppresses passing requestedWorkingDirectory to Ghostty when restoredTmuxStartupScript != nil, which means neither Ghostty nor the script tolerates a missing cwd in this path.

Suggested change
if let cwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines), !cwd.isEmpty {
lines.append("cd -- \(tmuxShellQuote(resolvePath(cwd))) || exit $?")
let quotedCwd = tmuxShellQuote(resolvePath(cwd))
lines.append("cd -- \(quotedCwd) || exit $?")
}
if let cwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines), !cwd.isEmpty {
let quotedCwd = tmuxShellQuote(resolvePath(cwd))
lines.append("{ cd -- \(quotedCwd) 2>/dev/null || [ ! -d \(quotedCwd) ]; } || exit $?")
}

lines.append("exec \"${SHELL:-/bin/sh}\" -lc \(tmuxShellQuote(commandText))")
do {
Expand Down
15 changes: 11 additions & 4 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17228,7 +17228,8 @@ struct CMUXCLI {
"rm -f -- \"$0\" 2>/dev/null || true"
]
if let cwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines), !cwd.isEmpty {
lines.append("cd -- \(codexTeamsShellQuote(cwd)) || exit $?")
let quotedCwd = codexTeamsShellQuote(cwd)
lines.append("{ cd -- \(quotedCwd) 2>/dev/null || [ ! -d \(quotedCwd) ]; } || exit $?")
}
lines.append("exec \"${SHELL:-/bin/sh}\" -lc \(codexTeamsShellQuote(commandText))")
do {
Expand Down Expand Up @@ -23572,9 +23573,15 @@ struct CMUXCLI {
var commandParts: [String] = []
commandParts.append(contentsOf: argv)

var command = commandParts.map(cliShellQuote).joined(separator: " ")
if let cwd = normalizedHookValue(workingDirectory) {
command = "cd \(cliShellQuote(cwd)) && \(command)"
let cwd = normalizedHookValue(workingDirectory)
let sanitizedCommandParts = AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: commandParts,
workingDirectory: cwd
)
let command = sanitizedCommandParts.map(cliShellQuote).joined(separator: " ")
if let cwd {
let quotedCwd = cliShellQuote(cwd)
return "{ cd -- \(quotedCwd) 2>/dev/null || [ ! -d \(quotedCwd) ]; } && \(command)"
}
return command
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,43 @@ public enum AgentLaunchSanitizer {
return preserveOptions(tail, policy: codexPolicy)
}

public static func removingSavedWorkingDirectoryOptions(
from args: [String],
workingDirectory: String?
) -> [String] {
guard let workingDirectory = normalizedWorkingDirectory(workingDirectory) else {
return args
}

let valueOptions: Set<String> = ["--cd", "-C", "--cwd", "--workspace", "-w"]
let optionPrefixes = valueOptions.map { "\($0)=" }
var result: [String] = []
var index = 0
while index < args.count {
let arg = args[index]
if arg == "--" {
result.append(contentsOf: args[index...])
break
}
if valueOptions.contains(arg),
index + 1 < args.count,
workingDirectoryValue(args[index + 1], matches: workingDirectory) {
index += 2
continue
}
if let prefix = optionPrefixes.first(where: { arg.hasPrefix($0) }) {
let value = String(arg.dropFirst(prefix.count))
if workingDirectoryValue(value, matches: workingDirectory) {
index += 1
continue
}
}
result.append(arg)
index += 1
}
return result
}

private static func preservedCodexLaunchArguments(args: [String]) -> [String]? {
if codexForkCommand(in: args) != nil {
return preservedCodexForkArguments(args: args)
Expand Down Expand Up @@ -350,6 +387,21 @@ public enum AgentLaunchSanitizer {
return droppedOptions.contains(String(arg[..<equals]))
}

private static func normalizedWorkingDirectory(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
return nil
}
return trimmed
}

private static func workingDirectoryValue(_ value: String, matches workingDirectory: String) -> Bool {
guard value == workingDirectory else {
return (value as NSString).expandingTildeInPath == (workingDirectory as NSString).expandingTildeInPath
}
return true
}

private static func runtimeOnlyOptionWidth(_ arg: String) -> Int? {
if let width = runtimeOnlyOptionWidths[arg] {
return width
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,4 +650,32 @@ struct AgentLaunchSanitizerTests {
) == ["amp", "--mode", "geppetto"]
)
}

@Test("Removes cwd options that duplicate the saved working directory")
func removesSavedWorkingDirectoryOptions() {
#expect(
AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: ["codex", "resume", "session", "--cd", "/tmp/project", "--model", "gpt-5.4"],
workingDirectory: "/tmp/project"
) == ["codex", "resume", "session", "--model", "gpt-5.4"]
)
#expect(
AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: ["grok", "-r", "session", "--cwd=/tmp/project", "--model", "grok-4"],
workingDirectory: "/tmp/project"
) == ["grok", "-r", "session", "--model", "grok-4"]
)
#expect(
AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: ["qoder", "--workspace", "/tmp/other", "--cwd", "/tmp/project"],
workingDirectory: "/tmp/project"
) == ["qoder", "--workspace", "/tmp/other"]
)
#expect(
AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: ["qoder", "-w", "/tmp/project", "--model", "best"],
workingDirectory: "/tmp/project"
) == ["qoder", "--model", "best"]
)
}
}
Loading
Loading