PARANOID local runtime
A daemon on your own computer that sits between you and a remote AI model. It sanitizes prompts locally, shows you exactly what will be sent, routes each conversation through its own Tor circuit, and refuses to send anything when a layer your privacy profile requires is missing.
Overview
PARANOID separates the three things an AI provider usually learns about you: who pays, where the request comes from and what personal context is in the prompt.
Everything runs on 127.0.0.1. The website you are reading is documentation only: prompts, keys and wallet state never go through it.
Install
Windows: one command
In PowerShell, as your normal user (no administrator rights):
irm https://www.paranoid.family/install.ps1 | iex
The script installs into %LOCALAPPDATA%\Programs\PARANOID:
- Node.js 24: the official portable build, checked against nodejs.org's
SHASUMS256.txt(skipped if Node 24+ is already installed). - The runtime: checked against the published
paranoid-local.zip.sha256. - Tor: the official Expert Bundle, checked against torproject.org's signed sums file (and its OpenPGP signature when
gpgis installed). - Ollama and a local model: Ollama via winget, or its installer after an Authenticode signature check; then
qwen3:8b(qwen3:4bunder 16 GB of memory). - Start Menu entries “PARANOID” and “Stop PARANOID”, start at sign-in, and an entry in Settings → Apps to uninstall.
Options, by running the script as a script block:
& ([scriptblock]::Create((irm https://www.paranoid.family/install.ps1))) -Model qwen3:4b -NoAutostart
# -Model <ollama model> -SkipOllama -SkipModel -NoAutostart -NoLaunch -PortableNode
Uninstalling keeps your settings, key and Tor data in %APPDATA%\paranoid; run uninstall.ps1 -RemoveData to delete them too.
Manual install (Windows or Linux)
Linux works the same way (bubblewrap is Linux-native there).
- Node.js 24 or newer. The runtime executes its TypeScript sources directly; there is no build step.
- The runtime: download the zip or clone the repository:
git clone https://github.com/paranoidfamily/paranoid cd paranoid npm install # two pinned dependencies (QuickJS sandbox) npm test # unit, integration and privacy-regression tests - A local model for sanitization. Install Ollama, then:
ollama pull qwen3:8b # about 5 GB; any instruction-following model on loopback works - Tor. The installer downloads the official Tor Expert Bundle into your profile directory and refuses to install unless its SHA-256 matches the published sums file and that file's OpenPGP signature is valid for the Tor Browser signing key:
node scripts/install-tor.mjs --require-signature - Start:
The daemon starts and supervisesnpm start # = node src/main.tstor.exe, then checks throughcheck.torproject.orgthat traffic exits from a Tor relay.
Profile directory: %APPDATA%\paranoid on Windows, ~/.config/paranoid on Linux (override with PARANOID_HOME). It holds config.json, local-token, secrets\, tor\ and tor-data\.
First chat
- Open
http://127.0.0.1:7700(or “Open chat” in the console). - Settings → paste an OpenRouter API key. It is stored with Windows DPAPI and never shown again.
- Pick a remote model and the local sanitizer model (
qwen3:8b). - Choose a profile (start with Private), write a message, review the sanitized text, approve.
Privacy profiles
| Profile | Requires | Sends |
|---|---|---|
| Standard | A model and a key (or zkAPI) | Directly. Sanitized when the local model is available; the original only after explicit approval. |
| Private | Standard + verified Tor + local sanitizer | Sanitized text only, over the conversation's own Tor circuit. |
| Paranoid | Private + zkAPI client + running cover traffic + agent sandbox | Sanitized text over Tor, paid only with a zkAPI lease; your API key is never used. Not available on /v1, which has no wallet. |
A profile never downgrades itself. When a required layer is missing, the chat names it and the API answers 409 privacy_policy without contacting the provider. Choosing Paranoid starts cover traffic automatically (see Cover traffic).
Request pipeline
- Rules (deterministic): secrets and API keys, private keys, wallet addresses, emails, IBANs, Luhn-valid card numbers, IP addresses, phone numbers, street addresses and introduced names become placeholders such as
[EMAIL_1]. Code blocks and inline code are set aside verbatim. - Entity extraction (local model): people and organizations tied to the writer (plus places and dates in strict) are listed and masked deterministically (
[PERSON_1],[ORG_1]). A first or last name alone gets the same placeholder. - Rewrite (local model): the request becomes an impersonal task; unnecessary personal context is generalized; placeholders and code are kept.
- Safety net: the rules run again on the rewrite; a reintroduced identifier is masked again.
- Review (chat UI): you see, can edit and must approve the exact text.
- Send: through the conversation's Tor identity, with an allow-listed request body and no identifying headers.
- Rehydrate: placeholders in the answer are restored locally, including across streaming chunks.
502 sanitize_failed. The original prompt is never sent as a fallback.Sanitizer
| Strictness | What runs |
|---|---|
minimal | Rules only, no local model. Fast; misses names that are not introduced explicitly. |
standard | Rules, entity extraction of people and organizations, rewrite, rules again. Default. |
strict | As standard, and also masks places and dates. |
The sanitizer must be a model server on a numeric loopback address (default http://127.0.0.1:11434/v1, Ollama's OpenAI-compatible endpoint). The address is read only from config.json, never from the HTTP API. Prompts over 128 KiB are refused.
Measured with qwen3:8b on a 12 GB GPU: 0.4–3.5 s per message once the model is loaded, about 13 s for the first message.
Tor routing
The daemon runs tor.exe with SocksPort 127.0.0.1:9050 IsolateSOCKSAuth and no control port. Each conversation gets random SOCKS credentials, which Tor maps to its own circuit, so two conversations do not share an exit. Hostnames are resolved by the exit (no local DNS). There is no fallback to a direct connection in Private or Paranoid.
Verified on a live run: one conversation kept its exit across requests; three conversations got three different exits.
Tor hides your IP address from the provider. It does not hide the request content (that is the sanitizer's job) and does not defeat an adversary who watches most of the network.
Cover traffic
Decoy GET requests through Tor to public HTTPS URLs you choose, with Poisson arrivals, each worker on its own circuit and responses capped at 64 KiB. Decoys never contain your content and never call a paid API.
| Setting | Default | Range |
|---|---|---|
ratePerMinute | 6 | 0.5–60 |
concurrency | 2 workers | 1–8 |
maxRequestsPerHour | 360 | 1–3,600 |
maxBytesPerHour | 64 MiB | 64 KiB–1 GiB |
autoStopMinutes | 60 | 1–1,440 |
In Paranoid, cover traffic starts with the profile once Tor is verified, is renewed at each automatic stop while the profile stays selected, and stops when you leave the profile. A spent hourly budget pauses it until the window frees. If you stop it yourself, it stays stopped and Paranoid reports cover traffic as missing.
Payments
OpenRouter API key
The simplest option. The key is stored with DPAPI (Windows, current user), passed to PowerShell on stdin, never logged and never returned by the API. OpenRouter can link requests made with it to your account; Standard and Private use it.
zkAPI
The wallet is the official zkAPI browser SDK (vendored from ethereum/zkapi at 6fb229a). It runs inside the local chat page: private notes in that page's browser storage, Groth16 proofs in a WebAssembly worker, deposits and withdrawals signed by your browser wallet. The daemon serves the pinned SDK files and relays the SDK's traffic over Tor, with a fresh circuit per request, only to the hosts the SDK's trusted deployment lists. It never sees note secrets or witnesses.
Per request, the chat asks the SDK for a proof-backed ephemeral OpenRouter key bound to that conversation, sends it with the approved prompt and releases it when the stream ends so the SDK can settle. In Paranoid this is the only way to pay.
not_deployed upstream).Agent sandbox
With Sandbox tools on, the model may call tools that run on your machine, under strict isolation. At most 4 tool rounds per message; unknown tools are refused; tool output is re-masked before it goes back to the model; client-side tools on /v1 are refused.
run_js — QuickJS / WebAssembly
JavaScript runs in QuickJS compiled to WebAssembly, in a worker thread: 32 MiB memory, 3 s interrupt deadline, then a hard kill. The guest has no filesystem, network, process, timer or host object (only console.log): egress is impossible by construction.
run_shell — bubblewrap
A POSIX sh script runs under bwrap: every namespace unshared (user, pid, network, ipc, uts, cgroup, mount), uid 65534, no capabilities, empty environment, read-only /usr, empty tmpfs /tmp, no home, no /mnt, only a loopback interface that is down. Limits: 5 s CPU, 512 MiB, 8 MiB files, 128 processes, a 6 s kill. The script is passed on stdin. Before the tool is offered, and again before each run, a probe inside the sandbox must confirm uid 65534, loopback only, no host paths and a read-only /usr; otherwise the tool is not offered.
Linux: sudo apt install bubblewrap. Windows: bubblewrap runs inside WSL2 (wsl --install, then sudo apt install bubblewrap in the distribution).
OpenAI-compatible API
Point any OpenAI client at http://127.0.0.1:7700/v1 with the bearer token from %APPDATA%\paranoid\local-token. Every request goes through the same pipeline without the review step: sanitization is automatic and fail-closed, and answers come back rehydrated.
| Endpoint | Notes |
|---|---|
GET /v1/models | The provider's model list, fetched over Tor in Private. |
POST /v1/chat/completions | Streaming (SSE) and non-streaming. Tool calling is refused. Body up to 4 MiB. |
curl http://127.0.0.1:7700/v1/chat/completions \
-H "Authorization: Bearer $(cat "$APPDATA/paranoid/local-token")" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-haiku-5.5","messages":[{"role":"user","content":"Summarize: ..."}]}'
from openai import OpenAI
token = open(os.path.expandvars(r"%APPDATA%\paranoid\local-token")).read().strip()
client = OpenAI(base_url="http://127.0.0.1:7700/v1", api_key=token)
r = client.chat.completions.create(model="anthropic/claude-haiku-5.5",
messages=[{"role": "user", "content": "Hello"}])
Conversations are matched by transcript prefix, or explicitly with the X-Paranoid-Conversation header; earlier user turns are replayed in their sanitized form. Responses carry X-Paranoid-Conversation, X-Paranoid-Route (tor or direct) and X-Paranoid-Sanitization.
Local API
Used by the chat UI; every call needs the bearer token, and state-changing calls need application/json. Body limit 512 KiB.
| Route | Purpose |
|---|---|
GET /api/status | Profile, readiness, missing layers, component details |
GET /api/profiles | Readiness of all three profiles |
POST /api/config/profile | {"profile":"private"} |
POST /api/config/provider | {"model":"…"} |
POST|DELETE /api/secrets/provider-key | Store or delete the OpenRouter key (never returned) |
POST|GET /api/sessions, DELETE /api/sessions/:id | Conversations (memory only) |
POST /api/privacy/sanitizer/preview | Sanitize a prompt; returns the text to send and the local placeholder map |
POST /api/chat/send | Send an approved preview; SSE events delta, tool, done, error |
POST /api/chat/cancel | Stop a running request |
GET|POST /api/privacy/sanitizer/* | status, models, config |
GET|POST /api/privacy/tor/* | status, start, verify, sessions, reconnect |
GET|POST /api/privacy/cover-traffic/* | status, config, start, stop |
GET /api/payment/* | status; balance, transactions and deposits live in the UI wallet (501 wallet_in_ui) |
POST /api/zkapi/relay | The SDK's network calls, over Tor, allow-listed hosts only |
GET /api/sandbox/status, POST /api/sandbox/bubblewrap | Sandbox health and policy; turn the shell on or off |
Configuration
config.json in the profile directory, validated on every load and change. Defaults:
{
"listen": { "host": "127.0.0.1", "port": 7700 },
"profile": "private",
"provider": { "baseUrl": "https://openrouter.ai/api/v1", "allowedHosts": ["openrouter.ai"], "model": null },
"tor": { "socks": "127.0.0.1:9050", "exe": null, "manage": true },
"sanitizer":{ "baseUrl": "http://127.0.0.1:11434/v1", "model": null, "strictness": "standard", "timeoutMs": 90000 },
"cover": { "urls": [], "ratePerMinute": 6, "concurrency": 2, "maxRequestsPerHour": 360,
"maxBytesPerHour": 67108864, "autoStopMinutes": 60 },
"sandbox": { "bubblewrap": true, "wslDistro": "" }
}
listen.hostmust be127.0.0.1or::1: remote access is not supported.- The provider host must be in
allowedHosts; the sanitizer must be a numeric loopback address. Both are read only from this file (no SSRF through the API). - Cover URLs must be public HTTPS hosts on port 443, at most 16.
wslDistroselects the WSL distribution for bubblewrap; empty means the default one.
Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | privacy_policy | A layer the profile requires is missing; missing lists which. Nothing was sent. |
| 502 | sanitize_failed | Local sanitization failed; the original was not sent. |
| 502 / 504 | provider_error, provider_timeout | The provider refused or timed out; the message includes the provider's reason (for example a rate-limited free model). |
| 401 / 402 | provider | Invalid key, or no credits left. |
| 503 | tor_unavailable | Tor is required and not reachable. |
| 401 | unauthorized | Missing or wrong local bearer token. |
| 403 / 421 | bad_origin, bad_host | Cross-site request or non-loopback Host header (DNS rebinding protection). |
| 413 / 415 | too_large, unsupported_media_type | Body over the limit, or not JSON. |
Security model
- Listens on loopback only. The
Hostheader must be a loopback name, which blocks DNS rebinding. - Requests with a foreign
OriginorSec-Fetch-Site: cross-siteare refused. The only exception is a top-level link that opens the chat page itself; the linking site cannot read or frame it, and every API call stays same-origin. - Every API call needs the local bearer token (constant-time comparison). The page that receives it is protected by
frame-ancestors 'none',X-Frame-Options: DENYand a CSP limiting scripts to the daemon. - Body limits, header timeout, per-call provider and sanitizer timeouts.
- Dependencies are pinned; the Tor bundle is verified by hash and signature; the zkAPI SDK is pinned to a commit and checked by its own asset hashes.
- This website: the console's wallet gate is read-only (it can only read your address, never request a signature or transaction), the console page cannot make network calls (
connect-src 'none'), and the site sends HSTS,nosniff,DENYframing and a restrictive Permissions-Policy.
Data retention
- Conversations, sanitized transcripts, placeholder maps and previews live in daemon memory only, wiped when a conversation ends, after 2 hours idle, or when the daemon stops.
- The chat page keeps messages in page memory; nothing is written to browser storage except the zkAPI wallet's notes.
- Logs are JSON lines with event names and counters only; the logger drops any field whose name suggests content, keys, tokens, identities or URLs.
Limits
- The provider still reads the sanitized task. Sanitization is not anonymization.
- Tor does not protect against a global network observer, and some providers rate-limit Tor exits.
- Cover traffic raises the cost of simple timing analysis; it is not a guarantee.
- zkAPI runs on Sepolia staging with development proving keys and no audit; test funds only.
- The sandboxes are not virtual machines.
- PARANOID has not been independently audited.
Status
| Subsystem | Status |
|---|---|
| Daemon, sessions, profiles | Working, tested |
| Local sanitizer | Working, verified with qwen3:8b |
| Tor, per-conversation isolation | Working, separate exits verified |
| OpenRouter chat | Working, verified end to end over Tor |
| Cover traffic | Working, budgets and auto-renewal in Paranoid |
| Sandbox: QuickJS | Working, tested |
| Sandbox: bubblewrap | Built and tested with a stand-in; not yet run against a real bwrap |
| zkAPI wallet | Wallet starts; deposit, lease and withdrawal not yet run |
| OpenAI-compatible endpoint | Working, tested |
| microVM | Not implemented |
Troubleshooting
“Open chat” does nothing or shows a connection error
The daemon is not running. Start it with npm start in the runtime folder.
Tor not verified
Run node scripts/install-tor.mjs --require-signature, restart the daemon, and check that nothing else uses port 9050. Settings → Tor shows the last error.
Sanitizer unavailable
Ollama is not running or no model is pulled: ollama pull qwen3:8b, then pick it in Settings. If the machine runs out of memory, choose a smaller model.
“Paranoid mode is unavailable”
The banner lists the missing layer. Cover traffic needs at least one URL and verified Tor. Payments need a funded zkAPI wallet; until then, use Private.
“provider: … temporarily rate-limited upstream”
Free models are shared and often saturated. Pick another model or add credits to your OpenRouter account.
License
MIT. Source: github.com/paranoidfamily/paranoid. Logic ported from zkapi-clientd (MIT, Open Anonymity Team); the zkAPI browser SDK is vendored from ethereum/zkapi; QuickJS via quickjs-emscripten (MIT). See THIRD_PARTY_NOTICES.md.
PARANOID is not affiliated with Vitalik Buterin, the Ethereum Foundation, Open Anonymity or the Tor Project.