Security
Noted is local-first, so most of its security work is about keeping a desktop app that renders arbitrary HTML and talks to AI providers from doing anything you did not ask for. This page summarizes the posture. To report a vulnerability, see SECURITY.md.
Process isolation
Both the main window and the Quick Capture window run with contextIsolation enabled and nodeIntegration disabled. The renderer has no direct Node access; its only channel to the system is the audited preload bridge.
Content Security Policy
A CSP is injected on every response. In production it is strict — script-src 'self' app: with no unsafe-inline or unsafe-eval, and a connect-src limited to the app itself and loopback. All AI traffic is proxied through the main process rather than made from the renderer, so the renderer never needs a wildcard network grant. The development CSP is loosened only as far as the Vite dev server requires.
Navigation guards
The app refuses to navigate away from itself. Requests to open a new window are sent to the OS browser and denied in-app, and any attempt to navigate the main frame to something other than the app protocol is blocked.
HTML sanitization
Every note body is sanitized with a shared DOMPurify policy — the same policy in the renderer, the main process, and the MCP server — which strips scripts, frames, and dangerous URL schemes. Sanitization runs both on input and before any export, so exported files never carry active content.
Outbound requests (SSRF defense)
The LLM proxy in the main process only allows requests to an allowlist: the known provider hosts, loopback, and the local endpoints you configure for LM Studio or Ollama. Because it is an allowlist rather than a denylist, attempts to reach cloud metadata endpoints, private ranges, or rebindable hosts simply are not on the list and are refused. Requests are also capped in time and size.
Fetching a web page (notes from a source)
The one place the app fetches an address a person typed, rather than a known AI provider, is Notes from a source, and it is done in the main process (electron/ingest-fetch.ts) because a page can lie about where it is. Every address a connection is made to is checked at connect time (a name that answers differently the second time is caught, which a check before the request would miss) against a block list: loopback, private ranges, link-local (the cloud-metadata address), CGNAT, multicast, reserved and documentation ranges, and IPv6 addresses that wrap an IPv4 one (mapped, NAT64, 6to4). A name with any blocked address is refused. Redirects are followed by hand, at most five, each hop checked again; only http and https, no user name or password in the address, no compressed bodies requested, 5 MB and 20 seconds at most, no cookies. The page is parsed with jsdom without running a script or loading anything it refers to.
Secrets
API keys and the GitHub token are stored with the macOS Keychain through Electron's safeStorage, kept only in memory in the main process, and encrypted when OS encryption is available. If it is not, the app falls back to a plaintext buffer and warns you once. Git errors are scrubbed of tokens and authorization headers before they ever reach the UI or a log.
Filesystem confinement
Note paths are validated by one shared rule (shared/vault/paths.ts) in the app, the MCP server and Git: .md only, folders nested at most 16 levels, at most 200 characters and 255 bytes per name, no traversal, no absolute path, and no hidden segment (.noted/, .git/, .obsidian/ and the history and trash folders are never reachable as notes). Paths are resolved through symlinks, starting from the nearest part that exists, so a link anywhere on the way cannot carry a write out of the vault, and the vault walk that lists notes never follows a link. Broad or destructive operations — wiping the vault, copying it elsewhere — only act on vault roots that were explicitly blessed through the native folder picker or a detected cloud path, recorded in an allowlist.
PII masking
Before any request reaches a cloud provider, Noted masks common personal-data patterns (emails, phone numbers, card numbers, and similar) with typed placeholders. It is on by default and applies to every outgoing message, including system prompts; local providers receive text verbatim.
A hint, not a boundary
PII masking is a best-effort filter, not a guarantee. Do not rely on it for regulated or highly sensitive data — review content before sending it to a cloud provider.
MCP server
The optional HTTP transport (Streamable HTTP, and the deprecated SSE) of the MCP server binds to loopback, rejects non-local Host/Origin headers, and requires a constant-time-compared bearer token on every request. The default stdio transport inherits the trust of the client that launched it.
Distribution
Release builds run with the macOS hardened runtime and a minimal set of entitlements, are signed with an Apple Developer ID, and are notarized and stapled by Apple so they pass Gatekeeper offline. Signing and notarization happen locally, never in CI, so no signing credentials are ever exposed to a build server.
Windows and Linux builds are not yet code-signed. Their auto-updates are verified by the SHA-512 checksum in the release metadata (latest*.yml), which protects against tampering in transit, but no signing certificate binds a build to the maintainer — so the trust root is the GitHub release itself. The publishing pipeline is locked down accordingly: the release workflow runs only on version tags (never on pull requests) and publishes to a draft release, and updates are never downloaded or installed without explicit user consent. Always install from the official Releases page.