Skip to content

NetBr3ak/paste-shield

Repository files navigation

Paste Shield

Remove names, emails, API keys and passwords from any text before you paste it into ChatGPT. 100% in your browser, nothing is uploaded.

Paste your text, check what got hidden, then copy the safe version into ChatGPT (or anywhere). Your text never leaves the tab: no servers, no uploads, no API calls. The only download is the AI model itself, once. You can verify this: open your browser's Network tab and watch it stay empty while you type.

Live: tenzanlogic.com/paste-shield Built by Tenzan Logic


Why

Pasting work text into ChatGPT leaks whatever was in it: customer names, invoice numbers, API keys, passwords. Server-side "redaction" tools just move the leak to their server. Paste Shield runs the entire pipeline locally, on a GPU you already own.

How it works

  1. Paste your text (or drop a .txt / .md / .log / ... file). An email, a log, some code, anything.
  2. Private details get hidden. Names, emails, addresses, phones, dates, account numbers, keys and passwords become neutral placeholder labels ({{PERSON_1}}, {{EMAIL_1}}, ...), computed on your device. Spot-check each one and reveal any false match.
  3. Copy or download the safe text. Paste it into ChatGPT or any tool; the placeholders read clearly and your data stays out.

You can also pin always-hide terms (client names, project codenames) that the model cannot infer.

Architecture

Everything inside the dashed boundary runs in the browser tab. The only network egress is a one-time, cached fetch of the model weights.

flowchart TB
    subgraph Tab["Browser tab (nothing leaves here)"]
        direction TB
        UI["page.tsx + PasteShieldApp<br/>React 19, App Router"]
        subgraph Detect["Detection (on device)"]
            direction LR
            Regex["regex-secrets.ts<br/>structured credentials"]
            Custom["custom-terms.ts<br/>always-hide list"]
            Worker["privacy.worker.ts<br/>Web Worker"]
        end
        Merge["merge-entities.ts<br/>merge - dedup - redact"]
        Store[("localStorage<br/>lang + custom terms")]
        SW["service worker<br/>offline shell + ORT cache"]
        UI --> Regex
        UI --> Custom
        UI --> Worker
        Regex --> Merge
        Custom --> Merge
        Worker --> Merge
        Merge --> UI
        UI <--> Store
        SW -.-> UI
    end
    Worker ==>|"model weights, once, then cached"| HF["huggingface.co<br/>AI model + ORT runtime"]
    HF -.->|"cached in CacheStorage"| Worker

    classDef egress stroke-dasharray: 4 4;
    class HF egress;
Loading

Data flow

A keystroke debounces through useDeferredValue, the cheap deterministic nets run instantly, and the model adds free-form PII once it is ready. The model never re-runs on text it has already seen (spans are cached by source string).

sequenceDiagram
    participant U as User
    participant App as PasteShieldApp
    participant Det as regex + custom sweeps
    participant W as privacy.worker
    participant R as redact()

    U->>App: paste / type
    App->>Det: sweep(text)
    Det-->>App: structured spans (instant)
    Note over App: model not ready, so the result is rules-only and Copy stays disabled
    App->>W: run(text) once ready
    W->>W: chunk into windows, locate spans, drop mis-tags
    W-->>App: model entities (or null sentinel on failure)
    App->>R: redact(text, deterministic + model)
    R-->>App: safe text + placeholder map
    App-->>U: highlighted output, Copy unlocked
Loading

Model lifecycle

The worker reports a monotonic load percentage, then settles into one terminal state. It fails closed: an error sticks, and Copy stays disabled until the model is deliberately reloaded.

stateDiagram-v2
    [*] --> lite: no WebGPU or under 4 GB RAM
    [*] --> loading: capable device
    loading --> ready: weights loaded
    loading --> error: load failed (after q4f16 to q4, WebGPU to WASM, cache-wipe retries)
    ready --> loading: Reload model
    error --> loading: Reload model
    lite --> [*]
    note right of ready
        Only "ready" unlocks Copy / Download.
        "lite" runs the deterministic nets only,
        which never substitute for the model.
    end note
Loading

Detection layers

Three layers, all on device:

  1. Privacy AI model (free-form PII): names, emails, addresses, phones, dates, account numbers. Understands sentence context, so it finds values that have no fixed format. Runs in a Web Worker via @huggingface/transformers v4 on WebGPU (required; WASM is only an automatic retry when a WebGPU adapter fails to initialize, and browsers without WebGPU get rules-only Lite mode). We keep every span (no confidence cutoff, since a miss is a leak) and flag low-confidence ones for review.
  2. Deterministic regex net (structured credentials only): API keys, tokens, JWTs, private keys, IBANs, cards, keyword passwords. Runs instantly, even before the model loads, because the model's secret recall is unreliable and a leaked key is catastrophic. It is not a general rule layer: no URL or name rules; a deterministic email rule backstops the model while it loads and in Lite mode.
  3. Your always-hide list: words only you know are sensitive (client names, project codenames).

The model's spans are otherwise only cleaned: boundary-safe offsets, dropping numeric or @-less mis-tags, and protecting any {{LABEL}} you paste back in. Over-redaction is the safe side by design.

Privacy invariants

  • No server-side inference, ever. No OpenAI / Anthropic / Gemini API. No user text crosses the network; the only fetch is the model weights, once.
  • The model runs only in a Web Worker via @huggingface/transformers on WebGPU (required; WASM is only an automatic retry when a WebGPU adapter fails to initialize, and browsers without WebGPU get rules-only Lite mode).
  • ONNX Runtime wasm is self-hosted (public/ort) so a strict CSP keeps connect-src locked to Hugging Face only.
  • Cross-origin isolation headers (COOP / COEP / CORP) are required for the model runtime and set in both next.config.ts and vercel.json.
  • Why script-src still carries 'unsafe-inline': replacing it with build-time sha256-... hashes of the inline scripts was attempted and aborted. Next.js embeds a random build ID in every inline RSC flight payload, so the hash set changes on every build of identical source (verified by hashing two consecutive builds), and hashes pinned in vercel.json could never match the build Vercel itself produces. Revisit if inline scripts become deterministic per commit, or via a nonce-based CSP (which would require dynamic rendering).
  • No analytics, no telemetry. The app ships no tracking of any kind.

Capability gate

The model needs WebGPU and roughly 4 GB of RAM. Without them the app says so honestly: there is no rules-only "safe to paste", because detection is the model or nothing.

Stack

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · @huggingface/transformers v4 · Web Workers · WebGPU · Vercel. Fully bilingual (English / Polski).

Develop

bun install
bun run dev        # copies ORT wasm, then next dev
bun run test       # vitest unit suite for the redaction/detection core

bun run build runs the same ORT copy step before next build. The codebase is intentionally small (around 2k LoC); see AGENTS.md for the hard constraints every change must respect.


Built by Tenzan Logic, a software studio that ships and operates its own production demos.

Case study: tenzanlogic.com/en/portfolio/paste-shield/

Releases

Packages

Contributors

Languages