Python scripts (standard library only, Python 3.9+) for cleaning
up password vaults when migrating between Bitwarden and Proton Pass.
The four tools share a small common.py helper module — keep it next to
the scripts.
| Script | What it does |
|---|---|
bw_protonpass_diff.py |
Diffs a Bitwarden export against a Proton Pass export and writes a Bitwarden-format JSON with only what's missing from (or newer than) Proton Pass — import it into Proton Pass without creating duplicates. |
protonpass_bw_diff.py |
The same diff in the other direction: writes a Bitwarden-format JSON with only the Proton Pass items missing from (or newer than) Bitwarden — import it into Bitwarden without creating duplicates. |
bw_dedupe.py |
Removes duplicates from a Bitwarden export and writes a clean Bitwarden-format JSON. |
protonpass_dedupe.py |
Removes duplicates from a Proton Pass export and writes a clean import-ready zip. |
- Bitwarden: Tools → Export vault →
.json(unencrypted) - Proton Pass: Settings → Export → JSON (produces a zip; all scripts
accept the zip directly, or the
data.jsoninside it)
# What's in Bitwarden but not in Proton Pass?
python bw_protonpass_diff.py bitwarden.json "Proton Pass_export_*.zip" -o import_me.json
# -> import import_me.json in Proton Pass: Settings -> Import -> Bitwarden
# What's in Proton Pass but not in Bitwarden?
python protonpass_bw_diff.py "Proton Pass_export_*.zip" bitwarden.json -o import_me.json
# -> import import_me.json in Bitwarden: Tools -> Import data -> Bitwarden (.json)
# Deduplicate a Bitwarden vault (preview first)
python bw_dedupe.py bitwarden.json --dry-run
python bw_dedupe.py bitwarden.json -o bitwarden_clean.json
# -> wipe the Bitwarden vault, then import the clean file
# Deduplicate a Proton Pass vault (preview first)
python protonpass_dedupe.py "Proton Pass_export_*.zip" --dry-run
python protonpass_dedupe.py "Proton Pass_export_*.zip" -o protonpass_clean.zip
# -> wipe the Proton Pass vaults, then Settings -> Import -> Proton PassLogins are grouped by (place, username) where place is:
- the URL's
host:portwithwww.stripped — the port matters so that different self-hosted services on the same machine don't collide - or an Android app package name (Bitwarden
androidapp://URIs, Proton PassallowedApps) - or, when an item has no URL at all, its name — so entries that merely share an email address aren't treated as duplicates
Groups sharing any key are merged (union-find), so an item with several URLs can bridge two groups.
Within a duplicate group:
- identical passwords → only the newest item is kept
- different passwords → all are kept; the older ones get a
[DUPLICATE? different passwords]name prefix so you can resolve them manually after import — no password is ever silently discarded - non-login items (notes, cards, identities) are deduplicated only on exactly identical content
Trashed items are dropped: something you deleted on purpose stays deleted.
bw_protonpass_diff.py additionally refuses to re-import a Bitwarden login whose
only match is in the Proton Pass trash (it lists them so you can double-check).
The diff scripts also skip notes and credit cards that already exist on the other side (notes match on name + note text, cards on name + card number), so re-running a diff after an import doesn't create duplicates. Other non-login items (identities, aliases, ...) can't be compared across formats and are always included for manual review.
protonpass_bw_diff.py converts Proton Pass items to Bitwarden's format: logins keep
their URLs, TOTP, custom fields and Android app links (as androidapp://
URIs); credit cards are mapped to Bitwarden cards (card PINs go into the
notes, which Bitwarden lacks a field for); notes stay notes; anything else
(aliases, identities, ...) becomes a secure note tagged for manual review.
- Exports contain all your passwords in plain text. Work in a private
directory and securely delete every export and output file afterwards
(
shred -uon Linux). - The scripts never print passwords — reports show item names and dates only.
- Always
--dry-runfirst and skim the report. - Import into a new/empty vault and verify before deleting anything.
- Passkeys never survive a JSON export/import — re-register them manually.
- File attachments are not included in JSON exports.