Skip to content

Security & privacy

Short version: your Claude conversations never leave your computer. No cloud, no telemetry, no analytics, no account. Recall is a program that runs on your laptop, reads files that already exist on your laptop, and shows them to you in your browser — and that's it.

At a glance

| ✅ Safe | 🚫 Can't happen | |---|---| | Everything stays local on your machine | Your sessions get uploaded anywhere | | The web UI is only reachable from your own computer | Someone on your Wi-Fi browsing your history | | Your original Claude Code files are never modified | A website you visit reading your conversations | | Pasted secrets (API keys, passwords) are caught before saving | Secret keys ending up in plain text by accident | | You can verify everything yourself with the commands below | Silent updates phoning home — we don't have telemetry |

How we keep you safe — in plain English

  • Nothing leaves your computer. Recall has no servers to send data to. We couldn't see your sessions if we tried.
  • The web app only works on your machine. The address starts with 127.0.0.1 — that's the universal "this computer only" address. Other devices on your home or office network literally cannot reach it.
  • Other websites can't sneak in. Even if you visit a malicious site while Recall is running, it's blocked by three independent safety checks (see "DNS rebinding" below if you're curious).
  • Your data survives mistakes. Deleting an alias or note doesn't truly delete it — the history is kept so you can always recover.
  • Pasting secrets is a first-class concern. Before the recall paste command stores anything, it scans for API keys, passwords, and private keys. If it finds one, it stops and asks.
  • Secrets pasted inside a Claude Code session (API keys you dropped into your terminal mid-conversation) are auto-redacted at index time — replaced with [REDACTED …] markers before they ever touch Recall's database or search index.
  • No accounts, no telemetry, no trackers. We don't know if you're using Recall and we don't want to.

Trust boundary

| Stays on your machine | Leaves your machine | |---|---| | Every session JSONL Recall reads | Nothing during normal use | | Your SQLite database at ~/.recall/db.sqlite | — | | Your notes at ~/.recall/notes/*.md | — | | Your aliases / tags / pins / paste archive | — | | The web UI (served from 127.0.0.1) | — | | | Only ever: one HTTPS call to validate your license key (v0.6+). Offline forever after that. |

There are no analytics, no crash reporters, no telemetry pings. We don't know if you're using the product and we don't want to.

Network posture

  • The daemon HTTP server binds to 127.0.0.1 only — never 0.0.0.0. A packet from another machine, even on your own LAN, cannot reach it.
  • No outbound requests are made by the daemon or CLI during indexing, browsing, searching, or context export.
  • The browser UI talks to http://127.0.0.1:<port> where the port is chosen dynamically at daemon start (avoiding common dev ports so we don't collide with your other local services).

DNS rebinding & cross-origin defense

Binding to loopback is necessary but not sufficient. A malicious website the user visits could otherwise reach a loopback daemon via DNS rebinding (the attacker's domain first resolves to their server, delivers JavaScript, then re-resolves to 127.0.0.1 — bypassing the same-origin policy). Recall defends in three layers:

  1. Host header allowlist — every request is rejected unless its Host header is 127.0.0.1 or localhost (with optional port). DNS-rebinding attacks send the attacker's hostname, which fails this check.
  2. Origin header validation — browser requests always include Origin; any cross-origin value is rejected with 403. Non-browser tools (curl, the CLI) omit it and are allowed through as normal.
  3. Strict Content-Security-Policy on every response: default-src 'self'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'. Even if attacker content ever rendered, it cannot load external resources, run inline scripts, or be embedded in an iframe.

Additional response headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Cross-Origin-Resource-Policy: same-origin.

XSS hardening in the UI

Session content is rendered as markdown, which means it could in theory contain raw HTML from tool outputs or pasted text. Every markdown render pipes through DOMPurify with a strict allowlist before hitting the DOM:

  • Scripts, iframes, forms, buttons (from untrusted sources), styles, and event handlers are all stripped.
  • href and src attributes are forbidden — javascript: and data: URI vectors are impossible by construction.
  • The only attributes that survive are class and title.

The copy-code button on each code block is trusted markup injected after sanitization, so our own UI keeps working while attacker HTML never does.

Path-traversal defense (paste expand)

The /api/paste-expand endpoint, which can optionally read files referenced by paste placeholders, applies two layered checks: (a) the requested path must actually appear in an indexed paste placeholder within the user's session, and (b) the resolved real path (after symlink resolution) must live under the user's home directory. Requests targeting /etc/passwd, other users' homes, or arbitrary absolute paths are rejected with 403.

Auto-redaction at ingest (session secrets)

When you paste an API key, password, or private key into your terminal during a Claude Code session, Claude Code writes that secret verbatim into the session's JSONL file under ~/.claude/projects/. By default, Recall's indexer would copy that into its own database — making your secret searchable across every session forever.

We don't do that. The indexer runs every message through the same secret scanner used by recall paste before anything lands in SQLite. Matches are replaced with a masked marker like [REDACTED OpenAI API key: sk-pr•••••xyz9] in both:

  • content_text (what the web UI and search see)
  • raw_json (the preserved full record)
  • first_user_message (the session preview shown in lists)

Patterns scrubbed: Anthropic / OpenAI / AWS / GitHub / Stripe / Slack / Google keys, JWTs, SSH-family private key headers, URLs with embedded passwords, and assignment-style secrets (password=, token:, api_key=).

The original JSONL at ~/.claude/projects/ is never modified — that's the durability invariant. Only our derived store is scrubbed, so if you ever need the cleartext for a legitimate reason, it's still in the source file.

Already-indexed sessions (retroactive redaction)

Sessions indexed before this feature landed still have cleartext in ~/.recall/db.sqlite. To retroactively scrub them, run:

recall index --force

This re-ingests every session with the redactor active. Runs locally, takes a few seconds per 1,000 messages.

"Never delete" data durability (aliases, notes, tags)

Every user-created write feature keeps history and mirrors to plain text on disk:

| Feature | SQLite | Plain-text mirror | History | |---|---|---|---| | Aliases | session_aliases | ~/.recall/aliases.json | previous_aliases[] + JSON audit | | Notes | session_notes | ~/.recall/notes/<id>.md | previous_versions[] + full file | | Tags | session_tags + tag_events | ~/.recall/tags.json | append-only event log |

A "cleared" value is stored as an empty string, never hard-deleted. If you nuke the SQLite file, the plain-text mirrors reconstruct everything. This is a deliberate trade-off: your data survives local disasters at the cost of being plain-text-on-disk. See below for the one carve-out.

The paste archive carve-out (v0.4.5)

The opt-in recall paste command is the one feature where you can truly --purge a row. Justification: clipboard content is far more likely to contain secrets than a note or alias, and no "soft-delete" for a leaked API key is acceptable. The carve-out is documented, bounded to one table, and explicitly mentioned here and in the ROADMAP's hard-rules section.

Secret scanner

Before archiving clipboard content, recall paste runs a regex sweep for:

  • Anthropic / OpenAI / AWS / GitHub / Stripe / Slack / Google API keys
  • JWTs
  • SSH / RSA / DSA / EC / OpenSSH / encrypted private key headers
  • URLs with embedded passwords (https://user:pass@host)
  • Assignment-style secrets (password=, token:, api_key=)

When a match fires, the archive is blocked until you explicitly confirm with y or pass --force. In non-interactive mode (piped through a script), a detected secret blocks the archive by default — the content still echoes to stdout so your paste pipeline isn't broken, but nothing is stored.

If something slipped through or you archived something you shouldn't have:

recall paste --list              # see what's archived
recall paste --purge <id>        # permanently destroy one

What we deliberately chose NOT to build

Locked into the roadmap as non-goals:

  • No VS Code / Cursor / Windsurf paste-hook extension. A compromised marketplace update could become a global paste keylogger for every install. Mitigations don't close the distribution-channel attack vector. The v0.7 IDE extension is scoped to non-sensitive metadata only (terminal tab name + shell PID for UUID correlation). It will never touch clipboard contents.
  • No global clipboard monitor daemon. Reads more than it needs, requires OS-level accessibility permissions, becomes a privilege-escalation magnet.
  • No cloud sync by default. The v0.8+ team-sharing feature will be opt-in, end-to-end encrypted, and still local-first — the local DB stays the source of truth; sync is additive.
  • No telemetry, analytics, or crash reporting. We don't track you because we can't — we don't ship the code that would.

Dangerous APIs — audit notes

A small number of surfaces use dangerouslySetInnerHTML. Each is safe by construction:

  • Message body (Message.tsx) — renders markdown via marked, then passes the output through DOMPurify with a strict allowlist (no scripts, no iframes, no href/src, no event handlers). The term-highlight regex only matches inside text nodes (>text<), never inside HTML attributes. No XSS route.
  • Search snippet (SessionsPane.tsx) — the server escapes all HTML entities first, then re-introduces <mark> where FTS marked hits with <<...>>. Attribute-injection is impossible because < and > are already escaped.
  • Help view (this page) — the markdown source is embedded at build time, never user-controlled.

Your data on disk — plain-text reality

Some things you should know by default:

  • Notes are stored as plain markdown files under ~/.recall/notes/. If you write an API key into a note, that key is plain text on disk under your user account. Treat these files like a dotfile — they're your data.
  • Aliases and tags likewise end up in plain-text mirrors (~/.recall/aliases.json, ~/.recall/tags.json).
  • The SQLite database has no row-level encryption. Anything you can read in the UI is readable by any process running as your user.

This is the same posture as every "local-first" tool (Obsidian, TextMate snippets, git dotfiles). Recall does not pretend to be a password manager.

Verifying it yourself

You can confirm the network posture at any time:

# See what the daemon is bound to — should be 127.0.0.1 only
lsof -iTCP -sTCP:LISTEN -P | grep recall

# Watch for any outbound traffic while using Recall — should be zero
sudo tcpdump -i any -n "host not 127.0.0.1 and not 0.0.0.0"

# Confirm Host-header allowlist rejects rebinding attacks (expect 403)
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Host: attacker.example.com" http://127.0.0.1:<port>/api/health

# Confirm cross-origin requests are rejected (expect 403)
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Origin: https://evil.example.com" http://127.0.0.1:<port>/api/stats

# Confirm CSP is present on every response
curl -sI http://127.0.0.1:<port>/ | grep -i content-security-policy

If either of the above ever shows Recall reaching the network, that's a bug — please tell us.