Skip to content

Security: bloknayrb/tandem

docs/security.md

Security

Tandem is designed local-first. The server binds to 127.0.0.1 by default, document content stays on disk, and there are no Tandem-operated servers in the picture.

Network posture

  • Default bind: 127.0.0.1. The MCP HTTP endpoint and Hocuspocus WebSocket only accept connections from the local machine.
  • LAN exposure (opt-in): set TANDEM_BIND_HOST=0.0.0.0 (or a specific interface) to expose Tandem on a LAN. Non-loopback requests require a Bearer token by default; Tandem auto-generates one on first run and stores it at {APP_DATA_DIR}/tandem_auth_token with mode 0o600.
  • Loopback detection is fail-closed. Authentication middleware uses req.socket.remoteAddress exclusively — never the Host header — so DNS rebinding attacks cannot trick the server into treating a remote request as loopback. IPv6 variants (::1, ::ffff:127.0.0.1) are normalized to 127.0.0.1.
  • Insecure LAN opt-in: TANDEM_ALLOW_UNAUTHENTICATED_LAN=1 disables the token requirement for non-loopback requests. Intended for trusted-network development only; never set it on a public network.

See configuration.md for the full environment-variable reference (ports, bind host, auth token, app-data paths).

CORS allowlist

The server accepts cross-origin requests from two origins only:

  • http://127.0.0.1:* (any port)
  • http://tauri.localhost (the Tauri WebView's fixed origin)

Bare http://localhost was narrowed out in PR #637 because it bypassed DNS-rebinding hardening. Hocuspocus WebSocket origin validation uses the same allowlist.

Auth tokens

  • Generation: 32 random bytes, base64url-encoded.
  • Storage: {APP_DATA_DIR}/tandem_auth_token, mode 0o600, written atomically (temp file + rename).
  • Comparison: both sides SHA-256-hashed, then compared with crypto.timingSafeEqual to prevent length-oracle attacks.
  • Rotation: tandem rotate-token generates a new token, posts it to /api/rotate-token, and updates MCP client configs. The old token remains valid for a 60-second grace window so connected clients can pick up the new value without a disconnect.

Privacy

  • Notes are user-private (ADR-027). Annotations with type: "note" are stripped from every MCP tool response and never appear in channel events. The AI cannot read them.
  • What the AI sees: the document content you open, selections you hold (subject to dwell-time gating), annotations you create or that the AI itself creates, and chat messages sent through the Tandem sidebar.
  • What the AI doesn't see: files you haven't opened, notes (per above), the auth token, and any environment variables that aren't surfaced through MCP tools.

Telemetry: none by default, crash reporting strictly opt-in

Tandem ships with no usage analytics and no telemetry beacons, and crash reporting is off by default. The only outbound traffic Tandem initiates out of the box is to your configured AI client over loopback (or LAN, if you opted in) and — for the desktop app — periodic update checks against the public release host. Update checks carry no analytics.

Crash reporting is available but strictly opt-in: it activates only when you set the TANDEM_SENTRY_DSN environment variable to a Sentry or self-hosted GlitchTip DSN that you control. With the variable unset (the default), no Sentry client is initialized in the desktop shell, the Tauri Sentry plugin is never registered, the WebView is never instrumented, and @sentry/node is never even loaded in the sidecar — there is no crash-reporting code path on the wire. When you do opt in, Tandem reports Rust panics + native minidumps (shell), JavaScript errors / unhandled rejections (WebView, bridged over Tauri IPC), and Node uncaught exceptions (sidecar) to your endpoint, scrubbing home-directory paths to ~/[user], redacting Anthropic/bearer-style secrets, and dropping request/document payloads and content breadcrumbs before egress. Document content and annotation bodies are never attached to events. Self-hosting GlitchTip keeps all crash data under your control. Settings → About shows the current on/off status. Implemented in src-tauri/src/sentry_reporting.rs, src/client/sentry.ts, and src/server/sentry.ts (#921).

Licensing activation (v1.0)

This describes the paid model arriving at v1.0; during the public beta Tandem is free and unlicensed. The system is implemented but ships dark behind a build flag (LICENSE_GATE_ENABLED, off until v1.0), so beta builds enforce nothing. See ADR-040 and docs/licensing-operations.md.

  • Running the app validates a license offline. The app verifies an Ed25519-signed license file locally against a public key embedded in the build. Running needs no network call — activation works air-gapped, and the signature is checked on your own machine. This is a separate key from the Ed25519/minisign key used to sign release artifacts. A valid license runs the current version forever; the signature alone gates running, while expiresAt governs only the update window.
  • The license file binds a copy to its buyer. It carries the buyer's email, an order ID, a type (personal/commercial/grandfathered), and an update-window expiry, all covered by the signature.
  • Enforcement is server-side, not client-trust. When the gate is active and a trial has expired with no license, the local server marks Hocuspocus document-room connections read-only AND gates Claude's mutation tools / mutating /api routes — both re-checked per request. The escape hatch is preserved: you can always open, read, and export your local files (and chat keeps working). The trial clock is deliberately soft (a local timestamp, no anti-rollback); the signed license is the only hard gate.
  • Updates authenticate entitlement without telemetry. The licensed updater sends an opaque license id (a UUID, not the buyer's key or email) to a small license-checked endpoint that serves new builds only while the update window is current. Unknown ids and expired windows get a byte-identical no-update response (no existence oracle), and the endpoint logs only { result, timestamp } — never the id — so the no-telemetry posture holds. An expired-window license still runs offline; it simply stops receiving new builds.

Reporting security issues

Email security reports to the maintainer listed in package.json or open a private security advisory at https://github.com/bloknayrb/tandem/security/advisories/new. Please don't file public issues for vulnerabilities.

There aren't any published security advisories