Privacy
What Halen sees, what stays on your Mac, and the only connection that ever leaves it.
Last updated 9 June 2026 ยท applies to Halen v0.3.0
What Halen sees
Once Accessibility is granted, the CaretObserver reads two AX attributes from whichever text field is currently focused, system-wide:
kAXValueAttribute: the full text of the field.kAXSelectedTextRangeAttribute: the caret offset (and selection if any).
That includes any text you’re editing in any app that exposes a native AX text element: Mail compose windows, Notes, Slack, Notion, Apple Calendar event titles, browser address bars, IDE editor panes, and so on. It does not include the screen pixel buffer, the keyboard event stream, window titles, file system contents, or anything outside the focused field.
Payloads are windowed to 8 000 characters around the caret before being published to the event bus, so a terminal scrollback or a long Notes document can’t blast inference plugins with megabytes of unrelated text.
Plugins that downstream the text further:
- Sentiment Guard re-windows to ~800 chars around the caret before hashing and sending to the on-device model.
- Burnout Copilot re-windows to ~800 chars before its yes/no tone call.
- Snippet Expander (AI snippets) sends the 500 chars immediately preceding the trigger as prior context.
- Meeting Prep sends EventKit event titles, attendee names, and notes. Never anything from focused text fields.
- Typo Fixer does string diffs locally; it never sends text to a model.
- Voice Dictation streams audio buffers to
SFSpeechRecognizerwithrequiresOnDeviceRecognition = true(see below).
What stays local
Everything by default. Concretely:
- Typo dictionary:
~/Library/Application Support/Halen/typos.json. Local file. Editable. - Sentiment rules:
~/Library/Application Support/Halen/com.halen.sentiment-guard/rules.json. Local file. - Approved-draft fingerprints:
approved.json. SHA-256 hex digests of windowed drafts the user marked “Looks fine”. The plaintext is not persisted; only the hash. - Snippets:
com.halen.snippet-expander/snippets.json. Local file. - Briefed-event ids:
com.halen.meeting-prep/processed.json. Contains only EventKit event identifiers, not titles or contents. - Bridge pairing token:
~/Library/Application Support/Halen/bridge-token, 0600 permissions. Used by the optional browser extension to authenticate to the local WebSocket on127.0.0.1:50765.
Network traffic
No inference ever leaves your Mac. The text-processing paths are loopback only — three of them, all to 127.0.0.1:
- Apple Foundation Models: runs in-process via Apple’s on-device API (macOS 26+). No socket at all.
- Bundled llama.cpp + Gemma 4 E4B: also in-process. No socket.
- Ollama (optional): HTTP POST to
http://localhost:11434/api/chat. The base URL is set in code; there is no setting that would let a process change it to a remote host. Ollama itself binds to127.0.0.1by default.
That traffic does not leave the loopback interface unless you have separately configured Ollama to bind to a non-localhost address.
Aside from the software-update check described next, there is no other outbound network code in the project. No analytics SDK, no remote logging endpoint, no crash reporter. The optional WebSocket bridge on 127.0.0.1:50765 only accepts inbound connections from the local browser extension; it never dials out.
Software updates
Halen keeps itself up to date with Sparkle, the standard open-source updater for Mac apps. This is the one connection that does leave your Mac, and it carries none of your content:
- The check: a plain HTTPS
GETofhttps://halen.dev/appcast.xml— a static, public file listing the latest version. It runs once a day and whenever you click Check for Updates in Settings. - The download: if a newer version exists, the signed
.dmgis fetched from GitHub Releases (github.com). Every update is verified against a pinned EdDSA public key before it’s installed, so a tampered or man-in-the-middle payload is rejected. - What’s sent: nothing beyond an ordinary web request. No account, no device identifier, no usage data — just the standard headers (including your IP, as with any HTTPS request) needed to fetch a file. The request body is empty.
Automatic checks are on by default. You can turn them off from the “Automatically check for updates” toggle in Sparkle’s update dialog, after which Halen makes zero non-loopback connections.
Apple on-device speech recognition
Voice Dictation uses SFSpeechRecognizer with:
req.requiresOnDeviceRecognition = true
When requiresOnDeviceRecognition = true, Apple’s recogniser refuses to fall back to a server-side path: if the on-device model for your locale isn’t installed, the request fails outright (recognizerUnavailable) rather than silently round-tripping audio to Apple.
The model itself is downloaded by macOS the first time you enable Dictation for a language (System Settings โ Keyboard โ Dictation). Once installed, recognition runs entirely on the Neural Engine.
EventKit
Burnout Copilot and Meeting Prep ask for full Calendars access via EKEventStore.requestFullAccessToEvents(). Both plugins:
- Read events (titles, start/end, attendees, notes) from the system calendar database.
- Burnout Copilot also writes a single “๐ฟ Halen break” event when the user accepts the break suggestion. No other writes.
Calendar data flows through the same pipeline as everything else: read into memory, sent to the on-device model if needed (Meeting Prep), never persisted outside macOS’s own EventKit store.
Telemetry
There is none. No analytics, no usage metrics, no error reporting, no remote feature flags. Logging goes to stderr and the unified system log. Nothing is uploaded.
Permissions, summarised
| Permission | Used by | Why |
|---|---|---|
| Accessibility | host (CaretObserver) | Read focused text, write back corrections / dictation / snippets |
| Input Monitoring | Ask Halen (โH) | Observe the โH hotkey globally; nothing else is recorded |
| Microphone | Voice Dictation | Capture audio for SFSpeechRecognizer |
| Speech Recognition | Voice Dictation | Convert audio to text on-device |
| Calendars (full access) | Burnout Copilot, Meeting Prep | Read events; Burnout writes the “๐ฟ Halen break” event |
| Notifications | Meeting Prep, Ask Halen | Briefing & clipboard-fallback alerts |
You can deny any of these and Halen continues to run. The dependent plugins surface their own “permission required” detail-view state with a one-click jump to System Settings.
Contact
Questions or concerns about how Halen handles your data? Email the maintainer or open an issue at github.com/lukataylo/halen.