Privacy and data flow
Exactly what leaves your machine, and where it goes.
skiffr is self-hosted. Your profile, answers and tokens are in your own Postgres, behind your own api. The extension is built to talk to no server except the api you configured and carries no tracker. The sites (landing page, docs, dashboard) can load your own Umami for page views, and the api can send error reports, logs and metrics to your own GlitchTip and OpenObserve, all off unless you set them and all from an allowlist, see observability.
The extension detects forms and fills them, and calls your api for that. After a fill it also sends the edits you make to your api, see below.
Data flow
web page -> extension -> your api -> your Postgres
|
+-> language model provider (Groq by default)
+-> embeddings provider (OpenAI, optional)What goes where
| To | What | When |
|---|---|---|
| your api | field labels, names, placeholders, types and option texts of the form, your token. After a fill, also the filled and final values of the fields you edit (the extension cuts values at 301 characters, the api accepts up to 2000) | when you fill, and when you submit or leave the page |
| language model provider | the same field descriptors, your profile key names, and the values of an allowlist of profile keys | when a field needs mapping or an answer is drafted |
| embeddings provider | the normalized question label | only with OPENAI_API_KEY, and only for learning |
| your Umami, GlitchTip, OpenObserve | page views without query or referrer, error class and stack, request method, route and status, model id and token counts. Never form values, labels, resume or job text, tokens or the message of an error. Prompts and answers only with AI_TRACE_CAPTURE_CONTENT=true | only when you set the matching variable |
| nobody | form submissions | never |
What the model sees of your profile
Key names are always sent, so the model can pick one. Values are sent only for the allowlist in packages/ai/src/shared-keys.ts:
- first and last name, city, country, work authorization and sponsorship, all links
- free-form fields labelled preferred name, headline, summary, experience, years of experience, skills, languages, notice period, availability, linkedin, github, portfolio or website
Email, phone, address, pay, ids and dates of birth are not on the list and stay out of every prompt. A value over 60 characters is not previewed in field mapping. Drafting an answer also sends the question, the job description and up to 5 of your earlier answers.
Password, hidden and file inputs are removed in code before a request is built. Text from pages and job posts is put in the prompt as escaped data blocks, and the model is told to treat it as data. The model's reply is checked in code: a profile key it invents becomes a skip.
The provider keeps what its own terms say. Leave the keys unset to send nothing: mapping and drafting then answer "No AI provider key is configured".
What the extension does on a page
- the content script is injected on all pages, because an application form can be on any careers site. It looks for an application form and shows the toast. Nothing goes to your api until you say yes or press the shortcut
- the fill engine never submits a form and never clicks submit, next or apply buttons
- it never touches password or hidden inputs
- it sets a file only on a resume or CV upload slot, and only when the caller hands it a file
- it opens custom dropdowns (including
button[role=combobox]widgets) and clicks their own options, and clicks the checkboxes and radios it fills, nothing else - fields a person cannot see or reach (honeypots) are skipped
What the api stores about learning
For every filled field the api keeps one event: the raw field label, the filled value and the final value, up to 2000 characters each, plus the site, the control type and an outcome (accepted, corrected, cleared). Answers you correct become learned answers, with an embedding of the question label only.
- fields with sensitive labels or autocomplete tokens, and values that look like a card number (Luhn), an IBAN or a US social security number, are never stored or filled
- events older than 90 days are deleted, but only inside the calls that record events or list suggestions, and 500 rows at a time. On a quiet account old events can stay until the next call
- events, answers and tokens are removed with the user row (
on delete cascade). The dashboard has no self-service account deletion yet, so today that is an admin or database operation
Gmail
Only when you connect it, see Gmail job emails: skiffr reads unread inbox mail from the senders you watch, stores job title, company and link (not the email), and marks processed mail read, out of the inbox and labelled. No email text goes to a model provider.
CV tailoring
Only when you connect it, see CV tailoring: the text of your base resume, the shared parts of your profile and the job description you paste go to the AI provider to propose edits. Your Reactive Resume key is stored encrypted and never leaves the server. The base resume is only read; changes go to a copy.
Job sources
Only for addresses you add, see Job sources: the api fetches those public pages and feeds with a request that names skiffr and carries no cookie or personal detail, and stores the title, company and link of each posting (not the page). The job links are never opened. No page text goes to a model provider.
Tokens
The extension authenticates with a personal access token that starts with skp_. The server keeps its SHA-256 hash and a short display tail, never the token. In the extension the background worker and the options page read it. Content scripts on web pages are kept from it by storage.local.setAccessLevel("TRUSTED_CONTEXTS"), which needs Chrome 142 or newer (the manifest's minimum_chrome_version). Calls to the api are made by the background worker. A token can only call a fixed list of procedures (TOKEN_PROCEDURES in packages/orpc/src/procedures.ts): profile, answers, jobs, applications and the two ai calls. Account, auth and token management need a signed-in session. Set EXTENSION_ORIGINS to pin the api to your own extension id.
Reporting a problem
Report vulnerabilities privately through GitHub, see SECURITY.md in the repo root.