I had an existing lock app that automatically locks my Mac when I walk away using Bluetooth proximity detection. It worked great—until it showed a popup asking me to buy a license to keep using it. Not a surprise, and I would’ve happily paid a year ago. But now? I looked at the feature list and thought: “Can I just build this myself?” The catch: I don’t know Swift, and I still don’t.

This is Lock—a macOS menubar app that detects when you walk away via your Apple Watch or iPhone’s Bluetooth signal and locks your screen automatically. Built in one day with Claude choosing Swift/SwiftUI and handling all the code while guiding me through Mac app notarization, Developer ID signing, and distribution.

Why Did We Do This?

The problem: I wanted to test if you could build a polished, single-purpose macOS app quickly—even in a language you don’t know—by having a coding agent handle all the implementation. I was curious if AI-assisted development could choose the right stack (Swift, SwiftUI, CoreBluetooth) and navigate macOS-specific processes (code signing, notarization, entitlements) without me needing to understand the language.

Existing solutions didn’t fit:

  • Commercial proximity apps: Closed-source, subscriptions, or one-time purchases for simple functionality
  • DIY AppleScript hacks: Unreliable, require accessibility permissions, clunky UX
  • System Preferences proximity unlock: Only handles unlocking (with Apple Watch), not automatic locking
  • Screensaver timers: Too slow, require manual configuration

The idea: Build a menubar app with Swift Package Manager, separate testable business logic from UI, use Kalman filtering for reliable detection, and ship a code-signed .app bundle—all in one focused build session.

What Problems Needed Solving?

AI-Driven Development: Swift Stack Selection

What the coding agent needed to handle:

  • Choose Swift/SwiftUI as the right stack for a native macOS menubar app
  • Implement SwiftUI’s declarative syntax and component architecture
  • Use Combine framework for reactive state management
  • Navigate @Published, @StateObject, @ObservedObject property wrappers
  • Configure Xcode, Swift Package Manager, entitlements, and code signing
  • Guide me through Mac-specific processes (Developer ID, notarization, distribution)

Reality: I didn’t learn Swift—Claude wrote all the Swift code. I don’t know what guard let unwrapping is, how some View opaque return types work, or the difference between structs and classes in Swift. The agent chose the language, implemented the patterns, and explained what was happening at each step so I could provide direction without needing to understand the syntax.

Two-Target Architecture

Wanted testable code. UI code (SwiftUI views) is hard to unit test. Solution: separate LockCore (business logic) from LockApp (UI). LockCore has no SwiftUI dependencies—just Bluetooth scanning, Kalman filtering, and proximity state machines. This meant:

  • LockCore could have unit tests for the Kalman filter
  • LockApp imports LockCore and wraps it in SwiftUI
  • State flows one direction: BluetoothManager → ProximityMonitor → AppState → Views

Bluetooth Device Detection

macOS requires explicit Bluetooth permissions. CoreBluetooth API is callback-based (delegate pattern). Needed to:

  • Scan for nearby devices (iPhone, iPad, Apple Watch)
  • Identify Apple devices by name patterns or manufacturer ID (0x004C)
  • Deduplicate devices (Apple devices broadcast multiple UUIDs)
  • Handle permission requests gracefully

Signal Noise and Kalman Filtering

RSSI (Bluetooth signal strength) is incredibly noisy. Standing still, your Watch’s signal might read -65 dBm, then -90 dBm, then -63 dBm within seconds. A simple threshold causes false locks.

The solution: Kalman filter. I didn’t know what a Kalman filter was before this project. Used Claude in research mode to understand the problem: “Why is RSSI noisy? How do you filter signal strength for proximity detection?” The research session explained Kalman filters—they model signal as position + velocity, distinguishing gradual decline (walking away) from random spikes (interference).

I’m not a Kalman expert. I know what problem it solves and roughly how it works. That was enough to write requirements. Claude implemented the math in Swift.

Private API for Screen Locking

macOS doesn’t provide a public API for immediate screen locking (screensaver APIs exist but don’t guarantee lock). The solution: SACLockScreenImmediate via dlopen. This means:

  • Not App Store compatible
  • Requires code signing with Developer ID
  • Must be notarized by Apple for distribution

SwiftUI menubar apps have quirks:

  • No traditional window—just a popover from the status bar
  • Must manage NSStatusItem lifecycle
  • Launch at login requires SMAppService API (new in macOS 13)
  • Popover dismissal on outside clicks needs explicit handling

The Plan

Here’s the plan I gave Claude. One-day sprint, structured as a Claude Code implementation plan.


Plan: swift-menubar-lock.md

Implementation Plan: Lock macOS Menubar App

Overview

Build a proximity-based screen lock for macOS using Bluetooth signal strength from Apple devices. Native Swift/SwiftUI menubar app with Kalman filtering, measure mode calibration, and code signing for distribution.

Goals

  1. Detect presence via Bluetooth RSSI from Apple Watch/iPhone
  2. Lock screen automatically when user walks away (2-3 second latency)
  3. Filter noise with Kalman filter to prevent false triggers
  4. Calibrate easily with measure mode (walk away, click Set)
  5. Ship properly with code signing and notarization

Architecture Decisions

Framework: Swift Package Manager

  • Swift Package Manager over Xcode project: Simpler dependency management, cleaner structure
  • Two targets: LockCore (business logic) and LockApp (SwiftUI UI)
  • Unit testable: LockCore has no UI dependencies, can test Kalman filter in isolation

UI: SwiftUI Menubar App

  • SwiftUI over AppKit: Modern, declarative, less boilerplate for simple UI
  • Menubar popover instead of window: Fits single-purpose utility app pattern
  • Dark theme: Matches macOS system preferences

Bluetooth: CoreBluetooth Framework

  • CoreBluetooth for device scanning and RSSI monitoring
  • Delegate pattern for Bluetooth callbacks
  • Device deduplication by name (Apple devices have multiple UUIDs)

Signal Processing: 2-State Kalman Filter

  • Position + velocity tracking for RSSI values
  • Process noise accounts for gradual signal change (walking)
  • Measurement noise accounts for environmental interference
  • Hysteresis: Different thresholds for present (-70 dBm) vs. away (-85 dBm)

State Management: Combine Framework

  • @Published properties in ProximityMonitor and AppState
  • ObservableObject protocol for reactive UI updates
  • One-way data flow: BluetoothManager → ProximityMonitor → AppState → Views

Lock Action: Private API

  • SACLockScreenImmediate via dlopen for instant screen lock
  • Alternative actions: Screensaver, macOS Shortcuts integration
  • Notifications: User-facing feedback on lock events

Implementation Tasks

Task 1: Project Setup

  • Create Swift Package with two targets: LockCore and LockApp
  • Configure entitlements (Bluetooth, notifications)
  • Add Info.plist with required permission strings

Files:

  • Package.swift: Target definitions, dependencies
  • Lock.entitlements: Bluetooth, notifications permissions
  • Sources/LockApp/Info.plist: Permission usage descriptions

Verification:

swift build  # Should compile without errors

Task 2: LockCore - Bluetooth Manager

  • Implement CoreBluetooth scanning
  • Device discovery with Apple device filtering
  • RSSI monitoring for selected device
  • Deduplication by device name

Files:

  • Sources/LockCore/Bluetooth/BluetoothManager.swift: CoreBluetooth wrapper
  • Sources/LockCore/DeviceInfo.swift: Device model struct

Key APIs:

  • CBCentralManager: Bluetooth central role
  • CBPeripheral: Device representation
  • advertisementData: Manufacturer ID extraction

Verification: Console logging shows discovered Apple devices with RSSI values

Task 3: LockCore - Kalman Filter

  • Implement 2-state Kalman filter (position + velocity)
  • Add process and measurement noise parameters
  • Unit tests for filter convergence and noise rejection

Files:

  • Sources/LockCore/SignalProcessing/KalmanFilter.swift: Filter implementation
  • Sources/LockCore/SignalProcessing/FilteredSignal.swift: Output model
  • Tests/LockCoreTests/KalmanFilterTests.swift: Unit tests

Math:

  • State vector: [RSSI, velocity]
  • Measurement: raw RSSI reading
  • Prediction step: estimate next state
  • Update step: correct with measurement

Verification: Tests pass, filter smooths noisy input sequence

Task 4: LockCore - Proximity Monitor

  • State machine: .unknown.present.uncertain.away
  • Integrate Kalman filter for RSSI smoothing
  • Grace period timer (15 seconds) before marking away
  • Configurable thresholds and scan intervals

Files:

  • Sources/LockCore/ProximityMonitor.swift: State machine
  • Sources/LockCore/SignalProcessing/ProximityState.swift: State enum

State transitions:

  • .unknown.present: RSSI above threshold for 3 seconds
  • .present.uncertain: RSSI below threshold
  • .uncertain.away: Grace period expires (15s)
  • .away.present: RSSI above threshold

Verification: Walk away from Mac, state transitions to .away after grace period

Task 5: LockCore - Lock Actions

  • Abstract LockAction protocol
  • ScreenLockAction: Private API dlopen
  • ScreensaverAction: Public API fallback
  • ShortcutAction: macOS Shortcuts integration

Files:

  • Sources/LockCore/Actions/LockAction.swift: Protocol
  • Sources/LockCore/Actions/ScreenLockAction.swift: Private API
  • Sources/LockCore/Actions/ScreensaverAction.swift: Public API
  • Sources/LockCore/Actions/ShortcutAction.swift: Shortcuts.app integration

Private API:

let handle = dlopen("/System/Library/PrivateFrameworks/login.framework/login", RTLD_LAZY)
let lockFunc = dlsym(handle, "SACLockScreenImmediate")

Verification: Manually trigger action, Mac screen locks

Task 6: LockApp - SwiftUI UI

  • Menubar icon with state indication
  • Popover with signal graph, device picker, settings
  • Measure mode UI (large dBm display, Set button)
  • Launch at login toggle

Files:

  • Sources/LockApp/LockApp.swift: App entry point
  • Sources/LockApp/AppState.swift: Observable app state
  • Sources/LockApp/Views/LockControlView.swift: Main popover view
  • Sources/LockApp/IconProvider.swift: Menubar icon logic

UI components:

  • Device dropdown (Bluetooth devices list)
  • Signal graph (30-point RSSI history)
  • Lock Distance slider (-100 to -20 dBm)
  • Lock Action picker (Screen Lock / Screensaver / Shortcuts)
  • Start Monitoring button

Verification: Popover shows live RSSI updates, graph renders

Task 7: Measure Mode Calibration

  • Toggle measure mode (disables lock action)
  • Large dBm display for visibility from distance
  • “Set” button captures lowest RSSI during walk-away
  • Automatically sets threshold to captured value

Files:

  • Sources/LockApp/AppState.swift: Measure mode state
  • Sources/LockApp/Views/LockControlView.swift: Calibration UI

Workflow:

  1. Click “Measure Mode”
  2. Walk away from Mac while watching dBm display
  3. At desired lock distance, click “Set”
  4. Threshold updates to lowest captured RSSI

Verification: Threshold auto-sets to measured value

Task 8: Code Signing & Distribution

  • Configure Developer ID code signing
  • Build release .app bundle
  • Notarize with Apple
  • Package as DMG

Files:

  • scripts/build-release.sh: Release build script
  • scripts/notarize.sh: Notarization automation

Steps:

  1. Build with swift build -c release
  2. Sign .app bundle with Developer ID
  3. Create DMG
  4. Submit to Apple notarization service
  5. Staple notarization ticket

Verification: DMG opens without Gatekeeper warnings

Critical Files

lock/
├── Package.swift                      # SPM config, targets
├── Lock.entitlements                  # Permissions
├── Sources/
│   ├── LockCore/                      # Business logic (no UI)
│   │   ├── Bluetooth/
│   │   │   └── BluetoothManager.swift # CoreBluetooth wrapper
│   │   ├── SignalProcessing/
│   │   │   ├── KalmanFilter.swift     # 2-state filter
│   │   │   ├── FilteredSignal.swift   # Filter output
│   │   │   └── ProximityState.swift   # State enum
│   │   ├── Actions/
│   │   │   ├── LockAction.swift       # Protocol
│   │   │   ├── ScreenLockAction.swift # Private API
│   │   │   ├── ScreensaverAction.swift
│   │   │   └── ShortcutAction.swift
│   │   ├── ProximityMonitor.swift     # State machine
│   │   └── DeviceInfo.swift           # Device model
│   └── LockApp/                       # SwiftUI UI
│       ├── LockApp.swift              # Entry point
│       ├── AppState.swift             # Observable state
│       ├── IconProvider.swift         # Menubar icons
│       └── Views/
│           └── LockControlView.swift  # Main popover
├── Tests/
│   └── LockCoreTests/
│       └── KalmanFilterTests.swift    # Filter unit tests
└── scripts/
    ├── build-release.sh               # Release build
    └── notarize.sh                    # Apple notarization

Verification

Local Testing

swift build              # Debug build
swift test               # Run Kalman filter tests
swift run                # Launch app
# Check: Menubar icon appears, popover opens, devices discovered

Proximity Detection Test

  1. Select Apple Watch/iPhone from device list
  2. Click “Start Monitoring”
  3. Walk away from Mac (10+ feet)
  4. Expected: Screen locks after 15-second grace period
  5. Return to Mac, unlock
  6. Expected: Popover shows “Present” state

Measure Mode Test

  1. Click “Measure Mode”
  2. Walk to desired lock distance while watching dBm
  3. Click “Set” at target distance
  4. Expected: Threshold slider updates to measured RSSI

Release Build

./scripts/build-release.sh
# Check: .app bundle in .build/release/Lock.app
# Check: Signed with Developer ID
./scripts/notarize.sh
# Check: Notarization succeeds, ticket stapled

Trade-offs

Private API vs. Public API

  • Chose private API (SACLockScreenImmediate) for instant locking
  • Trade-off: Not App Store compatible, requires notarization
  • Benefit: Immediate screen lock, best security UX
  • Fallback: Screensaver action uses public API, less reliable

Swift Package Manager vs. Xcode Project

  • Chose SPM for project structure
  • Trade-off: No Xcode project file, harder to configure UI tests
  • Benefit: Simpler dependency management, cleaner version control

Two Targets vs. Single Target

  • Chose two targets (LockCore + LockApp)
  • Trade-off: More complex project structure
  • Benefit: Testable business logic, faster test cycles (no UI overhead)

Kalman Filter vs. Moving Average

  • Chose Kalman filter for RSSI smoothing
  • Trade-off: More complex math, harder to tune
  • Benefit: Better noise rejection, distinguishes walking from interference

How Development Actually Went

The implementation plan above looks structured and complete. That’s because it was.

The development loop was surprisingly clean:

  1. Research mode - When I hit a concept I didn’t know (Kalman filters, CoreBluetooth patterns), ask Claude to explain just enough to understand the problem
  2. Feature requirements - Use feature-dev:feature-dev skill to write clear requirements for what the feature should do
  3. Implementation - Claude writes Swift code while I design logos in nano banana pro
  4. Test - Verify it works, move to next feature

No back-and-forth on syntax. No “how do I write this in Swift” questions. Just research → requirements → working code. I’m not a Kalman filter expert. I’m not a Swift developer. I didn’t need to be. That’s AI-assisted development done right: building in a language you don’t know and don’t need to learn.

Fun Challenges Encountered

Swift Property Wrapper Debugging

SwiftUI’s @Published, @StateObject, @ObservedObject caused runtime bugs that Claude had to debug. The agent explained the mental model: @Published marks a property for change notifications, @StateObject owns the lifecycle of an observable object, @ObservedObject subscribes to an existing object.

Issues the agent encountered and fixed:

  • Views not updating when state changed (missing @Published)
  • State resetting on view re-render (wrong choice of @ObservedObject vs @StateObject)

Lesson: AI-driven development still requires iterative debugging. The agent explained what was happening using React analogies (useState, useEffect) so I could understand the problem conceptually, then it fixed the Swift code. I still don’t know Swift syntax, but I understood the state management architecture.

The dlopen Debuggability Gap

Using private APIs via dlopen means no compiler help. Typos in function names fail silently at runtime:

// This compiles but crashes at runtime if function name is wrong:
let lockFunc = dlsym(handle, "SACLockScreenImmidiate") // Typo!

The fix required reading disassembly output and confirming the exact symbol name. Lesson: Private APIs are fragile. Always have fallback (screensaver action).

CoreBluetooth Delegate Dance

CoreBluetooth’s delegate pattern was verbose. Every callback needs nil checks and state validation:

func centralManager(_ central: CBCentralManager,
                   didDiscover peripheral: CBPeripheral,
                   advertisementData: [String: Any],
                   rssi RSSI: NSNumber) {
    guard RSSI.intValue != 127 else { return } // Invalid RSSI
    guard let name = peripheral.name else { return }
    // ... actual logic
}

Lesson: Bluetooth is inherently callback-based and async. Wrapping it in Combine publishers cleaned up the code significantly.

Kalman Filter Parameter Tuning

Initial Kalman filter had hardcoded noise parameters (process noise: 0.1, measurement noise: 10.0). This worked on my desk but failed in a coffee shop with WiFi interference.

The solution: made parameters configurable, added calibration presets (Stable Environment vs. Noisy Environment). Users can tweak if needed. Lesson: Signal processing parameters are environment-dependent. Provide sensible defaults but allow customization.

Code Signing Mystery Crashes

Release build crashed on launch with cryptic error: “killed by signal 9.” Debug build worked fine. The issue: code signing entitlements didn’t match capabilities.

The fix: Ensure Lock.entitlements exactly matches requested permissions (Bluetooth, notifications). Re-sign bundle after any entitlement change. Lesson: Code signing on macOS is unforgiving. Mismatch = instant termination.

The ironic part: After figuring all this out, I had to wait a week for Apple to activate my developer account before I could actually notarize and sign the app. Completely unnecessary for personal use—the unsigned version worked fine on my Mac—but I wanted to learn the full distribution process. That week of waiting taught me more about Apple’s developer ecosystem than any documentation could.

SwiftUI menubar apps don’t have a traditional window lifecycle. The popover dismisses on outside clicks, but state needs to persist. Initial implementation lost signal history on every open/close.

The fix: Hoist state into AppState (owned by @NSApplicationDelegateAdaptor), not inside the view. Views become stateless renderers. Lesson: SwiftUI menubar apps need careful state management. Use @StateObject at app level, not view level.

Results

First successful test: I walked away from my MacBook Pro, got about 10 feet away, and the screen locked. Came back to see a menubar app with clean UX showing my Watch’s signal strength. My reaction: “wtf?” Not in a bad way—in a “I just built a native Mac app in a language I don’t know and it actually works” way.

Build time: One day (Jan 10, 2026, ~8 hours from initial commit to first working menubar app). Development went surprisingly smoothly. When I hit concepts I didn’t understand (Kalman filters for signal noise, CoreBluetooth delegate patterns), I used research mode to learn just enough to write requirements. Then I’d use the feature-dev:feature-dev skill to specify what the feature should do, and Claude would implement it in Swift while I designed logos in nano banana pro.

Clean separation: I researched problems, specified solutions, and designed UX. Claude wrote Swift code I still can’t read.

Learning outcome: Successfully built a native macOS app in a language I don’t know and still don’t know. If you showed me Swift code right now, I’d have no clue what’s happening. Completely foreign. But I built a working Mac app in it.

Claude chose Swift/SwiftUI, wrote all the code, debugged Swift-specific errors, and guided me through the Mac app distribution process (Developer ID setup, code signing, notarization via developer.apple.com). I provided architecture direction and domain knowledge (Kalman filter math, proximity detection logic), but never touched Swift syntax. The agent translated my technical requirements into working Swift code.

Performance:

  • Detection latency: 2-3 seconds (grace period prevents false triggers)
  • RSSI scan interval: 0.5 seconds (balances responsiveness and battery)
  • False lock rate: ~0 with proper calibration
  • Battery impact: Minimal (CoreBluetooth is efficient for RSSI monitoring)

What’s next: Nothing. It works. It runs. It’s done.

This is my app for my MacBook Pro. Does one thing really well: locks when I walk away. I don’t have to worry about other Apple machines, other devices, other users, support requests, or writing manuals.

That’s the beauty of building for yourself—you can declare victory and move on.


Tech stack: Swift, SwiftUI, CoreBluetooth, Combine, Swift Package Manager Build time: 1 day Code signing: Developer ID with Apple notarization