diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 06858c55989f..0cf8dedf7909 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -59583,6 +59583,244 @@ } } }, + "settings.automation.claudeCode.notificationHook": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Claude Code Idle Notifications" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Claude Codeアイドル通知" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "de": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "es": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "fr": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "it": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "da": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "pl": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "ru": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "bs": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "ar": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "nb": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "pt-BR": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "th": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "tr": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + }, + "uk": { + "stringUnit": { + "state": "new", + "value": "Claude Code Idle Notifications" + } + } + } + }, + "settings.automation.claudeCode.notificationHook.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Claude Codeが入力待ちのときに通知します。完了通知には影響しません。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "de": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "es": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "fr": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "it": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "da": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "pl": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "ru": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "bs": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "ar": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "nb": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "pt-BR": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "th": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "tr": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + }, + "uk": { + "stringUnit": { + "state": "new", + "value": "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected." + } + } + } + }, "settings.automation.claudeCode.subtitleOff": { "extractionState": "manual", "localizations": { @@ -77615,6 +77853,125 @@ } } }, + "settings.search.alias.setting.automation.claude-notification-hook": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "automation.claudeCodeNotificationHook claude code idle notification hook waiting input" + } + } + } + }, "settings.search.alias.setting.automation.claude-path": { "extractionState": "manual", "localizations": { diff --git a/Resources/bin/claude b/Resources/bin/claude index 44431169f0ec..a094c022ef51 100755 --- a/Resources/bin/claude +++ b/Resources/bin/claude @@ -466,7 +466,17 @@ fi # This is Claude Code's native blocking decision hook. It covers # permissions, ExitPlanMode, and AskUserQuestion without using # PreToolUse denial as a side channel. -HOOKS_JSON='{"preferredNotifChannel":"notifications_disabled","hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude session-start","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude stop","timeout":10}]},{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks feed --source claude","timeout":10,"async":true}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude session-end","timeout":1}]}],"Notification":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude notification","timeout":10}]}],"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude prompt-submit","timeout":10}]}],"PreToolUse":[{"matcher":"CronCreate","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude cron-create-guard","timeout":5}]},{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude pre-tool-use","timeout":5,"async":true}]}],"PermissionRequest":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks feed --source claude","timeout":125}]}]}}' +HOOKS_BASE='{"preferredNotifChannel":"notifications_disabled","hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude session-start","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude stop","timeout":10}]},{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks feed --source claude","timeout":10,"async":true}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude session-end","timeout":1}]}],' + +HOOKS_NOTIFICATION='"Notification":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude notification","timeout":10}]}],' + +HOOKS_TAIL='"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude prompt-submit","timeout":10}]}],"PreToolUse":[{"matcher":"CronCreate","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude cron-create-guard","timeout":5}]},{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude pre-tool-use","timeout":5,"async":true}]}],"PermissionRequest":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks feed --source claude","timeout":125}]}]}}' + +if [[ "$CMUX_CLAUDE_NOTIFICATION_HOOK_DISABLED" != "1" ]]; then + HOOKS_JSON="${HOOKS_BASE}${HOOKS_NOTIFICATION}${HOOKS_TAIL}" +else + HOOKS_JSON="${HOOKS_BASE}${HOOKS_TAIL}" +fi if [[ "$SKIP_SESSION_ID" == true ]]; then exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@" diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index 508488b5e04a..266be4d86be4 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -101,6 +101,7 @@ extension CmuxSettingsFileStore { "automation.socketPassword", "automation.claudeCodeIntegration", "automation.claudeBinaryPath", + "automation.claudeCodeNotificationHook", "automation.cursorIntegration", "automation.geminiIntegration", "automation.portBase", diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index dee80dfdfcfe..0b2081cf5fd3 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -5361,6 +5361,9 @@ final class TerminalSurface: Identifiable, ObservableObject { if let customClaudePath = ClaudeCodeIntegrationSettings.customClaudePath() { setManagedEnvironmentValue("CMUX_CUSTOM_CLAUDE_PATH", customClaudePath) } + if !ClaudeCodeIntegrationSettings.notificationHookEnabled() { + setManagedEnvironmentValue("CMUX_CLAUDE_NOTIFICATION_HOOK_DISABLED", "1") + } if !CursorIntegrationSettings.hooksEnabled() { setManagedEnvironmentValue("CMUX_CURSOR_HOOKS_DISABLED", "1") } diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 9851841ae5fe..cedf4cf9cc2a 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -694,6 +694,9 @@ final class CmuxSettingsFileStore { if let raw = jsonString(section["claudeBinaryPath"]) { snapshot.managedUserDefaults[ClaudeCodeIntegrationSettings.customClaudePathKey] = .string(raw) } + if let value = jsonBool(section["claudeCodeNotificationHook"]) { + snapshot.managedUserDefaults[ClaudeCodeIntegrationSettings.notificationHookEnabledKey] = .bool(value) + } if let value = jsonBool(section["cursorIntegration"]) { snapshot.managedUserDefaults[CursorIntegrationSettings.hooksEnabledKey] = .bool(value) } diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index f395f9800a4f..803e77a873e0 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -339,6 +339,7 @@ enum SettingsSearchIndex { setting(.automation, "socket-password", String(localized: "settings.automation.socketPassword", defaultValue: "Socket Password"), "socket auth credential"), setting(.automation, "claude-code", String(localized: "settings.automation.claudeCode", defaultValue: "Claude Code Integration"), "agent hooks notifications"), setting(.automation, "claude-path", String(localized: "settings.automation.claudeCode.customPath", defaultValue: "Claude Binary Path"), "custom claude executable"), + setting(.automation, "claude-notification-hook", String(localized: "settings.automation.claudeCode.notificationHook", defaultValue: "Claude Code Idle Notifications"), "idle notification hook waiting input"), setting(.automation, "cursor", String(localized: "settings.automation.cursor", defaultValue: "Cursor Integration"), "agent hooks notifications"), setting(.automation, "gemini", String(localized: "settings.automation.gemini", defaultValue: "Gemini CLI Integration"), "agent hooks notifications"), setting(.automation, "port-base", String(localized: "settings.automation.portBase", defaultValue: "Port Base"), "CMUX_PORT start"), @@ -430,6 +431,7 @@ enum SettingsSearchIndex { "automation.socketPassword": settingID(for: .automation, idSuffix: "socket-password"), "automation.claudeCodeIntegration": settingID(for: .automation, idSuffix: "claude-code"), "automation.claudeBinaryPath": settingID(for: .automation, idSuffix: "claude-path"), + "automation.claudeCodeNotificationHook": settingID(for: .automation, idSuffix: "claude-notification-hook"), "automation.cursorIntegration": settingID(for: .automation, idSuffix: "cursor"), "automation.geminiIntegration": settingID(for: .automation, idSuffix: "gemini"), "automation.portBase": settingID(for: .automation, idSuffix: "port-base"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 3af6970fa445..7f8359610f09 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -88,6 +88,7 @@ enum SettingsSearchAliasIndex { "automation:socket-password": localized("settings.search.alias.setting.automation.socket-password", defaultValue: "automation.socketPassword auth token credential secret password access key"), "automation:claude-code": localized("settings.search.alias.setting.automation.claude-code", defaultValue: "automation.claudeCodeIntegration claude code hooks agent integration status notifications"), "automation:claude-path": localized("settings.search.alias.setting.automation.claude-path", defaultValue: "automation.claudeBinaryPath claude binary executable path cli command custom"), + "automation:claude-notification-hook": localized("settings.search.alias.setting.automation.claude-notification-hook", defaultValue: "automation.claudeCodeNotificationHook claude code idle notification hook waiting input"), "automation:cursor": localized("settings.search.alias.setting.automation.cursor", defaultValue: "automation.cursorIntegration cursor ide agent hooks notifications"), "automation:gemini": localized("settings.search.alias.setting.automation.gemini", defaultValue: "automation.geminiIntegration gemini cli google agent hooks notifications"), "automation:port-base": localized("settings.search.alias.setting.automation.port-base", defaultValue: "automation.portBase cmux_port start first base env environment variable"), diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 7de019ac4d59..b6e3625140d0 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -4656,6 +4656,8 @@ enum ClaudeCodeIntegrationSettings { static let hooksEnabledKey = "claudeCodeHooksEnabled" static let defaultHooksEnabled = true static let customClaudePathKey = "claudeCodeCustomClaudePath" + static let notificationHookEnabledKey = "claudeCodeNotificationHookEnabled" + static let defaultNotificationHookEnabled = true static func hooksEnabled(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: hooksEnabledKey) == nil { @@ -4664,6 +4666,13 @@ enum ClaudeCodeIntegrationSettings { return defaults.bool(forKey: hooksEnabledKey) } + static func notificationHookEnabled(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: notificationHookEnabledKey) == nil { + return defaultNotificationHookEnabled + } + return defaults.bool(forKey: notificationHookEnabledKey) + } + static func customClaudePath(defaults: UserDefaults = .standard) -> String? { let value = defaults.string(forKey: customClaudePathKey)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -5002,6 +5011,8 @@ struct SettingsView: View { private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled @AppStorage(ClaudeCodeIntegrationSettings.customClaudePathKey) private var customClaudePath = "" + @AppStorage(ClaudeCodeIntegrationSettings.notificationHookEnabledKey) + private var claudeCodeNotificationHookEnabled = ClaudeCodeIntegrationSettings.defaultNotificationHookEnabled @AppStorage(CursorIntegrationSettings.hooksEnabledKey) private var cursorHooksEnabled = CursorIntegrationSettings.defaultHooksEnabled @AppStorage(GeminiIntegrationSettings.hooksEnabledKey) @@ -6527,6 +6538,21 @@ struct SettingsView: View { SettingsCardDivider() + SettingsCardRow( + configurationReview: .json("automation.claudeCodeNotificationHook"), + String(localized: "settings.automation.claudeCode.notificationHook", defaultValue: "Claude Code Idle Notifications"), + subtitle: String(localized: "settings.automation.claudeCode.notificationHook.subtitle", defaultValue: "Notify when Claude Code reports that it is waiting for input. Completion notifications are unaffected.") + ) { + Toggle("", isOn: $claudeCodeNotificationHookEnabled) + .labelsHidden() + .controlSize(.small) + .accessibilityIdentifier("SettingsClaudeCodeNotificationHookToggle") + } + .disabled(!claudeCodeHooksEnabled) + .opacity(claudeCodeHooksEnabled ? 1 : 0.75) + + SettingsCardDivider() + 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.")) } @@ -7311,6 +7337,7 @@ struct SettingsView: View { socketControlMode = SocketControlSettings.defaultMode.rawValue claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled customClaudePath = "" + claudeCodeNotificationHookEnabled = ClaudeCodeIntegrationSettings.defaultNotificationHookEnabled cursorHooksEnabled = CursorIntegrationSettings.defaultHooksEnabled geminiHooksEnabled = GeminiIntegrationSettings.defaultHooksEnabled sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry