Docs Site GitHub Console
Documentation

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.

Content
Local sanitizer
Names, emails, keys, numbers replaced before anything leaves.
Network
Tor
One circuit per conversation; the provider sees a Tor exit.
Timing
Cover traffic
Bounded decoy requests blur usage patterns.
Payment
zkAPI
Zero-knowledge leases instead of an account-linked key.

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:

  1. Node.js 24: the official portable build, checked against nodejs.org's SHASUMS256.txt (skipped if Node 24+ is already installed).
  2. The runtime: checked against the published paranoid-local.zip.sha256.
  3. Tor: the official Expert Bundle, checked against torproject.org's signed sums file (and its OpenPGP signature when gpg is installed).
  4. Ollama and a local model: Ollama via winget, or its installer after an Authenticode signature check; then qwen3:8b (qwen3:4b under 16 GB of memory).
  5. 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.

Piping a script into PowerShell runs it with your user rights. Read it first if you prefer: it is short, and every file it downloads is verified before use.

Manual install (Windows or Linux)

Linux works the same way (bubblewrap is Linux-native there).

  1. Node.js 24 or newer. The runtime executes its TypeScript sources directly; there is no build step.
  2. 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
  3. A local model for sanitization. Install Ollama, then:
    ollama pull qwen3:8b     # about 5 GB; any instruction-following model on loopback works
  4. 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
  5. Start:
    npm start          # = node src/main.ts
    The daemon starts and supervises tor.exe, then checks through check.torproject.org that 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

  1. Open http://127.0.0.1:7700 (or “Open chat” in the console).
  2. Settings → paste an OpenRouter API key. It is stored with Windows DPAPI and never shown again.
  3. Pick a remote model and the local sanitizer model (qwen3:8b).
  4. Choose a profile (start with Private), write a message, review the sanitized text, approve.
The review step shows the exact text the provider will receive, and what stays on your device. You can edit it. Nothing is sent until you approve.

Privacy profiles

ProfileRequiresSends
StandardA model and a key (or zkAPI)Directly. Sanitized when the local model is available; the original only after explicit approval.
PrivateStandard + verified Tor + local sanitizerSanitized text only, over the conversation's own Tor circuit.
ParanoidPrivate + zkAPI client + running cover traffic + agent sandboxSanitized 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

  1. 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.
  2. 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.
  3. Rewrite (local model): the request becomes an impersonal task; unnecessary personal context is generalized; placeholders and code are kept.
  4. Safety net: the rules run again on the rewrite; a reintroduced identifier is masked again.
  5. Review (chat UI): you see, can edit and must approve the exact text.
  6. Send: through the conversation's Tor identity, with an allow-listed request body and no identifying headers.
  7. Rehydrate: placeholders in the answer are restored locally, including across streaming chunks.
Any failure in steps 1–4 (model server down, invalid JSON, wrong count, timeout) aborts with 502 sanitize_failed. The original prompt is never sent as a fallback.

Sanitizer

StrictnessWhat runs
minimalRules only, no local model. Fast; misses names that are not introduced explicitly.
standardRules, entity extraction of people and organizations, rewrite, rules again. Default.
strictAs 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.

Sanitization reduces what the provider learns; it is not anonymization. The provider still reads the sanitized task, and the task itself or your writing style can identify you. The rewrite can also lose nuance (in one recorded run “make it warmer” became “adjust the temperature”), which is why you review it.

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.

SettingDefaultRange
ratePerMinute60.5–60
concurrency2 workers1–8
maxRequestsPerHour3601–3,600
maxBytesPerHour64 MiB64 KiB–1 GiB
autoStopMinutes601–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.

Current limits, as stated upstream: Sepolia staging only, single-party development proving keys, no audit, and the operator requires a shared testnet password. Requests sharing a lease are linkable to each other. Mainnet is not available in the current SDK (its profile is marked 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).

Neither sandbox is a virtual machine. bubblewrap shares the Linux kernel, so a kernel exploit would escape it.

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.

EndpointNotes
GET /v1/modelsThe provider's model list, fetched over Tor in Private.
POST /v1/chat/completionsStreaming (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.

RoutePurpose
GET /api/statusProfile, readiness, missing layers, component details
GET /api/profilesReadiness of all three profiles
POST /api/config/profile{"profile":"private"}
POST /api/config/provider{"model":"…"}
POST|DELETE /api/secrets/provider-keyStore or delete the OpenRouter key (never returned)
POST|GET /api/sessions, DELETE /api/sessions/:idConversations (memory only)
POST /api/privacy/sanitizer/previewSanitize a prompt; returns the text to send and the local placeholder map
POST /api/chat/sendSend an approved preview; SSE events delta, tool, done, error
POST /api/chat/cancelStop 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/relayThe SDK's network calls, over Tor, allow-listed hosts only
GET /api/sandbox/status, POST /api/sandbox/bubblewrapSandbox 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.host must be 127.0.0.1 or ::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.
  • wslDistro selects the WSL distribution for bubblewrap; empty means the default one.

Errors

StatusCodeMeaning
409privacy_policyA layer the profile requires is missing; missing lists which. Nothing was sent.
502sanitize_failedLocal sanitization failed; the original was not sent.
502 / 504provider_error, provider_timeoutThe provider refused or timed out; the message includes the provider's reason (for example a rate-limited free model).
401 / 402providerInvalid key, or no credits left.
503tor_unavailableTor is required and not reachable.
401unauthorizedMissing or wrong local bearer token.
403 / 421bad_origin, bad_hostCross-site request or non-loopback Host header (DNS rebinding protection).
413 / 415too_large, unsupported_media_typeBody over the limit, or not JSON.

Security model

Each property below is linked to its test and to a public verification run on the proof page.
  • Listens on loopback only. The Host header must be a loopback name, which blocks DNS rebinding.
  • Requests with a foreign Origin or Sec-Fetch-Site: cross-site are 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: DENY and 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, DENY framing 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

SubsystemStatus
Daemon, sessions, profilesWorking, tested
Local sanitizerWorking, verified with qwen3:8b
Tor, per-conversation isolationWorking, separate exits verified
OpenRouter chatWorking, verified end to end over Tor
Cover trafficWorking, budgets and auto-renewal in Paranoid
Sandbox: QuickJSWorking, tested
Sandbox: bubblewrapBuilt and tested with a stand-in; not yet run against a real bwrap
zkAPI walletWallet starts; deposit, lease and withdrawal not yet run
OpenAI-compatible endpointWorking, tested
microVMNot 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.