Security assurance case

This document argues that Floe meets its security requirements for its intended environment: a local macOS launcher used by one person on their own Mac. It follows the structure of Thaw’s assurance case, but Floe’s risks are its own. The largest is that Floe runs third-party extensions without a sandbox.

Update it when a trust boundary or a major feature changes.

Security requirements

You can expect that:

  1. Floe runs on your Mac, with no Floe account and no analytics. Its data stays on the Mac unless you turn on iCloud sync or use a feature that talks to a service you chose, such as an AI provider or a sign-in.
  2. macOS asks before Floe gets a protected permission, such as Accessibility, Calendar or Contacts.
  3. A link from another app can’t complete a sign-in or change Floe’s look unless it answers a request Floe made.
  4. Passwords, sign-in tokens and AI keys are kept in the Keychain. Sign-in tokens belong to the installed extension that saved them, and an AI key belongs to the address it was saved for.
  5. Updates come over HTTPS and are checked with Sparkle’s EdDSA signature. Releases are signed with a Developer ID and notarized. See Verifying releases.
  6. You can report a security problem privately, and the maintainers aim to acknowledge it within 48 hours.

You can’t expect that:

  1. An extension or script command is contained. It runs under your account and can reach your files and the network. Only run code you trust.
  2. Floe protects you from someone who already controls your unlocked Mac session.
  3. Older releases get security fixes. Only the latest stable release is supported.

Threat model

What needs protecting

AssetWhy it matters
Update channelA forged update would run code as you
Sign-in tokens, passwords and AI keysThey give access to your accounts on other services
Your files, clipboard history and snippetsThey can hold private text
Permissions macOS granted to FloeAccessibility, Apple Events, Calendar and Contacts are powerful
Floe’s settingsChanging them could change what Floe does or runs

Scenarios

IDScenarioLikelihoodImpact
T1A malicious or compromised extensionMediumCode running as you, with access to your files
T2Another app opens a crafted floe:// or Raycast linkMediumA forged sign-in or appearance answer
T3Another app posts Floe’s distributed notificationsMediumForged messages between Floe’s two processes
T4Compromise of a dependency or CILow to mediumCode running in Floe
T5A tampered download or updateLowCode running as you
T6A malicious server answers an AI or update requestLowBad text shown, or an update refused
T7A crafted settings import or sync recordLowWrong settings or a crash

Out of scope: physical access to an unlocked Mac, bugs in macOS, and compromise of Apple’s notarization service.

Trust boundaries

  Other apps and scripts                     You
     │ floe:// links, notifications          │ the launcher, Settings, macOS prompts
     ▼                                       ▼
  ┌───────────────────── Floe (your account) ─────────────────────┐
  │  link routing and request checks  │  signed process messages  │
  └───┬───────────────────┬───────────────────┬───────────────┬───┘
      │ stdin/stdout      │ Sparkle           │ AppleScript   │ Keychain
      ▼                   ▼                   ▼               ▼
  Bun, one per command   HTTPS and EdDSA    Other apps      macOS
  (no sandbox)
BoundaryWhat crossesControls
Other apps to Floefloe://, raycast:// and com.raycast: linksIncomingLink accepts known schemes and hosts only, and is fuzzed. A sign-in’s return must carry the state of a waiting sign-in, used once, within ten minutes. Thaw’s answer must carry the random id of the request that is out, used once, within five seconds
Launcher to Settings processDistributed notificationsEach message is signed with a key made for that settings process and handed over a pipe, and numbers must count up, so a copied or forged message is dropped
Floe to an extensionNDJSON over stdin and stdoutA record of what the extension reached, shown in Settings. This is a record and not a limit
Store to your MacExtension source from raycast/extensions over HTTPSYou choose what to install. Dependencies install with scripts off (bun install --ignore-scripts)
Floe to AI servicesPrompts and keysA key goes only to the address it was saved for, and only over HTTPS or to a server on this Mac
Floe to the networkUpdate feed and downloadsHTTPS, and Sparkle’s EdDSA key in SUPublicEDKey. App Transport Security allows plain HTTP only to local addresses, for local model servers. Floe’s own code opens no listening socket
Floe to macOSPermissions and Apple EventsmacOS prompts, and features that wait until you grant the permission. Floe’s own search of browser tabs and files stays off until you switch it on in Settings › Privacy

Secure design principles

These are the Saltzer and Schroeder principles.

PrincipleHow Floe applies it
Economy of mechanismOne link router, and one small file that decides what a link is
Fail-safe defaultsAn unknown link does nothing. Sync, search sources and iCloud passwords are off until you choose them. Extensions install with scripts off
Complete mediationEvery link and every process message is checked when it arrives
Open designThe code is AGPL-3.0, and the security policy and this document are public
Separation of privilegeRelease signing runs only in the prod environment, separate from pull request CI. Settings runs as its own process
Least privilegeFloe doesn’t ask for root, and macOS grants each protected permission separately. Its entitlements are listed in Resources/Floe.entitlements. Extensions are the exception, as the first residual risk says
Least common mechanismEach extension command gets its own Bun process. Floe is a per-user app with no shared service
Psychological acceptabilitySettings shows what each extension has reached, and macOS prompts say why Floe asks

Common weaknesses countered

WeaknessCountermeasure
Memory corruption (CWE-119)Swift, Rust and TypeScript. unsafe is limited to system and language boundaries
Forged requests (CWE-346, CWE-352)State and request ids checked against what Floe asked for. Signed process messages
Injection into AppleScript (CWE-94)Values pass through AppleScript.literal
Credential exposure (CWE-522)Keychain storage, keys bound to their address, no key over plain HTTP to another machine
Insecure update (CWE-494)HTTPS, Sparkle EdDSA, Developer ID and notarization. SLSA provenance and Sigstore signatures on releases
Secrets in the repository (CWE-798)Signing material lives in the prod environment’s secrets
Vulnerable dependencies (CWE-1395)Locked versions, Dependabot and OSV scanning that must pass before merge

CodeQL, SonarCloud and SwiftLint run in CI. A nightly fuzz run with AddressSanitizer covers IncomingLink.

Residual risks

  1. Extensions aren’t contained. This is Floe’s largest risk, and it comes with running unmodified Raycast extensions. An extension can read your files, reach the network, start programs and run scripts in your browser’s tabs through the BrowserExtension API. Floe records what it sees, which a hostile extension could avoid. Floe also doesn’t disclaim responsibility for its child processes, so macOS may treat an extension’s request as Floe’s and apply the permissions you gave Floe.
  2. Store extensions aren’t pinned. They come from the current raycast/extensions tree, so Floe relies on GitHub’s HTTPS and that repository’s review. Floe doesn’t check a signature on them.
  3. Bun runs with loosened code signing. To run JavaScript, the bundled Bun has JIT and unsigned executable memory allowed, library validation off and DYLD variables allowed. Anyone who can already run code as you could use those entitlements, and Bun is signed with Floe’s team.
  4. Sign-in goes through raycast.com for now. Raycast’s page sends the browser back to Floe. It’s temporary and may stop working, as What Floe does not do yet says.
  5. Local apps can see process notifications. Signatures stop forgery, but another app can read a message and its payload, such as the settings page on screen. The one link sent this way is Thaw’s appearance answer; a sign-in’s return is never forwarded.
  6. Fuzzing covers one parser. It covers link routing and the sign-in state. It doesn’t cover Thaw’s appearance answers, settings import or extension manifests, which rely on Swift’s memory safety and unit tests.
  7. Coverage has limits. Statement coverage is measured on SonarCloud against the exclusions in sonar-project.properties. Branch coverage isn’t measured.

Evidence

ClaimEvidence
Reporting and responseSecurity Policy
Attack surfaceExternal interfaces
Link routingSources/Floe/App/IncomingLink.swift, Sources/Floe/App/IncomingURLRouter.swift, .github/workflows/fuzz.yml
Sign-in and token rulesSources/Floe/Extensions/OAuthBroker.swift
Process messagesSources/Floe/App/ProcessLink.swift
AI key rulesSources/Floe/AI/AISources.swift, AIEndpoint in Sources/Floe/Extensions/HostRequest.swift
Update and release authenticityVerifying releases, .github/workflows/release.yml
ArchitectureArchitecture
Automated analysis.github/workflows/ci.yml, codeql.yml, dependency-sca.yml, fuzz.yml, SonarCloud project thaw-app_Floe
Security scansCodeRabbit’s scans of 0.1.0 (#45) and 0.2.0 (#53), with fixes in the changelog

Revision

DateNote
2026-10-30First version, for the OpenSSF Best Practices assurance_case and security_review criteria

On this page