Security overview
Written for someone reviewing whether this is safe to run on a managed Mac. It describes
what the app is, what it can reach, how the Google credential is protected, and who can get
at any of it. The threat model and the full design are in the repository, in
docs/spec.md §8.
Last updated: 30 September 2026
Summary
- A single-user macOS desktop app. No server, no backend, no database, no accounts.
- Two read-only Google Calendar scopes. It cannot create, change or delete anything, and has no access to Gmail, Drive, Contacts or any admin API.
- The refresh token is encrypted by the macOS Keychain, tied to the app’s code signature, and readable only by the signed app running as that one user.
- Calendar content is held in memory only and never written to disk.
- Outbound traffic goes to Google’s endpoints and nowhere else.
- No telemetry, no analytics, no crash reporting, no auto-updater.
What it is technically
An Electron app (Chromium plus Node) for macOS on Apple silicon, written in TypeScript. It has four windows: a floating widget, a settings window, a small quick-add box for typing a TODO, and notifications. There is no Dock icon; it lives in the menu bar.
The app is split into a privileged process, which holds the credential and talks to Google, and sandboxed UI processes, which draw pixels and can do nothing else. Everything sensitive stays on the privileged side of that line. The four runtime dependencies are Google’s official calendar and auth clients, a logger, a settings store, and a schema validator.
Access to Google data
-
Scopes:
calendar.events.readonlyandcalendar.calendarlist.readonly. Read-only, events and the calendar list only. A stolen token could read a calendar; it could not modify one, change sharing, or reach another Google service. - Sign-in happens in your own browser, not in an embedded window, so your existing session, SSO, passkeys and security keys all apply. The app never sees your password.
-
The OAuth flow is the desktop-app pattern: a loopback redirect on
127.0.0.1with a random port, PKCE, and a randomstatecompared in constant time. The listener binds to the loopback interface only, accepts exactly one request on one exact path, rejects any request whoseHostheader does not match (which blocks DNS rebinding), and closes after the first callback or five minutes. There is no listening port at any other time. - Granted scopes are checked, not assumed. Google lets a user untick scopes; if the essential one is missing the app revokes what it was given rather than carrying on in a degraded state.
- Admin control is retained. The client ID can be trusted or blocked centrally under Admin console → Security → API controls → App access control, and access can be revoked there at any time, independently of anything the app or its user does.
Token management
-
At rest: the refresh token is encrypted with Electron’s
safeStorage. The encryption key lives in a macOS Keychain item that only the app’s own code signature can read without a prompt. The ciphertext is written to the app’s support folder with file mode0600and the folder itself is0700, so no other user account on the Mac can read either. - No plaintext fallback. If encryption is unavailable the app refuses to store the token and shows an error, rather than writing it in the clear.
- Access tokens never touch disk. They live in memory, expire within the hour, and are never persisted, logged or sent to the UI.
- Tokens are structurally unreachable from the UI. One module owns them, nothing else imports it, and no message channel returns one. This is enforced by the design rather than by care.
- Never written to logs, messages, the UI, the settings file, environment variables, URLs, notifications, crash dumps or the clipboard.
- Rotation and revocation: a rotated refresh token atomically replaces the old one. Disconnecting calls Google’s revoke endpoint first, then deletes the local ciphertext. A token Google reports as dead is deleted rather than retried, so there is no silent re-consent loop.
- Per-account isolation: each connected account has its own encrypted file and its own client. Revoking one has no effect on the other.
Who has access
One person: the user sitting at the Mac. There is nobody else in the system to grant access to, because there is no system.
- The developer has no access. There is no server to collect anything, no account to sign into, no remote configuration, and no channel through which data could be sent even accidentally. The app opens no network connection to anyone but Google.
-
No other user on the Mac has access. Credentials and settings sit in the
user’s own library folder at
0700, with the token file at0600, and the Keychain item belongs to that login keychain. - No other application on the Mac has access. The Keychain item is bound to this app’s code signature, so a different binary asking for the key does not get it silently.
- Data is never shared or transferred. Nothing is sold, shared, used for advertising, or used to train a model. See the privacy policy.
- Access can be withdrawn without the app’s cooperation, from the Workspace admin console or from the user’s own Google account permissions page. The app is not a dependency of its own revocation.
What is on disk, and what is not
- Calendar events: memory only. Quitting the app discards them.
- Event descriptions and attendee lists: read transiently to find a meeting link and to check whether the invitation was declined, then discarded. They never reach the UI and are never stored or logged.
- The refresh token: encrypted, as above.
- Settings: which calendars to show, which monitor, and similar. No calendar content.
- The TODO list: text the user types themselves, stored unencrypted in the app’s own folder so it survives a restart. It is not calendar data and is never sent anywhere. Stating it plainly: anything typed there is a note on that laptop, with the same exposure as any other file in the user’s home directory.
- Logs: counts, durations, status codes and identifiers. Never event titles, descriptions, attendees, meeting links or credentials. Secrets are stripped by exact key before anything is written, log files are size-capped so retention is bounded, and the crash reporter is never started.
Hardening
-
The UI is fully sandboxed: Chromium’s sandbox on, context isolation
on, no Node access, no network access, no navigation away from the app’s own pages,
no new windows, no permissions (camera, microphone, location, clipboard all denied), and a
strict Content Security Policy. UI code is served over an internal protocol from a
validated archive, never from
file://and never from a remote origin. - Calendar text is treated as hostile. Anyone who can send you an invite controls a title and a location. All of it is rendered as inert text, descriptions never reach the UI at all, and titles are length-capped.
- Every message between the UI and the privileged process is validated against a schema and checked against the origin allowed to send it. There is no generic “call anything” bridge; each window is handed only the specific operations it needs. Actions with side effects are rate-limited.
-
Meeting links are opened by the privileged process only, and only if they
are
https:on a short allowlist of known conferencing hosts. The UI asks to join an event by its identifier and never handles a URL, so a link embedded in an invite cannot become an arbitrary thing to open. - Electron fuses disable the routes that normally turn an Electron app into a scripting host: running as plain Node, injecting options at startup, and attaching a debugger that could read memory. Archive integrity checking is on, and the app refuses to load code from outside the validated archive.
- Hardened runtime, Developer ID signing and Apple notarisation for any build that leaves the developer’s machine, with the minimum entitlements needed to run. The signature is also what the Keychain uses to decide the app is entitled to its own key.
- Privacy mode replaces meeting and TODO titles before they are drawn, in the privileged process, so a screen share or recording captures nothing. It is a real redaction rather than a visual overlay.
Supply chain and updates
- Dependencies are pinned to exact versions with a committed lockfile.
- New releases of a dependency are uninstallable for three days, which is the window in which most hijacked packages are found and pulled.
- Package install scripts are disabled, so installing a dependency cannot execute code.
- Continuous integration runs registry signature and provenance checks plus a vulnerability audit on every change, alongside linting, type checking and the test suite.
- No auto-updater and no update server. Updates arrive through Homebrew, which checks a pinned hash for each release, and Gatekeeper checks the signature and notarisation on first launch. There is no self-update code to get wrong and no update endpoint to compromise.
Limits, stated plainly
Every control above protects against something specific. These are the things it does not protect against, and pretending otherwise would make the rest less useful.
- An attacker already running as that user can use the app’s access, as they could use the Google Calendar session in the browser. The controls raise the cost of lifting the token; they do not survive a compromised account.
- An unlocked laptop shows the calendar, continuously and by design. That is the point of the app. Privacy mode exists for shared screens, but it is opt-in.
- Nothing defends against root, or against code injected into the signed process. No in-process control can.
- Until Google verifies the app, sign-in shows an “unverified app” warning. That is a statement about review status, not about what the app does.
For a reviewer
The source is public and the design document states the threat model, the attack surface with a named control for each entry, and the reasons behind each decision, including the ones that were reversed. The test suite covers the security paths specifically: that credentials never appear in logs, that meeting URLs never reach the UI, that unexpected message senders are rejected, and that privacy mode redacts before anything is displayed.
Questions are welcome, as are findings. Raise them on the source repository, or at the support address on the Google consent screen. If you believe you have found a vulnerability, please report it privately first.