From 5e74161324b62e3132a35a5814e6b7181ca73131 Mon Sep 17 00:00:00 2001 From: Carl Mercier Date: Fri, 20 Feb 2026 16:24:50 -0600 Subject: [PATCH 1/7] Add wildcard hostname allowlist for built-in browser routing (#195) Adds a configurable host allowlist in Settings > Browser that controls which terminal links open in the cmux embedded browser vs the system default browser. Supports exact match and wildcard prefix patterns (e.g. localhost, 127.0.0.1, *.localtest.me). Empty list preserves existing behavior of opening all web links in cmux. Co-Authored-By: Claude Opus 4.6 --- Sources/GhosttyTerminalView.swift | 6 ++ Sources/Panels/BrowserPanel.swift | 32 +++++++++ Sources/cmuxApp.swift | 29 ++++++++ cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 70 +++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 25b859a1..820844dd 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -968,6 +968,12 @@ class GhosttyApp { NSWorkspace.shared.open(url) } case let .embeddedBrowser(url): + // If a host whitelist is configured and this host isn't in it, open externally + if let host = url.host, !BrowserLinkOpenSettings.hostMatchesWhitelist(host) { + return performOnMain { + NSWorkspace.shared.open(url) + } + } guard let tabId = surfaceView.tabId, let surfaceId = surfaceView.terminalSurface?.id else { return false } return performOnMain { diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 031887df..e083e705 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -67,12 +67,44 @@ enum BrowserLinkOpenSettings { static let openTerminalLinksInCmuxBrowserKey = "browserOpenTerminalLinksInCmuxBrowser" static let defaultOpenTerminalLinksInCmuxBrowser: Bool = true + static let browserHostWhitelistKey = "browserHostWhitelist" + static let defaultBrowserHostWhitelist: String = "" + static func openTerminalLinksInCmuxBrowser(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: openTerminalLinksInCmuxBrowserKey) == nil { return defaultOpenTerminalLinksInCmuxBrowser } return defaults.bool(forKey: openTerminalLinksInCmuxBrowserKey) } + + static func hostWhitelist(defaults: UserDefaults = .standard) -> [String] { + let raw = defaults.string(forKey: browserHostWhitelistKey) ?? defaultBrowserHostWhitelist + return raw + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + + /// 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 { + let patterns = hostWhitelist(defaults: defaults) + if patterns.isEmpty { return true } + let lowerHost = host.lowercased() + for pattern in patterns { + let lowerPattern = pattern.lowercased() + if lowerPattern.hasPrefix("*.") { + let suffix = String(lowerPattern.dropFirst(1)) // ".example.com" + if lowerHost.hasSuffix(suffix) || lowerHost == String(lowerPattern.dropFirst(2)) { + return true + } + } else if lowerHost == lowerPattern { + return true + } + } + return false + } } enum BrowserUserAgentSettings { diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index d9f99bec..8349489b 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2273,6 +2273,7 @@ struct SettingsView: View { @AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue @AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled @AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser + @AppStorage(BrowserLinkOpenSettings.browserHostWhitelistKey) private var browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist @AppStorage(NotificationBadgeSettings.dockBadgeEnabledKey) private var notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled @AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue @State private var shortcutResetToken = UUID() @@ -2427,6 +2428,33 @@ struct SettingsView: View { .controlSize(.small) } + if openTerminalLinksInCmuxBrowser { + SettingsCardDivider() + + VStack(alignment: .leading, spacing: 6) { + SettingsCardRow( + "Host Allowlist", + subtitle: "Only open these hosts in the cmux browser. One pattern per line. Supports wildcards (e.g. *.example.com). Leave empty to open all links in cmux." + ) { + EmptyView() + } + + TextEditor(text: $browserHostWhitelist) + .font(.system(.body, design: .monospaced)) + .frame(minHeight: 60, maxHeight: 120) + .scrollContentBackground(.hidden) + .padding(6) + .background(Color(nsColor: .controlBackgroundColor)) + .cornerRadius(6) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + .padding(.horizontal, 16) + .padding(.bottom, 12) + } + } + SettingsCardDivider() SettingsCardRow("Browsing History", subtitle: browserHistorySubtitle) { @@ -2577,6 +2605,7 @@ struct SettingsView: View { browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser + browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue KeyboardShortcutSettings.resetAll() diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 40662dab..5c1aa029 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2312,3 +2312,73 @@ final class TerminalOpenURLTargetResolutionTests: XCTestCase { } } } + +final class BrowserHostWhitelistTests: XCTestCase { + private var suiteName: String! + private var defaults: UserDefaults! + + override func setUp() { + super.setUp() + suiteName = "BrowserHostWhitelistTests.\(UUID().uuidString)" + defaults = UserDefaults(suiteName: suiteName) + defaults.removePersistentDomain(forName: suiteName) + } + + override func tearDown() { + defaults.removePersistentDomain(forName: suiteName) + defaults = nil + suiteName = nil + super.tearDown() + } + + func testEmptyWhitelistAllowsAll() { + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) + } + + func testExactMatch() { + defaults.set("localhost\n127.0.0.1", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) + } + + func testExactMatchIsCaseInsensitive() { + defaults.set("LocalHost", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("LOCALHOST", defaults: defaults)) + } + + func testWildcardSuffix() { + defaults.set("*.localtest.me", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("app.localtest.me", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("sub.app.localtest.me", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localtest.me", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) + } + + func testWildcardIsCaseInsensitive() { + defaults.set("*.Example.COM", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("sub.example.com", defaults: defaults)) + } + + func testBlankLinesAndWhitespaceIgnored() { + defaults.set(" localhost \n\n 127.0.0.1 \n", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) + } + + func testMixedExactAndWildcard() { + defaults.set("localhost\n127.0.0.1\n*.local.dev", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("app.local.dev", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("github.com", defaults: defaults)) + } + + func testDefaultWhitelistIsEmpty() { + let patterns = BrowserLinkOpenSettings.hostWhitelist(defaults: defaults) + XCTAssertTrue(patterns.isEmpty) + } +} From ce6b5e3999be91449e44f60c3e3f1423fdab678d Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:25:51 -0800 Subject: [PATCH 2/7] Harden terminal URL host allowlist routing --- Sources/GhosttyTerminalView.swift | 16 +++++++- Sources/Panels/BrowserPanel.swift | 40 ++++++++++++++----- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 29 ++++++++++++++ 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 9cac89e9..fe33201f 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -135,12 +135,18 @@ func resolveTerminalOpenURLTarget(_ rawValue: String) -> TerminalOpenURLTarget? if let parsed = URL(string: trimmed), let scheme = parsed.scheme?.lowercased() { if scheme == "http" || scheme == "https" { + guard BrowserInsecureHTTPSettings.normalizeHost(parsed.host ?? "") != nil else { + return .external(parsed) + } return .embeddedBrowser(parsed) } return .external(parsed) } if let webURL = resolveBrowserNavigableURL(trimmed) { + guard BrowserInsecureHTTPSettings.normalizeHost(webURL.host ?? "") != nil else { + return .external(webURL) + } return .embeddedBrowser(webURL) } @@ -979,8 +985,14 @@ class GhosttyApp { NSWorkspace.shared.open(url) } case let .embeddedBrowser(url): - // If a host whitelist is configured and this host isn't in it, open externally - if let host = url.host, !BrowserLinkOpenSettings.hostMatchesWhitelist(host) { + guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { + return performOnMain { + NSWorkspace.shared.open(url) + } + } + + // If a host whitelist is configured and this host isn't in it, open externally. + if !BrowserLinkOpenSettings.hostMatchesWhitelist(host) { return performOnMain { NSWorkspace.shared.open(url) } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 0d8ac297..974514ea 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -90,22 +90,40 @@ enum BrowserLinkOpenSettings { /// Empty whitelist means "allow all" (no filtering). /// Supports exact match and wildcard prefix (`*.example.com`). static func hostMatchesWhitelist(_ host: String, defaults: UserDefaults = .standard) -> Bool { - let patterns = hostWhitelist(defaults: defaults) - if patterns.isEmpty { return true } - let lowerHost = host.lowercased() - for pattern in patterns { - let lowerPattern = pattern.lowercased() - if lowerPattern.hasPrefix("*.") { - let suffix = String(lowerPattern.dropFirst(1)) // ".example.com" - if lowerHost.hasSuffix(suffix) || lowerHost == String(lowerPattern.dropFirst(2)) { - return true - } - } else if lowerHost == lowerPattern { + let rawPatterns = hostWhitelist(defaults: defaults) + if rawPatterns.isEmpty { return true } + guard let normalizedHost = BrowserInsecureHTTPSettings.normalizeHost(host) else { return false } + for rawPattern in rawPatterns { + guard let pattern = normalizeWhitelistPattern(rawPattern) else { continue } + if hostMatchesPattern(normalizedHost, pattern: pattern) { return true } } return false } + + private static func normalizeWhitelistPattern(_ rawPattern: String) -> String? { + let trimmed = rawPattern + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + guard !trimmed.isEmpty else { return nil } + + if trimmed.hasPrefix("*.") { + let suffixRaw = String(trimmed.dropFirst(2)) + guard let suffix = BrowserInsecureHTTPSettings.normalizeHost(suffixRaw) else { return nil } + return "*.\(suffix)" + } + + return BrowserInsecureHTTPSettings.normalizeHost(trimmed) + } + + private static func hostMatchesPattern(_ host: String, pattern: String) -> Bool { + if pattern.hasPrefix("*.") { + let suffix = String(pattern.dropFirst(2)) + return host == suffix || host.hasSuffix(".\(suffix)") + } + return host == pattern + } } enum BrowserInsecureHTTPSettings { diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index edfa1897..bc169203 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2861,6 +2861,18 @@ final class TerminalOpenURLTargetResolutionTests: XCTestCase { XCTFail("Expected non-web scheme to open externally") } } + + func testResolvesHostlessHTTPSAsExternal() throws { + let target = try XCTUnwrap(resolveTerminalOpenURLTarget("https:///tmp/cmux.txt")) + switch target { + case let .external(url): + XCTAssertEqual(url.scheme, "https") + XCTAssertNil(url.host) + XCTAssertEqual(url.path, "/tmp/cmux.txt") + default: + XCTFail("Expected hostless HTTPS URL to open externally") + } + } } final class BrowserHostWhitelistTests: XCTestCase { @@ -2931,4 +2943,21 @@ final class BrowserHostWhitelistTests: XCTestCase { let patterns = BrowserLinkOpenSettings.hostWhitelist(defaults: defaults) XCTAssertTrue(patterns.isEmpty) } + + func testWildcardRequiresDotBoundary() { + defaults.set("*.example.com", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("badexample.com", defaults: defaults)) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com.evil", defaults: defaults)) + } + + func testWhitelistNormalizesSchemesPortsAndTrailingDots() { + defaults.set("https://LOCALHOST:3000/path\n*.Example.COM:443", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost.", defaults: defaults)) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("api.example.com", defaults: defaults)) + } + + func testInvalidWhitelistEntriesDoNotImplicitlyAllowAll() { + defaults.set("http://\n*.\n", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) + } } From 1d5be22820f445919f84c74b2cfedec0221ca8f8 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:34:34 -0800 Subject: [PATCH 3/7] Normalize IDN host allowlist entries --- Sources/Panels/BrowserPanel.swift | 10 +++++++++- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 974514ea..ca100a99 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -239,7 +239,15 @@ enum BrowserInsecureHTTPSettings { private static func trimHost(_ raw: String) -> String? { let trimmed = raw.trimmingCharacters(in: CharacterSet(charactersIn: ".")) - return trimmed.isEmpty ? nil : trimmed + guard !trimmed.isEmpty else { return nil } + + // Canonicalize IDN entries (e.g. bücher.example -> xn--bcher-kva.example) + // so user-entered allowlist patterns compare against URL.host consistently. + if let canonicalized = URL(string: "https://\(trimmed)")?.host { + return canonicalized + } + + return trimmed } } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index bc169203..7ee933a4 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2960,4 +2960,9 @@ final class BrowserHostWhitelistTests: XCTestCase { defaults.set("http://\n*.\n", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) } + + func testUnicodeWhitelistEntryMatchesPunycodeHost() { + defaults.set("b\\u{00FC}cher.example", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("xn--bcher-kva.example", defaults: defaults)) + } } From b3c6c94ce06d44ac890238e5180029171224a132 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:41:13 -0800 Subject: [PATCH 4/7] Fix IDN whitelist regression test literal --- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 7ee933a4..e27d3db3 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2962,7 +2962,7 @@ final class BrowserHostWhitelistTests: XCTestCase { } func testUnicodeWhitelistEntryMatchesPunycodeHost() { - defaults.set("b\\u{00FC}cher.example", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) + defaults.set("b\u{00FC}cher.example", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("xn--bcher-kva.example", defaults: defaults)) } } From a210d77f7f911e6e3415a015cd935cf05be24728 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:45:57 -0800 Subject: [PATCH 5/7] Clarify embedded browser allowlist wording and docs --- Sources/cmuxApp.swift | 8 ++++---- web/app/docs/configuration/page.tsx | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 998bdde5..49ddd17c 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2608,8 +2608,8 @@ struct SettingsView: View { VStack(alignment: .leading, spacing: 6) { SettingsCardRow( - "Host Allowlist", - subtitle: "Only open these hosts in the cmux browser. One pattern per line. Supports wildcards (e.g. *.example.com). Leave empty to open all links in cmux." + "Hosts to Open in Embedded Browser", + subtitle: "When you click links in terminal output, only these hosts open in cmux. Other hosts open in your default browser. One host or wildcard per line (for example: example.com, *.internal.example). Leave empty to open all links in cmux." ) { EmptyView() } @@ -2633,10 +2633,10 @@ struct SettingsView: View { SettingsCardDivider() VStack(alignment: .leading, spacing: 8) { - Text("HTTP Host Allowlist") + Text("HTTP Hosts Allowed in Embedded Browser") .font(.system(size: 13, weight: .semibold)) - Text("HTTP loads outside this list show a warning prompt with options to open externally or proceed.") + Text("Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt.") .font(.caption) .foregroundStyle(.secondary) diff --git a/web/app/docs/configuration/page.tsx b/web/app/docs/configuration/page.tsx index 49bea384..09bbeaeb 100644 --- a/web/app/docs/configuration/page.tsx +++ b/web/app/docs/configuration/page.tsx @@ -108,6 +108,26 @@ working-directory = ~/Projects`} “cmux processes only” mode. +

Browser link behavior

+

+ In Settings → Browser, cmux exposes two host lists with + different purposes: +

+ +

Example config

{`# Font font-family = SF Mono From c6675c1a88d3dc744706a9177d29bb313d8cd79f Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:49:02 -0800 Subject: [PATCH 6/7] Clarify optional 0.0.0.0 allowlist usage --- Sources/cmuxApp.swift | 4 ++-- web/app/docs/configuration/page.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index d47737ec..c6e43a13 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2649,7 +2649,7 @@ struct SettingsView: View { Text("HTTP Hosts Allowed in Embedded Browser") .font(.system(size: 13, weight: .semibold)) - Text("Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt.") + Text("Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt. Add 0.0.0.0 if your local dev server emits links with that host.") .font(.caption) .foregroundStyle(.secondary) @@ -2669,7 +2669,7 @@ struct SettingsView: View { ViewThatFits(in: .horizontal) { HStack(alignment: .center, spacing: 10) { - Text("One host or wildcard per line (for example: localhost, 127.0.0.1, *.localtest.me).") + Text("One host or wildcard per line (for example: localhost, 127.0.0.1, 0.0.0.0, *.localtest.me).") .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) diff --git a/web/app/docs/configuration/page.tsx b/web/app/docs/configuration/page.tsx index 09bbeaeb..828ea8a1 100644 --- a/web/app/docs/configuration/page.tsx +++ b/web/app/docs/configuration/page.tsx @@ -124,7 +124,8 @@ working-directory = ~/Projects`}
  • HTTP Hosts Allowed in Embedded Browser — applies only to HTTP (non-HTTPS) URLs. Hosts in this list can open in cmux without - a warning prompt. + a warning prompt. If your local dev server emits links using{" "} + 0.0.0.0, add it explicitly to this list.
  • From 01313b6c9a290161d9d557f779bd29a01239251f Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:55:59 -0800 Subject: [PATCH 7/7] Expand default HTTP host allowlist for local dev --- Sources/Panels/BrowserPanel.swift | 4 +++- Sources/cmuxApp.swift | 6 +++--- cmuxTests/UpdatePillReleaseVisibilityTests.swift | 5 ++++- web/app/docs/configuration/page.tsx | 5 +++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index ca100a99..51406de9 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -129,8 +129,10 @@ enum BrowserLinkOpenSettings { enum BrowserInsecureHTTPSettings { static let allowlistKey = "browserInsecureHTTPAllowlist" static let defaultAllowlistPatterns = [ - "127.0.0.1", "localhost", + "127.0.0.1", + "::1", + "0.0.0.0", "*.localtest.me", ] static let defaultAllowlistText = defaultAllowlistPatterns.joined(separator: "\n") diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index c6e43a13..24841d43 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2649,7 +2649,7 @@ struct SettingsView: View { Text("HTTP Hosts Allowed in Embedded Browser") .font(.system(size: 13, weight: .semibold)) - Text("Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt. Add 0.0.0.0 if your local dev server emits links with that host.") + Text("Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt. Defaults include localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me.") .font(.caption) .foregroundStyle(.secondary) @@ -2669,7 +2669,7 @@ struct SettingsView: View { ViewThatFits(in: .horizontal) { HStack(alignment: .center, spacing: 10) { - Text("One host or wildcard per line (for example: localhost, 127.0.0.1, 0.0.0.0, *.localtest.me).") + Text("One host or wildcard per line (for example: localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me).") .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -2686,7 +2686,7 @@ struct SettingsView: View { } VStack(alignment: .leading, spacing: 8) { - Text("One host or wildcard per line (for example: localhost, 127.0.0.1, *.localtest.me).") + Text("One host or wildcard per line (for example: localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me).") .font(.caption) .foregroundStyle(.secondary) diff --git a/cmuxTests/UpdatePillReleaseVisibilityTests.swift b/cmuxTests/UpdatePillReleaseVisibilityTests.swift index c782eee9..3186eb6a 100644 --- a/cmuxTests/UpdatePillReleaseVisibilityTests.swift +++ b/cmuxTests/UpdatePillReleaseVisibilityTests.swift @@ -102,12 +102,15 @@ final class BrowserInsecureHTTPSettingsTests: XCTestCase { func testDefaultAllowlistPatternsArePresent() { XCTAssertEqual( BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: nil), - ["127.0.0.1", "localhost", "*.localtest.me"] + ["localhost", "127.0.0.1", "::1", "0.0.0.0", "*.localtest.me"] ) } func testWildcardAndExactHostMatching() { XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("localhost", rawAllowlist: nil)) + XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("127.0.0.1", rawAllowlist: nil)) + XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("::1", rawAllowlist: nil)) + XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("0.0.0.0", rawAllowlist: nil)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("api.localtest.me", rawAllowlist: nil)) XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("neverssl.com", rawAllowlist: nil)) } diff --git a/web/app/docs/configuration/page.tsx b/web/app/docs/configuration/page.tsx index 828ea8a1..cb1adcf8 100644 --- a/web/app/docs/configuration/page.tsx +++ b/web/app/docs/configuration/page.tsx @@ -124,8 +124,9 @@ working-directory = ~/Projects`}
  • HTTP Hosts Allowed in Embedded Browser — applies only to HTTP (non-HTTPS) URLs. Hosts in this list can open in cmux without - a warning prompt. If your local dev server emits links using{" "} - 0.0.0.0, add it explicitly to this list. + a warning prompt. Defaults include localhost,{" "} + 127.0.0.1, ::1, 0.0.0.0, and{" "} + *.localtest.me.