diff --git a/README.md b/README.md
index 4773f7b..b8fe63e 100644
--- a/README.md
+++ b/README.md
@@ -115,9 +115,10 @@ trust the three BarKeep commands. Codex and ChatGPT desktop share these hooks.
| Feature | Permission | Why |
|---|---|---|
+| Busy Bar connection | Local Network | Required for the bar's local HTTP API over both its USB network interface and Wi-Fi. BarKeep asks on first launch. |
| Notification forwarding | Full Disk Access | macOS stores delivered notifications in a TCC-protected SQLite DB (`~/Library/Group Containers/group.com.apple.usernoted/db2/db`). BarKeep polls it read-only; only titles/bodies matching your app filter are read, and they go straight to the bar over your local USB or Wi-Fi connection. |
| Calendar auto-busy | Calendar (full access) | To know when you're in an event. |
-| Everything else | none | Mic detection reads CoreAudio device state, not audio. |
+| Microphone detection | None | Mic detection reads CoreAudio device state, not audio. |
Grant Full Disk Access in System Settings → Privacy & Security → Full Disk
Access → add the installed `BarKeep.app`. Official release builds use a stable
diff --git a/Sources/BarKeep/AppState.swift b/Sources/BarKeep/AppState.swift
index 50238ac..11edaf6 100644
--- a/Sources/BarKeep/AppState.swift
+++ b/Sources/BarKeep/AppState.swift
@@ -151,6 +151,7 @@ final class AppState {
var calendarAccessGranted: Bool { calendarMonitor.accessGranted }
let client: BusyBarClient
+ private let localNetworkPermissionTrigger = LocalNetworkPermissionTrigger()
private let micMonitor = MicMonitor()
private let notificationWatcher = NotificationWatcher()
private let calendarMonitor = CalendarMonitor()
@@ -205,6 +206,12 @@ final class AppState {
self.pingHost = defaults.string(forKey: "pingHost") ?? "1.1.1.1"
self.weatherCelsius = defaults.bool(forKey: "weatherCelsius")
self.client = BusyBarClient(host: host, token: token)
+ localNetworkPermissionTrigger.onAccessAvailable = { [weak self] in
+ Task { @MainActor [weak self] in
+ await self?.refreshDeviceStatus()
+ }
+ }
+ localNetworkPermissionTrigger.requestAccess()
slackSync.token = self.slackToken
syncPingLoop()
syncWeatherLoop()
diff --git a/Sources/BarKeep/LocalNetworkPermissionTrigger.swift b/Sources/BarKeep/LocalNetworkPermissionTrigger.swift
new file mode 100644
index 0000000..bdfbb6d
--- /dev/null
+++ b/Sources/BarKeep/LocalNetworkPermissionTrigger.swift
@@ -0,0 +1,40 @@
+import Foundation
+import Network
+
+/// Performs an explicit local-network operation so macOS presents its privacy
+/// prompt before the Busy Bar client begins making direct-IP requests.
+final class LocalNetworkPermissionTrigger: @unchecked Sendable {
+ private let queue = DispatchQueue(label: "dev.barkeep.local-network-permission")
+ private var browser: NWBrowser?
+ var onAccessAvailable: (@Sendable () -> Void)?
+
+ func requestAccess() {
+ guard browser == nil else { return }
+
+ let parameters = NWParameters()
+ parameters.includePeerToPeer = true
+
+ let browser = NWBrowser(
+ for: .bonjour(type: "_barkeep._tcp", domain: nil),
+ using: parameters
+ )
+ browser.stateUpdateHandler = { [weak self] state in
+ switch state {
+ case .ready:
+ self?.onAccessAvailable?()
+ self?.stop()
+ case .failed, .cancelled:
+ self?.stop()
+ default:
+ break
+ }
+ }
+ browser.start(queue: queue)
+ self.browser = browser
+ }
+
+ private func stop() {
+ browser?.cancel()
+ browser = nil
+ }
+}
diff --git a/packaging/Info.plist b/packaging/Info.plist
index b1eaf24..5f75af8 100644
--- a/packaging/Info.plist
+++ b/packaging/Info.plist
@@ -13,12 +13,18 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.8
+ 1.0.9
LSMinimumSystemVersion
14.0
LSUIElement
NSCalendarsFullAccessUsageDescription
BarKeep sets your Busy Bar to busy during calendar events and shows a countdown to your next meeting.
+ NSLocalNetworkUsageDescription
+ BarKeep connects directly to your Busy Bar over USB or Wi-Fi on your local network.
+ NSBonjourServices
+
+ _barkeep._tcp
+