Docs
How to use Tightlip
Tightlip swaps sensitive details for placeholders on your device, sends the AI only the placeholders, then puts the real values back into the answer. This page covers what it finds, the SDK, the command line tool, the browser extension and the metering API.
How Tightlip works
Before your text goes to an AI, Tightlip reads it on your device and swaps each sensitive item for a placeholder. A line like Jane Doe, SSN 123-45-6789 reaches the AI as [PERSON_1], SSN [SSN_1]. The AI works with the placeholders. When its answer comes back, Tightlip puts the real values back in, again on your device. The table that links each placeholder to its real value is called the vault. The vault stays on your device. It is never sent to the AI or to Tightlip.
Placeholders
- The format is
[TYPE_N]: [PERSON_1], [SSN_2], [CASE_NUMBER_1]. Numbers count up separately for each type. - The same value always gets the same placeholder within one conversation. Names and organisations match whatever the capital letters. Numbers match on their digits, so
123-45-6789and123456789share one placeholder. - People are linked. If Jane Doe is [PERSON_1], a later “Ms. Doe” becomes [PERSON_1_LAST] and a later “Jane” becomes [PERSON_1_FIRST]. The AI can still tell who is who.
- Restore copes with the ways models reshape placeholders: [Person_1], [PERSON 1],
PERSON_1without brackets,\[PERSON\_1\]from Markdown escaping, bold or italic wrappers and possessives such as [PERSON_1]’s. A placeholder Tightlip does not know is left as it is. - Streamed answers work too. If a chunk ends halfway through a placeholder, Tightlip holds those few characters back until the placeholder is complete.
Where the vault lives
| Where you use Tightlip | Where the vault is kept |
|---|---|
| SDK and proxy | In the memory of your own process. |
| Browser extension | In the browser’s session storage, which is cleared when the browser closes. You can choose to keep it on this device for 30 days. |
| Web workspace | In the page’s memory until you close the tab. |
| Leak check | In the page’s memory. Nothing is sent anywhere. |
What gets detected
Detection is automatic and it will miss things. Now and then it will also hide something harmless. Read the preview before you send. For anything that must be hidden every time, such as client names, matter names and project code names, add it to customTerms in the SDK or to Custom terms in the extension and the workspace. Custom terms are always protected.
Tightlip finds things in three ways:
- Checked patterns. The shape has to match, and where the number has a built-in check digit the maths has to work out. A 16-digit number that fails the card checksum is left alone.
- A keyword just before. Some numbers look like any other number. Tightlip protects them only when a keyword such as “account”, “MRN” or “passport” comes within about 40 characters before them, in the same sentence.
- Name lists and cues. People are found after titles (Mr, Ms, Dr, Judge), role words (patient, client, plaintiff, borrower) and labels such as “Name:”. Greetings, sign-offs and “X v. Y” case captions count too. So does a common first name followed by a capitalised surname. The name lists come from public-domain US data (Social Security first names and Census surnames). Names that are rare in the US are found less often, so add them as custom terms.
Most formats are US formats: US street addresses, US phone numbers and US government IDs. International phone numbers written with a leading + and IBANs are covered too. When two matches overlap, the longer and more confident one wins.
People and organisations
| Placeholder | How Tightlip finds it |
|---|---|
| [PERSON_N] | Found after titles, role words and labels, and in greetings, sign-offs and case captions. Also a first name from the list followed by a capitalised surname. Words that are also names, such as April, May or Bill, need extra evidence. |
| [ORG_N] | A capitalised name ending in a company or institution word: LLC, Inc, Corp, Ltd, LLP, PLLC, P.C., N.A., Bank, Credit Union, Hospital, Clinic, Medical Center, Health, Partners, Holdings, Capital, Group, Foundation, Trust and similar. Also names such as “Bank of …” and “University of …”. |
| [CUSTOM_N] | Anything on your custom terms list, matched as a whole word whatever the capital letters. Give a term a type, such as PERSON, and it gets that placeholder instead. |
Contact details
| Placeholder | How Tightlip finds it |
|---|---|
| [EMAIL_N] | Email address pattern. Image file names such as icon@2x.png are skipped. |
| [PHONE_N] | US numbers written with dashes, dots, spaces or brackets, with or without +1. International numbers that start with +. A bare run of 10 digits only when a word such as phone, cell or fax comes just before it. |
| [ADDRESS_N] | US street addresses: a house number, a street name and a suffix such as St, Ave or Blvd, with an optional unit, city, state and ZIP. PO boxes too. |
| [ZIP_N] | A 5-digit or ZIP+4 code after a state abbreviation, or after the words ZIP or postal code. |
Government IDs
| Placeholder | How Tightlip finds it |
|---|---|
| [SSN_N] | Nine digits written 3-2-4 with dashes or spaces, checked against the numbers that are never issued (area 000, 666 or 900 and up, group 00, serial 0000). Nine digits in a row only when SSN or social security comes just before. |
| [ITIN_N] | The same shape as an SSN, in the ranges used for ITINs. |
| [EIN_N] | A number such as 12-3456789 when EIN, tax ID, TIN or employer identification comes just before. |
| [PASSPORT_N] | 6 to 10 letters and digits after the word passport. |
| [DRIVER_LICENSE_N] | 5 to 16 letters and digits after driver’s license, DL no. or license no. |
| [VIN_N] | A 17-character vehicle number with a valid check digit, or any 17-character vehicle number after VIN. |
| [NATIONAL_ID_N] | National ID numbers outside the US: Pakistan CNIC and NICOP, UAE Emirates ID, India Aadhaar and PAN, UK National Insurance and NHS numbers, Canada SIN, Australia TFN. Also any number after a label such as CNIC #, ID no., NTN or national ID, even when it is not in the standard format. |
Financial
| Placeholder | How Tightlip finds it |
|---|---|
| [CREDIT_CARD_N] | 13 to 19 digits that pass the Luhn checksum and fall in a real card issuer range. |
| [IBAN_N] | A country code and check digits, confirmed with the IBAN mod-97 check. |
| [SWIFT_N] | An 8 or 11 character bank code, only after SWIFT, BIC or wire. |
| [ROUTING_NUMBER_N] | Nine digits that pass the ABA routing checksum, after routing, ABA or RTN. |
| [BANK_ACCOUNT_N] | 6 to 20 digits after account, acct, checking, savings or loan no. |
Health
| Placeholder | How Tightlip finds it |
|---|---|
| [MRN_N] | A record number after MRN, medical record, chart no. or patient ID. |
| [HEALTH_PLAN_ID_N] | An ID after member ID, subscriber ID, policy number, group number, plan ID or Medicaid. |
| [NPI_N] | Ten digits that pass the NPI checksum. Protected after the word NPI in every profile. Protected without the keyword only in the healthcare profile. |
| [DEA_N] | Two letters and seven digits that pass the DEA checksum. |
| [MBI_N] | The 11-character Medicare Beneficiary Identifier format. |
| [DOB_N] | A full date shortly after DOB, date of birth, born or birthday. |
| [DATE_N] | Every full date, such as 03/05/1980 or March 5, 1980, in the healthcare profile only. A year on its own is left alone. |
| [AGE_N] | Ages of 90 and over, written as “age 92” or “95-year-old”. |
Legal
| Placeholder | How Tightlip finds it |
|---|---|
| [CASE_NUMBER_N] | Federal docket numbers such as 1:23-cv-04567. Any number after Case No., Docket No., Index No., Civil Action No., Claim No., Matter No. or File No. |
Technical
| Placeholder | How Tightlip finds it |
|---|---|
| [SECRET_N] | API keys and tokens with known shapes: OpenAI sk-, Anthropic sk-ant-, AWS AKIA, GitHub ghp_, Slack xox, Google AIza, Stripe, npm, JWTs and more. Private key blocks and database URLs with a password in them. Values set on names such as password, api_key or token. |
| [URL_N] | Links that look personal: a user name or password inside the link, a query string with values, a path such as /users/… or /patients/… followed by an ID, or a long mixed ID. A plain link to a home page is left alone. |
| [IP_ADDRESS_N] | IPv4 and IPv6 addresses. Localhost addresses and software version numbers are skipped. |
| [DEVICE_ID_N] | Serial numbers and IMEIs after serial, S/N, device ID or IMEI. |
Profiles
A profile tunes detection for the kind of text you work with. Pick one in the extension popup, in the workspace or with the profile option in the SDK. Today one profile changes the rules: healthcare. The others use the same rules as general for now.
| Profile | What changes |
|---|---|
general | The default. Dates are protected only when they look like a date of birth. |
legal | Same rules as general. Case numbers and parties in “X v. Y” captions are found in every profile. |
healthcare | Every full date is protected, and NPI numbers are protected even without the word NPI. It still misses things, so use it with AI tools your organisation already allows for patient information. |
finance | Same rules as general. Card, account, routing, IBAN and SWIFT numbers are found in every profile. |
developer | Same rules as general. API keys, tokens and IP addresses are found in every profile. |
SDK
The SDK is the npm package tightlip. It runs on Node 18 or later and wraps the official OpenAI and Anthropic clients.
Install
Until the package is on the npm registry, install it from our download link:
npm install https://tightlipai.com/downloads/tightlip-0.1.0.tgzMetered calls need an API key. Get one at /app/keys and keep it in an environment variable called TIGHTLIP_API_KEY.
Quick start
import { Tightlip } from 'tightlip';
const tl = new Tightlip({ apiKey: process.env.TIGHTLIP_API_KEY, profile: 'legal' });
// One session per conversation, so a name keeps the same placeholder every turn.
const s = tl.session();
const safe = await s.protect('Draft a reply to Jane Doe about case 1:23-cv-04567.');
// safe.text is 'Draft a reply to [PERSON_1] about case [CASE_NUMBER_1].'
const modelOutput = await callYourModel(safe.text);
const answer = s.restore(modelOutput); // real values back, on your machineWrap your OpenAI or Anthropic client
wrapOpenAI and wrapAnthropic return a client that you use exactly like the one you passed in. Each request is protected before it leaves your server, and each answer is restored before your code sees it, streamed answers included. The OpenAI wrapper covers chat.completions.create and responses.create. The Anthropic wrapper covers messages.create and messages.stream.
import OpenAI from 'openai';
import { Tightlip } from 'tightlip';
const tl = new Tightlip({ apiKey: process.env.TIGHTLIP_API_KEY, profile: 'finance' });
const openai = tl.wrapOpenAI(new OpenAI());
const res = await openai.chat.completions.create({
model: 'your-model-id',
messages: [{ role: 'user', content: 'Summarise the dispute on account 00123456789 for John Smith.' }],
});
console.log(res.choices[0].message.content); // real values restored
// Streaming works the same way.
const stream = await openai.chat.completions.create({
model: 'your-model-id',
messages: [{ role: 'user', content: 'Write a short note to John Smith.' }],
stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? '');import Anthropic from '@anthropic-ai/sdk';
import { Tightlip } from 'tightlip';
const tl = new Tightlip({ apiKey: process.env.TIGHTLIP_API_KEY });
const anthropic = tl.wrapAnthropic(new Anthropic());
const msg = await anthropic.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Summarise this intake note for patient Maria Lopez, MRN 00482913.' }],
});
// Streaming
const stream = anthropic.messages.stream({
model: 'claude-haiku-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Draft a follow-up for Maria Lopez.' }],
});
stream.on('text', (text) => process.stdout.write(text)); // restored as it arrives
await stream.finalMessage();Each wrapped call uses a fresh vault. To keep numbering stable across a conversation, pass a session: tl.wrapOpenAI(client, { session: tl.session() }). The same option works for wrapAnthropic.
Options
Pass these to new Tightlip() alongside apiKey.
| Option | Default | What it does |
|---|---|---|
profile | 'general' | One of general, legal, healthcare, finance, developer. See Profiles. |
customTerms | none | Words to protect every time, such as client and matter names. Each entry is { term, type? }. The type picks the placeholder and is CUSTOM if you leave it out. |
allowTerms | none | Words never to protect, such as your own firm’s name. A match is left alone when its whole text equals an allowed term, ignoring capital letters. Check the preview once, because a name inside the term can still be caught on its own. |
types | all | Only look for these types. Custom terms are always protected. |
minConfidence | 0.5 | Every match gets a score from 0 to 1, and matches below this number are skipped. Lower it to catch more. Raise it to hide less. A bare 10-digit number with no phone keyword scores below 0.5. |
const tl = new Tightlip({
apiKey: process.env.TIGHTLIP_API_KEY,
profile: 'legal',
customTerms: [
{ term: 'Northwind Holdings', type: 'ORG' },
{ term: 'Project Heron' },
],
allowTerms: ['Brightwater Legal LLP'], // our own firm
});
// 'Brightwater Legal LLP acts for Northwind Holdings on Project Heron.'
// becomes
// 'Brightwater Legal LLP acts for [ORG_1] on [CUSTOM_1].'Credits and metering
A protect() call that replaces at least one item costs credits. A call that finds nothing is free.
- Up to 8,000 input tokens: 1 credit (USD 0.05).
- Each further 8,000 input tokens, or part of them: 1 more credit.
- Tokens are estimated on your machine at about 4 characters each, so 1 credit covers about 32,000 characters.
To count credits the SDK makes one request to Tightlip: POST https://tightlipai.com/api/v1/meter with your key in the Authorization: Bearer header and a body such as { "credits": 1, "source": "sdk" }. That body is the whole request. Your text, the placeholders, the detections and the vault are never in it.
If your balance is empty, Tightlip answers with HTTP 402 and the SDK throws TightlipQuotaError. The prompt is not sent to the model. Tightlip fails closed: when protection cannot run, nothing goes out.
TightlipQuotaError: the balance is empty.TightlipAuthError: the key is missing or was rejected.TightlipMeterError: the meter could not be reached. SetfailOpen: trueto send anyway in this one case. An empty balance always blocks.ProtectError: detection itself failed.
import { Tightlip, TightlipQuotaError } from 'tightlip';
try {
const safe = await s.protect(text);
// ...call your model with safe.text
} catch (err) {
if (err instanceof TightlipQuotaError) {
// Out of credits. Nothing was sent. Top up at https://tightlipai.com/app/billing
}
throw err;
}Command line
The package also installs a tightlip command. Install the package first (see SDK). Until tightlip is published to npm, npx only finds it in a project where you installed it from our link.
Check a file
check prints what Tightlip would protect in a file. It runs entirely on your machine, needs no key and uses no credits.
npx tightlip check contract.txtRun the local proxy
proxy starts a local server that speaks the OpenAI and Anthropic APIs. Point a tool or an SDK at it and every request is protected on the way out and restored on the way back. It meters credits the same way as the SDK, with source set to proxy.
# macOS and Linux
export TIGHTLIP_API_KEY=tl_live_...
npx tightlip proxy --port 8787
# Windows PowerShell
$env:TIGHTLIP_API_KEY = "tl_live_..."
npx tightlip proxy --port 8787Then change the base URL in your client:
// OpenAI
const openai = new OpenAI({ baseURL: 'http://localhost:8787/v1' });
// Anthropic
const anthropic = new Anthropic({ baseURL: 'http://localhost:8787' });The official OpenAI and Anthropic SDKs also read the base URL from an environment variable:
OPENAI_BASE_URL=http://localhost:8787/v1
ANTHROPIC_BASE_URL=http://localhost:8787You still use your own OpenAI or Anthropic account, which pays for the model as before. Tightlip credits pay for the protection.
Browser extension
The Chrome extension protects what you type into these AI sites: chatgpt.com, chat.openai.com, claude.ai, gemini.google.com, copilot.microsoft.com and perplexity.ai. You keep using your own ChatGPT, Claude or Gemini account. Tightlip only changes the text before it leaves the page.
Install
Until the Chrome Web Store listing is live, install it from our zip file:
- Download tightlip-extension.zip.
- Unzip it into a folder you will keep. Chrome loads the extension from that folder, so if you delete the folder the extension stops working.
- Open
chrome://extensionsin the address bar. - Turn on Developer mode, top right.
- Click Load unpacked and choose the unzipped folder (the one with manifest.json inside).
- Pin Tightlip from the puzzle-piece menu. Open it and paste your API key if you have one.
What happens when you send
When you press Enter or click send, Tightlip checks the message box first. If it finds nothing sensitive, the message goes as usual. If it finds something, it swaps those items for placeholders, sends the new version and shows a note such as “Tightlip protected 6 items” with a count by type. Click “See what the AI saw” in that note to read the exact text that was sent. The AI site only receives the placeholder version, and that is also what its chat history keeps.
If Tightlip cannot check a message or cannot update the message box, it sends nothing and leaves your text as it was.
Answers with real values
As the answer appears, Tightlip swaps placeholders back to the real values on your screen. Restored values have a dotted underline. Hover over one and it says “Restored on your device by Tightlip”. When you copy part of an answer, or use the site’s own Copy button, the copy has the real values in it.
The restore data lives in the browser’s memory and is cleared when you close the browser. After that, or on another computer, old chats show placeholders. To keep restore data longer, turn on “Remember restore data on this device for 30 days” in the popup. It is off by default and never leaves your computer. Turning it off deletes what was kept.
On Perplexity, the as-you-type search suggestions are switched off while Tightlip protects the site, because they send your draft before you press send. Pausing Tightlip on Perplexity turns them back on.
Settings
Click the Tightlip icon to open the popup. From there you can:
- turn protection on or off for each site
- pick a profile
- add terms to Always protect (kept on this device)
- paste your API key and see your credits
- see how many items were protected this week
- keep or clear restore data
Credits
A message with nothing sensitive in it is free. A protected message costs 1 credit, plus 1 more for each further 8,000 tokens (about 32,000 characters) in very long messages. You can try it without an account: your first 25 protected messages need no key. After that, add a key from /app/keys. New accounts get USD 20 of free credits.
The extension talks to Tightlip only to spend credits and check your balance. The spend request carries your key, a credit count and the word “extension”. The balance check carries only your key. With no key saved, it makes neither request.
When your credits run out, Tightlip stops the send, tells you why and sends nothing. You can top up, or pause Tightlip on that site. While it is paused, messages there go to the AI exactly as you typed them.
Metering API
The SDK, the proxy and the extension all use this one endpoint to spend credits. You only need it if you build your own integration. It takes a number and a label. It never takes text.
POST https://tightlipai.com/api/v1/meterHeaders
| Header | Value |
|---|---|
Authorization | Bearer tl_live_... |
Content-Type | application/json |
Body
| Field | Type | Rules |
|---|---|---|
credits | integer | From 1 to 100. |
source | string | One of sdk, extension or proxy. |
Responses
| Status | Meaning |
|---|---|
| 200 | Credits spent. The body is { "ok": true, "remaining": 1234 }, where remaining is the balance left. |
| 402 | Not enough credits, so nothing was spent. The body is { "ok": false, "remaining": 0 }. Do not send the prompt. |
| 401 | The key is missing, malformed or has been rotated. |
| 400 | The body does not match the rules above. |
| 503 | Metering is not configured on the server. Try again later. |
Example
This spends 1 real credit.
curl -X POST https://tightlipai.com/api/v1/meter \
-H "Authorization: Bearer $TIGHTLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"credits": 1, "source": "sdk"}'API keys
Keys look like tl_live_ followed by a long string. Get yours, or rotate it, at /app/keys. The key is shown once and Tightlip does not keep a copy, so store it somewhere safe. Rotating gives you a new key and stops the old one working straight away. If you paste a key into a prompt by mistake, Tightlip treats it as a secret and swaps it for a placeholder.
Web workspace
The workspace at /app is a chat with the AI model included in the credit. You paste text or drop a .txt, .pdf or .docx file. Files are read in your browser. You see a live preview of what the AI will see before you send.
Only the protected messages leave your browser. They pass through Tightlip’s server on the way to Anthropic, which runs the model, and Tightlip does not store or log them. The original text, the vault and your custom terms stay in your browser. The security page shows the full data flow for every part of Tightlip. Please do not put patient health information into the workspace.
Help
Email hello@tightlipai.com. Tell us what you were using (SDK version, extension or workspace) and what happened. Please do not send us the sensitive text itself. A made-up example that shows the same problem works better.