Repository navigation
Add Claude Binary Path setting #2514
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
44e2d60
e7bd964
b9fc1eb
66d286e
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 |
|---|---|---|
|
|
@@ -9688,12 +9688,30 @@ struct CMUXCLI { | |
| .deletingLastPathComponent() | ||
| .appendingPathComponent("claude", isDirectory: false) | ||
| .path | ||
| let claudeExecutablePath = resolveClaudeExecutable(searchPath: launcherEnvironment["PATH"]) | ||
| ?? { | ||
| guard let bundledClaudePath, | ||
| FileManager.default.isExecutableFile(atPath: bundledClaudePath) else { return nil } | ||
| return bundledClaudePath | ||
| }() | ||
| let claudeExecutablePath: String? = { | ||
| // Check custom path from Settings > Automation > Claude Code. | ||
| // Try env var first (set by the app per-session), then UserDefaults. | ||
| let candidates = [ | ||
| launcherEnvironment["CMUX_CUSTOM_CLAUDE_PATH"], | ||
| UserDefaults.standard.string(forKey: "claudeCodeCustomClaudePath"), | ||
| ] | ||
| for raw in candidates { | ||
| guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines), | ||
| !trimmed.isEmpty else { continue } | ||
| var isDir: ObjCBool = false | ||
| guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), | ||
| !isDir.boolValue, | ||
| FileManager.default.isExecutableFile(atPath: trimmed), | ||
| !isCmuxClaudeWrapper(at: trimmed) else { continue } | ||
| return trimmed | ||
| } | ||
|
Comment on lines
+9691
to
+9707
Contributor
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.
The two-source lookup has a subtle precedence issue: any process that injects In practice, for the GUI-launched path the env var and UserDefaults are always in sync (both come from the same This is probably intentional, but it's worth confirming that having a hidden override mechanism (env var) that takes priority over the visible Settings UI is the desired UX — particularly because there is no user-visible indication that a different path is being used when the env var is set externally.
Contributor
Author
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. The env var taking precedence over UserDefaults is intentional: the app sets it per-session from the same UserDefaults value. The env var path exists so the bundled wrapper script can read the setting without accessing UserDefaults directly. In practice they always agree. — Claude Code |
||
| return resolveClaudeExecutable(searchPath: launcherEnvironment["PATH"]) | ||
| ?? { | ||
| guard let bundledClaudePath, | ||
| FileManager.default.isExecutableFile(atPath: bundledClaudePath) else { return nil } | ||
| return bundledClaudePath | ||
| }() | ||
| }() | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| configureClaudeTeamsEnvironment( | ||
| processEnvironment: launcherEnvironment, | ||
| shimDirectory: shimDirectory, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3921,13 +3921,20 @@ enum CommandPaletteSwitcherSearchSettings { | |
| enum ClaudeCodeIntegrationSettings { | ||
| static let hooksEnabledKey = "claudeCodeHooksEnabled" | ||
| static let defaultHooksEnabled = true | ||
| static let customClaudePathKey = "claudeCodeCustomClaudePath" | ||
|
|
||
| static func hooksEnabled(defaults: UserDefaults = .standard) -> Bool { | ||
| if defaults.object(forKey: hooksEnabledKey) == nil { | ||
| return defaultHooksEnabled | ||
| } | ||
| return defaults.bool(forKey: hooksEnabledKey) | ||
| } | ||
|
|
||
| static func customClaudePath(defaults: UserDefaults = .standard) -> String? { | ||
| let value = defaults.string(forKey: customClaudePathKey)? | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" | ||
| return value.isEmpty ? nil : value | ||
| } | ||
|
Comment on lines
+3933
to
+3937
Contributor
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.
This is inconsistent with the existing pattern used by
Contributor
Author
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. Fixed. customClaudePath() now trims whitespace before the empty check. — Claude Code
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| enum WelcomeSettings { | ||
|
|
@@ -4006,6 +4013,8 @@ struct SettingsView: View { | |
| @AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue | ||
| @AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey) | ||
| private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled | ||
| @AppStorage(ClaudeCodeIntegrationSettings.customClaudePathKey) | ||
| private var customClaudePath = "" | ||
| @AppStorage(TelemetrySettings.sendAnonymousTelemetryKey) | ||
| private var sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry | ||
| @AppStorage(PreferredEditorSettings.key) private var preferredEditorCommand = "" | ||
|
|
@@ -5383,6 +5392,20 @@ struct SettingsView: View { | |
| SettingsCardNote(String(localized: "settings.automation.claudeCode.note", defaultValue: "When enabled, cmux wraps the claude command to inject session tracking and notification hooks. Disable if you prefer to manage Claude Code hooks yourself.")) | ||
| } | ||
|
|
||
| SettingsCard { | ||
| SettingsCardRow( | ||
| String(localized: "settings.automation.claudeCode.customPath", defaultValue: "Claude Binary Path"), | ||
| subtitle: String(localized: "settings.automation.claudeCode.customPath.subtitle", defaultValue: "Custom path to the claude binary. Leave empty to use PATH.") | ||
| ) { | ||
| TextField( | ||
| String(localized: "settings.automation.claudeCode.customPath.placeholder", defaultValue: "e.g. /usr/local/bin/claude"), | ||
| text: $customClaudePath | ||
| ) | ||
| .textFieldStyle(.roundedBorder) | ||
|
Comment on lines
5394
to
+5404
Contributor
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.
The Consider using Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| .frame(width: 200) | ||
| } | ||
| } | ||
|
|
||
| SettingsCard { | ||
| SettingsCardRow(String(localized: "settings.automation.portBase", defaultValue: "Port Base"), subtitle: String(localized: "settings.automation.portBase.subtitle", defaultValue: "Starting port for CMUX_PORT env var."), controlWidth: pickerColumnWidth) { | ||
| TextField("", value: $cmuxPortBase, format: .number) | ||
|
|
@@ -5945,6 +5968,7 @@ struct SettingsView: View { | |
| AppIconSettings.applyIcon(.automatic) | ||
| socketControlMode = SocketControlSettings.defaultMode.rawValue | ||
| claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled | ||
| customClaudePath = "" | ||
| sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry | ||
| preferredEditorCommand = "" | ||
| browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.