Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -58362,6 +58362,108 @@
}
}
},
"settings.browser.hostBlacklist": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hosts to Block from Embedded Browser"
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "内蔵ブラウザからブロックするホスト"
}
}
}
},
"settings.browser.hostBlacklist.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Applies to terminal link clicks and intercepted `open https://...` calls. These hosts always open in your default browser. All other hosts open in cmux. One host or wildcard per line (for example: example.com, *.internal.example). Leave empty to open all hosts in cmux."
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "ターミナルのリンククリックおよびインターセプトされた`open https://...`呼び出しに適用されます。これらのホストは常にデフォルトブラウザで開きます。その他のホストはcmuxで開きます。1行に1つのホストまたはワイルドカード(例: example.com, *.internal.example)。空欄にするとすべてのホストをcmuxで開きます。"
}
}
}
},
"settings.browser.hostListMode": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Host List Mode"
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "ホストリストモード"
}
}
}
},
"settings.browser.hostListMode.blacklist": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Blacklist"
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "ブラックリスト"
}
}
}
},
"settings.browser.hostListMode.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Whitelist: only listed hosts open in cmux. Blacklist: listed hosts always open in your default browser."
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "ホワイトリスト:リストのホストのみcmuxで開きます。ブラックリスト:リストのホストは常にデフォルトブラウザで開きます。"
}
}
}
},
"settings.browser.hostListMode.whitelist": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Whitelist"
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "ホワイトリスト"
}
}
}
},
"settings.browser.hostWhitelist": {
"extractionState": "manual",
"localizations": {
Expand Down
48 changes: 37 additions & 11 deletions Resources/bin/open
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ elif command -v python3 >/dev/null 2>&1; then
fi

settings_domain="${CMUX_BUNDLE_ID:-}"
whitelist_raw=""
whitelist_patterns=()
host_list_raw=""
host_list_patterns=()
host_list_mode="whitelist"
Comment thread
sharunkumar marked this conversation as resolved.

system_open() {
exec "$SYSTEM_OPEN_BIN" "$@"
Expand Down Expand Up @@ -338,29 +339,49 @@ host_matches_pattern() {
[[ "$host" == "$pattern" ]]
}

host_matches_whitelist() {
host_matches_list_pattern() {
local url="$1"
if [[ ${#whitelist_patterns[@]} -eq 0 ]]; then
if [[ ${#host_list_patterns[@]} -eq 0 ]]; then
return 0
fi

local host
host="$(normalize_host "$url")" || return 1
for pattern in "${whitelist_patterns[@]}"; do
for pattern in "${host_list_patterns[@]}"; do
if host_matches_pattern "$host" "$pattern"; then
return 0
fi
done
return 1
}

load_whitelist_patterns() {
# Determine whether a URL's host should open in cmux, respecting the
# configured host-list mode (whitelist or blacklist).
should_open_in_cmux() {
local url="$1"
if [[ ${#host_list_patterns[@]} -eq 0 ]]; then
return 0
fi

if [[ "$host_list_mode" == "blacklist" ]]; then
# Blacklist mode: if the host matches a pattern, it should NOT open in cmux.
if host_matches_list_pattern "$url"; then
return 1
fi
return 0
fi

# Whitelist mode (default): delegate directly.
host_matches_list_pattern "$url"
}

load_host_list_patterns() {
local raw="$1"
local line
while IFS= read -r line || [[ -n "$line" ]]; do
local normalized
normalized="$(normalize_whitelist_pattern "$line")" || continue
whitelist_patterns+=("$normalized")
host_list_patterns+=("$normalized")
done <<< "$raw"
}

Expand Down Expand Up @@ -433,9 +454,14 @@ if [[ -n "$settings_domain" ]]; then
system_open "$@"
fi

whitelist_raw="$("$DEFAULTS_BIN" read "$settings_domain" browserHostWhitelist 2>/dev/null || true)"
if [[ -n "$whitelist_raw" ]]; then
load_whitelist_patterns "$whitelist_raw"
host_list_raw="$("$DEFAULTS_BIN" read "$settings_domain" browserHostWhitelist 2>/dev/null || true)"
if [[ -n "$host_list_raw" ]]; then
load_host_list_patterns "$host_list_raw"
fi

host_list_mode_raw="$("$DEFAULTS_BIN" read "$settings_domain" browserHostListMode 2>/dev/null || true)"
if [[ "$host_list_mode_raw" == "blacklist" ]]; then
host_list_mode="blacklist"
fi

fi
Expand All @@ -453,7 +479,7 @@ fi
# terminal link clicks and intercepted `open` commands share one regex dialect.
failed_urls=()
for url in "${cmux_targets[@]}"; do
if is_http_url "$url" && ! host_matches_whitelist "$url"; then
if is_http_url "$url" && ! should_open_in_cmux "$url"; then
failed_urls+=("$url")
continue
fi
Expand Down
23 changes: 21 additions & 2 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8672,9 +8672,18 @@ struct ContentView: View {
var openedCount = 0
if openSidebarPullRequestLinksInCmuxBrowser {
for pullRequest in pullRequests {
if tabManager.openBrowser(url: pullRequest.url, insertAtEnd: true) != nil {
let url = pullRequest.url
// Respect external-open patterns and host whitelist/blacklist settings.
let shouldOpenExternally = BrowserLinkOpenSettings.shouldOpenExternally(url)
|| {
guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { return false }
return !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host)
Comment on lines +8679 to +8680

@cubic-dev-ai cubic-dev-ai Bot Mar 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Normalization failure is treated as "open in cmux" here, which can bypass host-list filtering for non-normalizable hosts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 5889:

<comment>Normalization failure is treated as "open in cmux" here, which can bypass host-list filtering for non-normalizable hosts.</comment>

<file context>
@@ -5882,9 +5882,18 @@ struct ContentView: View {
+                // Respect external-open patterns and host whitelist/blacklist settings.
+                let shouldOpenExternally = BrowserLinkOpenSettings.shouldOpenExternally(url)
+                    || {
+                        guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { return false }
+                        return !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host)
+                    }()
</file context>
Suggested change
guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { return false }
return !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host)
return !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: url.host ?? "")
Fix with Cubic

}()
if shouldOpenExternally {
if NSWorkspace.shared.open(url) { openedCount += 1 }
} else if tabManager.openBrowser(url: url, insertAtEnd: true) != nil {
openedCount += 1
} else if NSWorkspace.shared.open(pullRequest.url) {
} else if NSWorkspace.shared.open(url) {
openedCount += 1
}
}
Expand Down Expand Up @@ -13793,6 +13802,16 @@ private struct TabItemView: View, Equatable {
private func openPullRequestLink(_ url: URL) {
updateSelection()
if openSidebarPullRequestLinksInCmuxBrowser {
// Respect external-open patterns and host whitelist/blacklist settings.
if BrowserLinkOpenSettings.shouldOpenExternally(url) {
NSWorkspace.shared.open(url)
return
}
if let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? ""),
!BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host) {
Comment on lines +13810 to +13811

@cubic-dev-ai cubic-dev-ai Bot Mar 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This if let bypasses host-list enforcement when host normalization fails, allowing embedded open for hosts that should be treated as non-eligible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 10264:

<comment>This `if let` bypasses host-list enforcement when host normalization fails, allowing embedded open for hosts that should be treated as non-eligible.</comment>

<file context>
@@ -10247,6 +10256,16 @@ private struct TabItemView: View {
+                NSWorkspace.shared.open(url)
+                return
+            }
+            if let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? ""),
+               !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host) {
+                NSWorkspace.shared.open(url)
</file context>
Suggested change
if let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? ""),
!BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host) {
if !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: url.host ?? "") {
Fix with Cubic

NSWorkspace.shared.open(url)
return
}
if tabManager.openBrowser(
inWorkspace: tab.id,
url: url,
Expand Down
7 changes: 4 additions & 3 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3123,10 +3123,11 @@ class GhosttyApp {
}
}

// If a host whitelist is configured and this host isn't in it, open externally.
if !BrowserLinkOpenSettings.hostMatchesWhitelist(host) {
// If the host-list rules (whitelist or blacklist mode) say this host should not
// open in cmux, open externally.
if !BrowserLinkOpenSettings.shouldOpenInCmuxBrowser(host: host) {
#if DEBUG
dlog("link.openURL target=embedded but hostWhitelist miss host=\(host) url=\(url)")
dlog("link.openURL target=embedded but hostList miss host=\(host) url=\(url)")
#endif
return performOnMain {
NSWorkspace.shared.open(url)
Expand Down
1 change: 1 addition & 0 deletions Sources/KeyboardShortcutSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ final class CmuxSettingsFileStore {
"browser.theme",
"browser.openTerminalLinksInCmuxBrowser",
"browser.interceptTerminalOpenCommandInCmuxBrowser",
"browser.hostListMode",
"browser.hostsToOpenInEmbeddedBrowser",
"browser.urlsToAlwaysOpenExternally",
"browser.insecureHttpHostsAllowedInEmbeddedBrowser",
Expand Down
39 changes: 35 additions & 4 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,26 @@ enum BrowserLinkOpenSettings {

static let browserHostWhitelistKey = "browserHostWhitelist"
static let defaultBrowserHostWhitelist: String = ""
static let browserHostListModeKey = "browserHostListMode"
static let defaultBrowserHostListMode: String = "whitelist"
static let browserExternalOpenPatternsKey = "browserExternalOpenPatterns"
static let defaultBrowserExternalOpenPatterns: String = ""

/// The two supported host-list modes.
/// - `whitelist`: only listed hosts open in cmux (empty list = allow all).
/// - `blacklist`: listed hosts always open externally (empty list = allow all in cmux).
enum HostListMode: String, CaseIterable, Identifiable {
case whitelist
case blacklist

var id: String { rawValue }
}

static func hostListMode(defaults: UserDefaults = .standard) -> HostListMode {
let raw = defaults.string(forKey: browserHostListModeKey) ?? defaultBrowserHostListMode
return HostListMode(rawValue: raw) ?? .whitelist
}

static func openTerminalLinksInCmuxBrowser(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: openTerminalLinksInCmuxBrowserKey) == nil {
return defaultOpenTerminalLinksInCmuxBrowser
Expand Down Expand Up @@ -627,12 +644,26 @@ enum BrowserLinkOpenSettings {
return false
}

/// Check whether a hostname matches the configured whitelist.
/// Empty whitelist means "allow all" (no filtering).
/// Supports exact match and wildcard prefix (`*.example.com`).
static func hostMatchesWhitelist(_ host: String, defaults: UserDefaults = .standard) -> Bool {
/// Determine whether a host should open in the cmux embedded browser,
/// respecting the configured host-list mode (whitelist or blacklist).
///
/// - Whitelist mode (default): empty list = allow all; non-empty = only listed hosts open in cmux.
/// - Blacklist mode: empty list = allow all; non-empty = listed hosts open externally, everything else in cmux.
static func shouldOpenInCmuxBrowser(host: String, defaults: UserDefaults = .standard) -> Bool {
let rawPatterns = hostWhitelist(defaults: defaults)
if rawPatterns.isEmpty { return true }

let matchesEntry = hostMatchesEntry(host, rawPatterns: rawPatterns)
switch hostListMode(defaults: defaults) {
case .whitelist:
return matchesEntry
case .blacklist:
return !matchesEntry
}
}

/// Check whether a host matches any entry in the provided raw patterns list.
private static func hostMatchesEntry(_ host: String, rawPatterns: [String]) -> Bool {
guard let normalizedHost = BrowserInsecureHTTPSettings.normalizeHost(host) else { return false }
for rawPattern in rawPatterns {
guard let pattern = normalizeWhitelistPattern(rawPattern) else { continue }
Expand Down
Loading