2026-10-28 · 4 min

nubli, hide private data before you paste it into an ai

A small local tool that swaps names, IDs, emails and accounts for placeholders, so you can ask an AI about a real document without handing over the real data.

A lot of my questions for an AI start with a document I shouldn't share as is. A contract, a client report, a spreadsheet with names and account numbers. The question is about the structure or the wording, not about who the client is. But the names come along anyway.

Nubli is the tool I built for that. It hides the private parts of a document before you paste it anywhere.

What it does

You give it a file. It finds names, companies, ID numbers, phones, emails, bank accounts and addresses, and replaces each one with a placeholder that keeps the type: PERSON-0001, COMPANY-0001, EMAIL-0001@example.com. The same value always gets the same placeholder, so the AI can still follow who is who.

It reads Markdown and plain text, CSV, Word, Excel, PDFs and images. Scanned files work with OCR if you have Tesseract installed.

Everything runs on your machine. It's one Python file, it makes no network calls, and it's MIT licensed.

What it looks like

This is a fake contract:

Entre Constructora Ríos S.A. y María Fernández, cédula 8-765-4321.
Dirección: Calle 50, Edificio Demo, Piso 3
Teléfono: 6000-0000
Correo: maria.fernandez@constructorarios.example
Cuenta: 04-12-34-567890-1
Pago mensual: B/. 4,500.00

I ran it with one rule for the company:

nubli contrato.md --replace "Constructora Ríos S.A.=Demo Company" -o salida.md

And got this:

Entre Demo Company. y PERSON-0001, cédula ID-0001.
Dirección: ADDRESS-0001, PERSON-0002, Piso 3
Teléfono: PHONE-0001
Correo: EMAIL-0001@example.com
Cuenta: ACCOUNT-0001
Pago mensual: B/. 4,500.00

The amount stays, because that's usually what you want to ask about. "Edificio Demo" got the wrong label, PERSON-0002 instead of part of the address. It's still hidden, but it shows the detection is a guess, not a guarantee.

Your own rules

Automatic detection can't know that "Ríos" is your client and "Calle 50" is just a street. So you can tell it. --replace takes original=replacement, and it also matches variants: Constructora Rios SA, CONSTRUCTORA RÍOS, S. A. and small typos. With many rules, you put them in a JSON file and pass --rules rules.json.

If you don't want to remember flags, running nubli with no arguments opens a menu that asks for the file, the output and the rules step by step.

PDFs with logos

Some documents have the client's name in a logo or a scanned header, where there's no text to replace. For those, --format images renders each page as an image and covers parts of it:

  • --redact-header 0.12 covers the top 12% of every page.
  • --redact-region "40,30,220,90=LOGO" covers a specific box.

Safe by default

A few things it does without asking:

  • If nothing matched, it refuses to write the output. A file with zero replacements is more likely a mistake than a clean document.
  • It won't write the output over the original file.
  • The JSON report lists what was replaced without including the original values.
  • The optional name detection (spaCy) only loads a model you already have. It never downloads one.

What testing it found

Before writing this post I had my coding agent run Nubli on fake contracts like the one above. The first run leaked an email: the company rule matched constructorarios inside the domain, broke the address with a space, and the email detector no longer recognized it. It also broke an amount and cut an account number in half.

All of that is fixed in this pull request, with a test for each case. The example above is the output after the fix.

Limits

Nubli is early alpha. It catches a lot, but it's pattern matching, not understanding. Two things I'd do with any real document:

  • Pass --strict-pii. Without it, Nubli warns about values it isn't sure of but still writes the file.
  • Read the output before you share it.

The code is on GitHub. If you try it on your own documents and something gets through, open an issue with a fake version of the text. That's exactly the kind of case it needs.