Repository navigation
fix(#2947): preserve TERM_PROGRAM for claude-teams across Go + Swift surfaces #2999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8369e3c
3b2b37b
3952711
3bcfcdb
4f02421
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10366,6 +10366,29 @@ struct CMUXCLI { | |||||||||||||||||||||
| return ["--teammate-mode", "auto"] + commandArgs | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Returns "xterm-ghostty" if its terminfo entry is available, otherwise | ||||||||||||||||||||||
| /// "xterm-256color" — which is universally supported. This avoids a race | ||||||||||||||||||||||
| /// where `cmux claude-teams` (etc.) could exec a child process that | ||||||||||||||||||||||
| /// inherits TERM=xterm-ghostty before cmux's terminfo overlay has | ||||||||||||||||||||||
| /// finished installing (matters for SSH'd agents on fresh remote hosts; | ||||||||||||||||||||||
| /// also belt-and-suspenders for local Macs without Ghostty installed | ||||||||||||||||||||||
| /// system-wide). Mirrors the Go-side `resolveDefaultTerm` in | ||||||||||||||||||||||
| /// daemon/remote/cmd/cmuxd-remote/agent_launch.go. | ||||||||||||||||||||||
| private static func resolveDefaultTerm() -> String { | ||||||||||||||||||||||
| let process = Process() | ||||||||||||||||||||||
| process.executableURL = URL(fileURLWithPath: "/usr/bin/env") | ||||||||||||||||||||||
| process.arguments = ["infocmp", "xterm-ghostty"] | ||||||||||||||||||||||
| process.standardOutput = Pipe() | ||||||||||||||||||||||
| process.standardError = Pipe() | ||||||||||||||||||||||
| do { | ||||||||||||||||||||||
| try process.run() | ||||||||||||||||||||||
| process.waitUntilExit() | ||||||||||||||||||||||
|
Comment on lines
+10381
to
+10385
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
python3 - <<'PY'
from pathlib import Path
for path in Path(".").rglob("cmux.swift"):
text = path.read_text(errors="ignore")
marker = "private static func resolveDefaultTerm()"
start = text.find(marker)
if start == -1:
continue
end = text.find("\n }\n", start)
body = text[start:end if end != -1 else start + 1200]
print(path)
print("uses Pipe():", "Pipe()" in body)
print("drains pipe data:", "readDataToEndOfFile" in body or ".read(" in body)
break
else:
raise SystemExit("cmux.swift with resolveDefaultTerm() not found")
PYRepository: manaflow-ai/cmux Length of output: 116 🏁 Script executed: #!/bin/bash
# View the exact code context at lines 10381-10385 and surrounding
sed -n '10369,10395p' CLI/cmux.swift
echo "---"
echo "Searching for FileHandle.nullDevice usage patterns in Swift files:"
rg "FileHandle\.nullDevice" -A 2 -B 2 CLI/cmux.swift | head -30
echo "---"
echo "Checking for other Pipe() + waitUntilExit patterns without draining:"
rg -A 5 'process\.standardOutput = Pipe\(\)' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 1878 Discard probe output instead of piping it. Lines 10382–10383 create pipes that are never drained before 🐛 Proposed fix- process.standardOutput = Pipe()
- process.standardError = Pipe()
+ process.standardOutput = FileHandle.nullDevice
+ process.standardError = FileHandle.nullDevice📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| return process.terminationStatus == 0 ? "xterm-ghostty" : "xterm-256color" | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| return "xterm-256color" | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private func configureTmuxCompatEnvironment( | ||||||||||||||||||||||
| processEnvironment: [String: String], | ||||||||||||||||||||||
| shimDirectory: URL, | ||||||||||||||||||||||
|
|
@@ -10376,6 +10399,7 @@ struct CMUXCLI { | |||||||||||||||||||||
| tmuxPathPrefix: String, | ||||||||||||||||||||||
| cmuxBinEnvVar: String, | ||||||||||||||||||||||
| termOverrideEnvVar: String, | ||||||||||||||||||||||
| preserveTermProgram: Bool = false, | ||||||||||||||||||||||
| extraEnvVars: [(key: String, value: String)] = [] | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| let updatedPath = prependPathEntries( | ||||||||||||||||||||||
|
|
@@ -10392,20 +10416,37 @@ struct CMUXCLI { | |||||||||||||||||||||
| let fakeTmuxPane = focusedContext.map { "%\($0.paneHandle)" } | ||||||||||||||||||||||
| ?? processEnvironment["TMUX_PANE"] | ||||||||||||||||||||||
| ?? "%1" | ||||||||||||||||||||||
| let fakeTerm = processEnvironment[termOverrideEnvVar] ?? "screen-256color" | ||||||||||||||||||||||
| let fakeTerm = processEnvironment[termOverrideEnvVar] ?? Self.resolveDefaultTerm() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| setenv(cmuxBinEnvVar, executablePath, 1) | ||||||||||||||||||||||
| setenv("PATH", updatedPath, 1) | ||||||||||||||||||||||
| setenv("TMUX", fakeTmuxValue, 1) | ||||||||||||||||||||||
| setenv("TMUX_PANE", fakeTmuxPane, 1) | ||||||||||||||||||||||
| setenv("TERM", fakeTerm, 1) | ||||||||||||||||||||||
| // Read COLORTERM from the live env (not the snapshot) so the fallback | ||||||||||||||||||||||
| // is robust against any future re-ordering that mutates COLORTERM | ||||||||||||||||||||||
| // earlier — and so it stays in sync with the Go daemon's behavior in | ||||||||||||||||||||||
| // daemon/remote/cmd/cmuxd-remote/agent_launch.go. | ||||||||||||||||||||||
| let liveColorTerm = getenv("COLORTERM").map { String(cString: $0) } ?? "" | ||||||||||||||||||||||
| if liveColorTerm.isEmpty { | ||||||||||||||||||||||
| setenv("COLORTERM", "truecolor", 1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| setenv("CMUX_SOCKET_PATH", socketPath, 1) | ||||||||||||||||||||||
| setenv("CMUX_SOCKET", socketPath, 1) | ||||||||||||||||||||||
| if let explicitPassword, | ||||||||||||||||||||||
| !explicitPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { | ||||||||||||||||||||||
| setenv("CMUX_SOCKET_PASSWORD", explicitPassword, 1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| unsetenv("TERM_PROGRAM") | ||||||||||||||||||||||
| // opencode-family agents (omo/omx/omc) react to any non-empty | ||||||||||||||||||||||
| // TERM_PROGRAM by switching to a light theme — they treat the variable | ||||||||||||||||||||||
| // as an outer-host marker regardless of value (regression #2516). | ||||||||||||||||||||||
| // claude-teams must preserve TERM_PROGRAM since Claude Code v2.1.112+ | ||||||||||||||||||||||
| // crashes during permission escalation when it's missing (issue #2947). | ||||||||||||||||||||||
| // The default (false) keeps the legacy "unset" behavior, so any new | ||||||||||||||||||||||
| // agent type defaults to the historically common case. | ||||||||||||||||||||||
| if !preserveTermProgram { | ||||||||||||||||||||||
| unsetenv("TERM_PROGRAM") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| for envVar in extraEnvVars { | ||||||||||||||||||||||
| setenv(envVar.key, envVar.value, 1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
@@ -10446,6 +10487,7 @@ struct CMUXCLI { | |||||||||||||||||||||
| tmuxPathPrefix: "cmux-claude-teams", | ||||||||||||||||||||||
| cmuxBinEnvVar: "CMUX_CLAUDE_TEAMS_CMUX_BIN", | ||||||||||||||||||||||
| termOverrideEnvVar: "CMUX_CLAUDE_TEAMS_TERM", | ||||||||||||||||||||||
| preserveTermProgram: true, | ||||||||||||||||||||||
| extraEnvVars: [ | ||||||||||||||||||||||
| (key: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", value: "1"), | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Use
FileHandle.nullDeviceinstead ofPipe()for stdout/stderr. These pipes are never drained beforewaitUntilExit(), which can deadlock if the subprocess writes enough to fill the pipe buffer (~64KB). Since the output ofinfocmpis discarded anyway,FileHandle.nullDeviceis the correct choice — and matches the Go-side pattern (cmd.Stdout = nil).Prompt for AI agents