Anonymize a CSV column by column
This page rewrites the sensitive columns of a CSV and leaves the rest alone. Seven strategies per column: keep, redact, a real salted SHA-256, masking, fake values, generalization into buckets, or a shuffle. The same value always gets the same token inside one run, so your joins survive. Nothing is uploaded. The file and the salt both stay in this browser tab.
Not sure what is in the file yet? Scan it first
The salt is the whole argument
Start with the failure this is built to avoid. Somebody hashes a column of email addresses with plain SHA-256, ships the file, and calls it anonymous. It is not. Email addresses live in a small and highly guessable space. Anybody who suspects who your customers are takes their own list, hashes it with the same algorithm, and joins the two columns. No cryptography is broken. There is no brute force worth the name. It is a lookup table, and building one for a few million addresses costs a laptop about a minute. The same argument applies to phone numbers, national identifiers, postal codes and any other column drawn from a set an outsider can enumerate.
A salt closes that door. Before hashing, 128 bits from crypto.getRandomValues are prefixed to the value, so the digest depends on a secret the attacker does not have. The salt here is generated once per page load. It is never written into the output file, never sent over the network, and never saved to storage. When you close the tab it is gone. That means the mapping from token back to value cannot be reproduced afterwards by anybody, including us, because we never held it in the first place.
The algorithm underneath is SHA-256 through WebCrypto, the browser's own implementation. If a browser does not expose crypto.subtle, or has no cryptographic random source, the tool refuses to hash and says so. It does not quietly fall back to something weaker that produces a token which looks identical and is not. A wrong answer that looks right is the worst outcome available on a privacy page.
Tokens are truncated to sixteen hex characters by default, which is sixty four bits. That will not collide on any file a browser can open, and it keeps the output readable in a spreadsheet, which a sixty four character digest does not. You can raise it as far as the full digest if you would rather have the headroom.
The trade-off you have to know about
Because the salt is new on every page load, tokens are stable in one direction only. Say that plainly: the same input produces the same token inside a single run, and a different token in the next run.
Both halves matter. The first half is why a join survives. A customer appearing on four hundred order rows gets one token on all four hundred, so counts, group-bys and funnel analysis on the anonymized file give the same answers as on the original. Anonymize your orders file and your refunds file without reloading in between and the two still join on the hashed customer key.
The second half is why a leaked export cannot be linked to an older one. It also means that if you anonymize a file today and the same file again next week, the two are not comparable, and there is no way to recover the earlier tokens. Plan around it. When two exports have to line up, produce them in the same session before you reload. When you want two exports that nobody can stitch together, reload between them on purpose. The tool prints a warning saying which situation you are in whenever any column is hashed, because it is not the kind of thing that should live only in documentation.
One more consequence worth naming. Consistent tokens preserve frequency. If one customer accounts for a third of the rows, one token accounts for a third of the rows, and in a small population that shape can identify somebody without any value ever being revealed. That is not a flaw in the hashing, it is what deterministic pseudonymization is. If frequency itself is sensitive, use shuffle or redact on that column instead.
Worked example: a repeated customer, and a join that survives
Here is orders.csv. Note that rows one and three are the same person:
order_id,customer_email,full_name,mobile_phone,zip,order_total,signup_date
A-9001,rita.okafor@northwind.example,Rita Okafor,+44 7700 900431,SW1A 1AA,148,2024-03-11
A-9002,dan.mercer@northwind.example,Dan Mercer,+1 415 555 0132,94043,2310,2024-03-12
A-9003,rita.okafor@northwind.example,Rita Okafor,+44 7700 900431,SW1A 1AA,37,2024-04-02
Run it with the automatic plan and the output is this:
order_id,customer_email,full_name,mobile_phone,zip,order_total,signup_date
74ddd07aa4a179b7,134bccd0eaaa3c30,Nadia Ramirez,+*************1,SW1A ***,148,2024-03
a8587b3c4baf7b5d,6ea8dc30849c37a3,Karan Khan,+*************2,940**,2310,2024-03
e21c420b048e0172,134bccd0eaaa3c30,Nadia Ramirez,+*************1,SW1A ***,37,2024-04
Look at the email column. Rows one and three carry 134bccd0eaaa3c30, the identical token, because the same input hashed with the same session salt gives the same digest and distinct values are hashed once and reused. That is the join key doing its job: a group-by on this column still tells you Rita placed two orders totaling 185. The name column agrees, because fake values are drawn deterministically from the value too, so one person keeps one invented name throughout the file. Meanwhile order_id gets three different tokens, correctly, because those were three different orders.
And the summary that comes with it:
3 rows, 7 columns
6 columns changed, 1 kept
order_id → hash (from the column name), 3 cells changed
customer_email → hash (from the column name), 3 cells changed
full_name → fake (from the column name), 3 cells changed
mobile_phone → mask (from the column name), 3 cells changed
zip → generalize (from the column name), 3 cells changed
order_total → keep (from the column name), 0 cells changed
signup_date → generalize (from the column name), 3 cells changed
Warning: 1 column was left exactly as it is: order_total.
Check none of them identifies anybody on its own or in
combination with the others.
The per-column change count is there so you can catch a strategy that did nothing. A masked column reporting zero changed cells means it was empty, and an empty column that you believed was being masked is exactly the kind of thing worth knowing before the file leaves your machine. The (from the column name) tag marks a guess rather than an instruction from you, which is your cue to check it.
Notice also what the postal codes did. SW1A 1AA became SW1A ***, keeping the UK outward code, and 94043 became 940**, keeping the US sectional center. Those are the standard de-identification cuts and they keep the column useful for regional analysis. The dates lost their day and kept their month. Any numeric column that you route to generalize goes into a bucket one order of magnitude below the value and never narrower than ten, so 148 becomes 100-199 and 37 becomes 30-39. A bucket of width one is not a generalization, it is the original value with a dash in it.
Seven strategies, and when each one is the right answer
- keep. Nothing happens, and the column is named in a warning so it cannot slip past you. Right for measures, statuses and dimensions that carry no identity.
- redact. Every non-blank cell becomes the literal text
[redacted]. Right for free text and street addresses, where any structure you preserve is structure somebody can read. - hash. Salted SHA-256, truncated. Right for join keys: customer ids, email addresses, device identifiers. It is the only strategy that keeps a relationship intact without keeping a value.
- mask. Keeps one character at each end, or as many as you ask for, and stars the middle. On an email it keeps the first character of the local part and the top level domain, and it does not leave the domain in the clear. Right for phone numbers and card numbers where support staff still need to confirm a value against a customer.
- fake. A plausible invented value chosen deterministically from the original, so one person keeps one alias. Right for anything a human has to read: a demo, a screenshot, a bug report, a training data set.
- generalize. A number into a bucket, a date into a month, a postal code into an area. Right when you want the column to stay analytically useful while dropping the precision that singles people out. This is the strategy that actually moves you toward anonymity rather than pseudonymity.
- shuffle. The column keeps every one of its values and loses its link to the rest of the row. Right for a salary column when you want the distribution for a chart and nobody's actual salary. Do not use it on a join key.
Masking deserves the extra sentence it got. The tempting shortcut is to keep the email domain, which looks harmless and preserves a useful grouping. It is not harmless. A corporate domain names the employer, and employer plus job title plus city is often enough to identify one person on a small file. So the domain goes behind stars and only the top level domain survives, which is enough to tell an address apart from a phone number and not enough to name a company.
What the free anonymizers usually do instead
This matters because the words on the button are the same and the thing underneath is not. The common free CSV anonymizer offers hash, mask and fake, exactly as this one does. Here is what those three mean over there.
Its hash is djb2: a thirty two bit non-cryptographic checksum from 1991, designed for hash tables, emitted as tok_<base36>. It is not a cryptographic hash and it was never meant to be one. Over a domain the size of an email list it is brute-forceable in seconds on a laptop, because thirty two bits is about four billion possibilities and there is no salt to widen the search. It also collides: at a few tens of thousands of distinct values you should expect two different customers to receive the same token, which quietly merges two people into one row in every downstream count.
Its mask leaves the email domain in the clear, so j***@acme-widgets.com is the output. The name is gone and the employer is not, which on an HR export or a customer list of any specificity is most of the way back to a person.
Its fake pools hold twenty first names, sixteen surnames and ten cities. On a five thousand row file every first name appears around two hundred and fifty times, which stops being an anonymized data set and starts being a data set about twenty people. The pools here are 264 first names, 270 surnames and 274 cities, which is more than seventy one thousand name combinations, and the phone numbers come from reserved fiction ranges so a generated number can never ring anybody.
None of this makes that tool useless for a screenshot. It makes the word hash on its interface wrong, and a person reading that word makes decisions about real people's data based on it. That is the whole reason this page spends four paragraphs on a salt.
This is not legal advice, and it is not compliance
Running a file through this page does not make it GDPR compliant, HIPAA compliant, or compliant with anything else. Nothing here is legal advice. What the tool does is reduce exposure in one file. What compliance depends on is why you hold the data, what lawful basis you claim, how long you keep it, who can reach it, what your processor contracts say, and what the rest of your systems do with the same records. None of that is visible from a browser tab.
Two distinctions are worth carrying with you. First, pseudonymized data is still personal data. Replacing a name with a token is a genuine safeguard and it changes what controls you need, and it does not take the file outside the regulation. Second, true anonymization is a much higher bar. Data counts as anonymous only when no one, using any means reasonably likely to be used, can get back to a person, and that test has to account for data somebody already holds. Re-identification by combination is always on the table: a hashed customer key next to a real postal code, a real birth month and a real job title still singles people out. Clearing the anonymity bar normally means suppressing or generalizing until the file cannot pick anybody out, and that costs analytical value. Anybody promising one-click anonymous data is selling something.
What this page is genuinely good at: producing a shareable version of a file for a vendor, a demo or a bug report, keeping the joins that make the file worth sharing, and telling you in writing which columns it did not touch. Do that, then take the result to whoever owns privacy where you work.
Practical notes
- The plan comes from headers, then from you. The automatic plan reads each header with the same role detector the PII scanner uses. Run once, copy the printed plan, edit it, paste it into the custom box.
- Typos in a plan are reported, never ignored. An unknown column or an unknown strategy comes back as a warning naming the dropped rule, because a silently ignored rule leaves a column in the clear.
- Blank cells stay blank. No strategy invents a value where there was none, so missingness is preserved and a hashed empty string never appears.
- Distinct values are hashed once. A column of fifty thousand rows with three hundred distinct values does three hundred digests, which is both faster and the reason tokens repeat.
- The fake seed is separate from the salt. Fake values are reproducible from a seed you control, so a demo file can be regenerated exactly. Hashes are not, because the salt is random by design. Do not confuse the two.
- Check the output before you send it. Open the result in the viewer, or run it back through the scanner. A second pass costs thirty seconds and catches the column you forgot.
Frequently Asked Questions
What are the seven strategies?
Keep leaves the column untouched and says so in the summary. Redact writes the literal text [redacted] into every non-blank cell. Hash computes SHA-256 over a per-session salt plus the value and keeps the first sixteen hex characters. Mask keeps a character at each end and stars the middle. Fake swaps the value for a plausible invented one drawn from a pool. Generalize widens a value into a bucket: a number into a range, a date into a month, a postal code into an area. Shuffle reorders the column in place so the distribution survives and the link to the row does not.
Why does the hash need a salt at all?
Because an unsalted SHA-256 of an email address is reversible in practice, not in theory. Email addresses come from a small, guessable space. An attacker who suspects your customers are on a particular mailing list hashes their own copy of that list and joins the two columns. Nothing is broken and nothing is brute-forced; it is a lookup. The salt is 128 bits from crypto.getRandomValues, generated once per page load, prefixed to every value before hashing. It is never written into the output file, never sent anywhere, and never stored, so once you close the tab nobody can rebuild the mapping. That includes us, because we never had it.
Will my join still work after anonymizing?
Inside one run, yes. The same input value always produces the same token, so a customer who appears on forty rows gets one token on all forty, and two files you anonymize without reloading the page will join to each other on a hashed column. Across runs, no. Reload the page and a new salt is generated, so every token changes. If you need two exports to line up, produce both in the same session. If you want two exports that cannot be linked, reload between them. That is a deliberate choice and the tool tells you which side of it you are on in the warnings.
Does this make my file GDPR or HIPAA compliant?
No, and nothing on this page is legal advice. Replacing a name with a token is pseudonymization, and pseudonymized data is still personal data under GDPR. Recital 26 is explicit about it. True anonymization is a much higher bar: the data has to be beyond re-identification by any means reasonably likely to be used, which has to account for combining your file with other data somebody already holds. A file with a hashed customer id, a masked phone and a real postal code plus a real birth month is not anonymous. Use this tool to reduce exposure and then talk to whoever owns privacy where you work.
How does the tool decide what to do with each column?
From the header, using the same role reader the PII scanner uses. A header that reads as an email gets hash, because emails are usually the join key. A name gets fake, because a report full of tokens is unreadable and a report with an invented name in it can be shown to somebody. A phone or a financial value gets mask, an address gets redact, a city gets fake, a postal code or a date gets generalize, and anything unrecognized gets keep. Every decision is printed in the summary with the phrase from the column name attached, so you can see which choices were guessed. Switch to a custom plan to override any of them.
What does the custom plan syntax look like?
A comma separated list of column=strategy pairs, for example full_name=fake, work_email=hash, department=keep. Semicolons and newlines work as separators too, and column matching ignores case. A typo is never silently ignored: an unknown column name or an unknown strategy comes back as a warning naming the rule that was dropped, because a plan with a misspelled column in it would otherwise leave that column in the clear while you believed it was handled. The auto plan is also printed in the copyable syntax, so the fastest way to write a custom plan is to run once, copy the printed plan, and edit the two entries you disagree with.
Are the fake names and phone numbers safe to publish?
The phone numbers are, by construction. Every generated number comes from a reserved fiction range: +1 555-01xx in North America, +44 7700 900xxx from Ofcom's drama block, and the German, French and Australian equivalents. None of them can ring a real person. The names come from pools of 264 first names and 270 surnames, which gives over 71,000 combinations, and the cities from a pool of 274. Coincidences are still possible on a large file, so treat a fake name as a placeholder rather than a guarantee that nobody is called that.
Is the file uploaded anywhere?
No. The file is read, hashed and rewritten by JavaScript in your tab. There is no upload endpoint on this page, no account, and no server that ever sees a row. The salt is generated in your browser by the platform random number generator and dies with the tab. If you are about to feed a tool a file full of customer records, opening the network panel first is a reasonable thing to do, and you will see it stay quiet.
Related
Share the file, not the people in it
Free, no account, no upload. A real salted SHA-256, seven strategies, consistent tokens, and a written list of every column left untouched.
Back to the anonymizer