Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
25030ee
feat: suppress ignored Claude notifications
austinywang May 12, 2026
17f1477
Merge remote-tracking branch 'origin/main' into issue-3602-suppress-i…
austinywang May 12, 2026
636c344
fix: tighten Claude notification type filtering
austinywang May 12, 2026
bee9922
refactor: share Claude notification type normalization
austinywang May 12, 2026
0cf31e7
Merge remote-tracking branch 'origin/main' into issue-3602-suppress-i…
austinywang May 12, 2026
5adc3d5
fix: extract ignored Claude types from raw hook input
austinywang May 12, 2026
0a9029e
refactor: share Claude notification filter keys
austinywang May 12, 2026
d9091ba
Merge remote-tracking branch 'origin/main' into issue-3602-suppress-i…
austinywang May 13, 2026
eb366d3
merge: resolve conflicts with main
austinywang May 14, 2026
48ea5c2
Merge remote-tracking branch 'origin/main' into issue-3602-suppress-i…
austinywang May 17, 2026
a22d3b6
fix: sanitize suppressed notification telemetry
austinywang May 18, 2026
1a010d2
fix: parse fallback Claude notification types
austinywang May 18, 2026
183a71a
fix: scope Claude notification type extraction
austinywang May 18, 2026
9a613da
Merge remote-tracking branch 'origin/main' into issue-3602-suppress-i…
austinywang May 18, 2026
dcddc10
merge: sync with origin main
austinywang May 19, 2026
57574ad
fix: read live Claude notification settings
austinywang May 19, 2026
23613f8
fix: skip invalid Claude notification configs
austinywang May 19, 2026
0d442bc
fix: honor live Claude notification config precedence
austinywang May 19, 2026
270a7c7
fix: reject invalid Claude notification config values
austinywang May 19, 2026
76ee9f0
fix: treat malformed notifications config as invalid
austinywang May 19, 2026
4c7ba93
refactor: isolate Claude notification domain
austinywang May 19, 2026
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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ jobs:
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_socket_operation_deadline.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_socket_autodiscovery.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_clear_running_status.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_ignored_notification_types.py
- save-swift-package-cache:
prefix: unit
- save-zig-cache
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ jobs:
python3 tests/test_claude_wrapper_hooks.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_stop_last_assistant.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_clear_running_status.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_claude_hook_ignored_notification_types.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_pi_extension_install.py

tests-build-and-lag:
Expand Down
101 changes: 100 additions & 1 deletion CLI/cmux.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import CMUXAgentLaunch
import CMUXClaudeNotifications
import CoreFoundation
import CryptoKit
import Darwin
Expand Down Expand Up @@ -332,6 +333,7 @@ private struct ClaudeHookParsedInput {
let rawObject: [String: Any]?
let object: [String: Any]?
let rawFallback: String?
let rawFallbackNotificationTypeValues: [String]
let sessionId: String?
let turnId: String?
let cwd: String?
Expand Down Expand Up @@ -16696,6 +16698,16 @@ struct CMUXCLI {

case "notification", "notify":
telemetry.breadcrumb("claude-hook.notification")
let notificationTypes = Self.claudeNotificationTypes(parsedInput: parsedInput)
let suppressedNotificationTypes = Self.suppressedClaudeNotificationTypes(for: notificationTypes)
if !suppressedNotificationTypes.isEmpty {
telemetry.breadcrumb(
"claude-hook.notification.suppressed",
data: ["types": suppressedNotificationTypes.sorted().joined(separator: ",")]
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
print("OK")
return
Comment thread
lawrencecchen marked this conversation as resolved.
}
var summary = summarizeClaudeHookNotification(parsedInput: parsedInput)

let mappedSession = parsedInput.sessionId.flatMap { try? sessionStore.lookup(sessionId: $0) }
Expand Down Expand Up @@ -17312,7 +17324,7 @@ struct CMUXCLI {
guard !trimmed.isEmpty,
let data = trimmed.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data, options: []),
let object = json as? [String: Any] else {
let object = Self.claudeHookObject(fromJSONValue: json) else {
let fallback = trimmed.isEmpty ? nil : truncate(
normalizedSingleLine(redactClaudeSensitiveSpans(trimmed)),
maxLength: 180
Expand All @@ -17321,6 +17333,7 @@ struct CMUXCLI {
rawObject: nil,
object: nil,
rawFallback: fallback,
rawFallbackNotificationTypeValues: ClaudeNotificationTypeExtractor.values(inRawFallback: trimmed),
sessionId: nil,
turnId: nil,
cwd: nil,
Expand All @@ -17337,13 +17350,27 @@ struct CMUXCLI {
rawObject: object,
object: compactObject,
rawFallback: nil,
rawFallbackNotificationTypeValues: [],
sessionId: sessionId,
turnId: turnId,
cwd: cwd,
transcriptPath: transcriptPath
)
}

private static func claudeHookObject(fromJSONValue value: Any) -> [String: Any]? {
if let object = value as? [String: Any] {
return object
}
guard let string = value as? String,
let data = string.trimmingCharacters(in: .whitespacesAndNewlines).data(using: .utf8),
let nested = try? JSONSerialization.jsonObject(with: data, options: []),
let object = nested as? [String: Any] else {
return nil
}
return object
}

private func compactClaudeHookObject(_ object: [String: Any]) -> [String: Any] {
var compact: [String: Any] = [:]

Expand Down Expand Up @@ -18722,6 +18749,78 @@ struct CMUXCLI {
return ("Attention", "Claude needs your attention")
}

private static let claudeIgnoredNotificationTypesDefaultsKey =
ClaudeNotificationTypeNormalization.ignoredTypesDefaultsKey
private static let claudeIgnoredNotificationTypesEnvKey =
ClaudeNotificationTypeNormalization.ignoredTypesEnvironmentKey

private static func suppressedClaudeNotificationTypes(for types: Set<String>) -> Set<String> {
ClaudeNotificationSuppression.suppressedTypes(
notificationTypes: types,
ignoredTypes: ignoredClaudeNotificationTypes()
)
}

private static func ignoredClaudeNotificationTypes() -> Set<String> {
// Hook subprocess environments are immutable after terminal launch; read cmux.json first
// so existing terminals follow live settings changes.
if let settingsTypes = ignoredClaudeNotificationTypesFromSettingsFiles() {
return settingsTypes
}

if let env = ProcessInfo.processInfo.environment[claudeIgnoredNotificationTypesEnvKey] {
return normalizedClaudeNotificationTypes(
env.components(separatedBy: CharacterSet(charactersIn: ", \n\t"))
)
}
Comment thread
cursor[bot] marked this conversation as resolved.

if let stored = UserDefaults.standard.array(forKey: claudeIgnoredNotificationTypesDefaultsKey) as? [String] {
return normalizedClaudeNotificationTypes(stored)
}
if let stored = UserDefaults.standard.string(forKey: claudeIgnoredNotificationTypesDefaultsKey) {
return normalizedClaudeNotificationTypes(
stored.components(separatedBy: CharacterSet(charactersIn: ", \n\t"))
)
}
return []
}

private static func ignoredClaudeNotificationTypesFromSettingsFiles(
fileManager: FileManager = .default,
environment: [String: String] = ProcessInfo.processInfo.environment
) -> Set<String>? {
let paths = ClaudeIgnoredNotificationSettings.settingsPaths(
primaryDisplayPath: primarySettingsDisplayPath,
legacyDisplayPath: legacySettingsDisplayPath,
appSupportDirectories: CmuxApplicationSupportDirectories.userDirectories(
environment: environment,
fileManager: fileManager
),
fileManager: fileManager,
environment: environment
)

return ClaudeIgnoredNotificationSettings.ignoredTypesFromSettingsFiles(
paths: paths,
fileManager: fileManager
) { data in
try JSONCParser.preprocess(data: data)
}
}

private static func claudeNotificationTypes(parsedInput: ClaudeHookParsedInput) -> Set<String> {
var rawValues: [String] = []
if let object = parsedInput.rawObject ?? parsedInput.object {
rawValues.append(contentsOf: ClaudeNotificationTypeExtractor.values(inJSONValue: object))
}
rawValues.append(contentsOf: parsedInput.rawFallbackNotificationTypeValues)
return normalizedClaudeNotificationTypes(rawValues)
}

private static func normalizedClaudeNotificationTypes(_ values: [String]) -> Set<String> {
ClaudeNotificationTypeNormalization.normalizedSet(values)
}

private func firstString(in object: [String: Any], keys: [String]) -> String? {
for key in keys {
guard let value = object[key] else { continue }
Expand Down
24 changes: 24 additions & 0 deletions Packages/CMUXClaudeNotifications/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "CMUXClaudeNotifications",
platforms: [
.macOS(.v13),
],
products: [
.library(
name: "CMUXClaudeNotifications",
targets: ["CMUXClaudeNotifications"]
),
],
targets: [
.target(
name: "CMUXClaudeNotifications"
),
.testTarget(
name: "CMUXClaudeNotificationsTests",
dependencies: ["CMUXClaudeNotifications"]
),
]
)
Loading
Loading