Skip to main content

KP2BW - KeePass to Bitwarden Converter

PyPI

Migrates KeePass databases to Bitwarden via the bw CLI,
with advantages over the built-in Bitwarden importer:

  • Encrypted in-memory transfer Data never hits disk unencrypted (except attachments, which are cleaned up after upload).
  • KeePass REF resolution Username/password references are resolved.
    Matching credentials merge URLs and a compatible TOTP into one entry. Conflicting TOTP or other content that cannot be merged stays in a separate entry to prevent data loss.
  • Passkey migration KeePassXC FIDO2/passkey credentials (KPEX_PASSKEY_*) are converted to Bitwarden fido2Credentials.
  • Custom properties & attachments Imported as Bitwarden custom fields or attachments (values >10k chars auto-upload as files).
  • Long notes handling Notes exceeding 10k chars are uploaded as notes.txt attachments.
  • Idempotent re-runs that sync changes Safe to run repeatedly; existing entries are updated in place when their KeePass content changed (notes, credentials, URIs, fields) and never duplicated. Each item is stamped with its KeePass UUID in a KP2BW_ID field, so distinct entries that share a title stay separate and a re-run is matched by identity rather than title. Edits you make in Bitwarden are protected: a re-run preserves them instead of reverting to KeePass (a KP2BW_SYNC content stamp tells your edit apart from kp2bw's own writes); --force-update overrides.
    Disable updates with --no-update.
  • Nested folders KeePass folder hierarchy is recreated in Bitwarden.
  • Recycle Bin filtering Deleted entries are automatically excluded.
  • Expiry awareness Expired entries are marked [EXPIRED] in notes; optionally skip them entirely with --skip-expired.
  • Metadata preservation KeePass tags and expiry date are folded into a single KP2BW_META custom field (YAML), omitted when an entry has neither. Created/modified timestamps are not migrated — Bitwarden manages its own creation/revision dates.
  • Tag filtering Import only entries matching specific tags.
  • Organization & collection support Upload into a Bitwarden organization with automatic or manual collection assignment.
  • Full UTF-8 & cross-platform Works on Windows, macOS, and Linux.

Fork of jampe/kp2bw.

Usage/Installation

# run directly
uvx kp2bw@latest

# install globally with:
uv tool install kp2bw

# update with:
uv tool update kp2bw

kp2bw --version
kp2bw passwords.kdbx

or from a GitHub URL (pull requests display a "🧪 Test this PR" section with convenience commands to run the branch without installing):

# install with:
uv tool install git+https://github.com/kjanat/kp2bw
kp2bw passwords.kdbx

# run directly without installing:
uvx --from git+https://github.com/kjanat/kp2bw kp2bw passwords.kdbx

Prerequisites

Install the Bitwarden CLI and log in once before using kp2bw:

# optional: point to a self-hosted instance
bw config server https://your-domain.com/

# log in (only needed once; kp2bw uses `bw unlock` afterwards)
bw login <user>

# ensure you get to a point where you are properly logged in
bw status | jq . # jq optional but makes it easier to read the JSON output
// Example return value:
{
	"serverUrl": "https://bitwarden.example.com",
	"lastSync": "2020-06-16T06:33:51.419Z",
	"userEmail": "user@example.com",
	"userId": "00000000-0000-0000-0000-000000000000",
	"status": "locked",
}

Usage

kp2bw [-h] [-V] [-k PASSWORD] [-K FILE] [-b PASSWORD] [-o ID]
       [-t TAG [TAG ...]] [-c ID] [--path-to-name | --no-path-to-name]
       [--path-to-name-skip N] [--skip-expired | --no-skip-expired]
       [--include-recycle-bin | --no-include-recycle-bin]
       [--metadata | --no-metadata] [--update | --no-update] [--force-update]
       [--include-oversize-secrets] [--folder | --no-folder] [--uri-match MODE]
       [--totp-pps | --no-totp-pps]
       [--interpret-uri-syntax | --no-interpret-uri-syntax]
       [--migrate-uris] [--report-uris SOURCE] [--strip-ids] [-y] [-v] [-d]
       [FILE]
Flag Description Env var
keepass_file Path to your KeePass 2.x database KP2BW_KEEPASS_FILE
-k, --keepass-password KeePass password (prompted if omitted) KP2BW_KEEPASS_PASSWORD
-K, --keepass-keyfile KeePass key file KP2BW_KEEPASS_KEYFILE
-b, --bitwarden-password Bitwarden password (prompted if omitted) KP2BW_BITWARDEN_PASSWORD
-o, --bitwarden-org Bitwarden Organization ID KP2BW_BITWARDEN_ORG
-c, --bitwarden-collection Collection ID, auto for top-level folder names, or nested for full folder paths KP2BW_BITWARDEN_COLLECTION
-t, --import-tags Only import entries with these tags KP2BW_IMPORT_TAGS (comma-separated)
--folder / --no-folder Create personal Bitwarden folders from KeePass groups (default: on, but off when --bitwarden-org is set) KP2BW_CREATE_FOLDERS
--path-to-name / --no-path-to-name Prepend folder path to entry names (default: off) KP2BW_PATH_TO_NAME
--path-to-name-skip Skip first N folders in path prefix (default: 1) KP2BW_PATH_TO_NAME_SKIP
--skip-expired Skip entries that have expired in KeePass KP2BW_SKIP_EXPIRED
--include-recycle-bin Include Recycle Bin entries (excluded by default) KP2BW_INCLUDE_RECYCLE_BIN
--metadata / --no-metadata Toggle KeePass tags/expiry as a KP2BW_META field (default: on) KP2BW_MIGRATE_METADATA
--update / --no-update Update existing entries changed in KeePass (default: on) KP2BW_UPDATE
--force-update Overwrite items even if edited in Bitwarden since the last run (default: protect such edits) KP2BW_FORCE_UPDATE
--include-oversize-secrets Offload over-limit secret fields1 to a .txt attachment instead of dropping them (default: off) KP2BW_INCLUDE_OVERSIZE_SECRETS
--uri-match MODE Match mode for plain URLs: default(default, account default)/domain/host/startswith/exact/regex/never KP2BW_URI_MATCH
--interpret-uri-syntax Honor KeePassXC quote/wildcard URL syntax on additional URLs (default: on; --no-… for literal) KP2BW_INTERPRET_URI_SYNTAX
--totp-pps Read TOTP from a Pleasant Password Server export (TOTPSecret/TOTPDigits/TOTPPeriod) instead of the KeePass TimeOtp-* fields; the unread scheme's secret stays a hidden field (default: off) KP2BW_TOTP_PPS
--migrate-uris Upgrade existing items: re-fold legacy KP2A_URL*/AndroidApp fields into login URIs, then exit (no KeePass) KP2BW_MIGRATE_URIS
--report-uris SOURCE Print a read-only URI collision report (keepass or bitwarden) and exit; lists registrable domains with multiple hosts KP2BW_REPORT_URIS
--strip-ids Finalize: remove the KP2BW_ID/KP2BW_SYNC stamps from migrated items, then exit (no migration; no KeePass db) KP2BW_STRIP_IDS
-y, --yes Skip the Bitwarden CLI setup confirmation prompt KP2BW_YES
-v, --verbose Verbose output KP2BW_VERBOSE
-d, --debug Debug output — includes third-party library logs KP2BW_DEBUG
-V, --version Print the installed kp2bw version and exit -
--doctor Print environment diagnostics (kp2bw/bw/server versions, install method, .env detection) and exit; non-zero exit when the bw CLI is unusable -
--redact With --doctor: mask the server URL and home-anchored paths so the report is safe to paste in a public issue -

Configuration precedence is always: CLI flag > environment variable > built-in default.

Organization collections

Bitwarden folders are personal-vault metadata. When importing into an organization, use --bitwarden-collection auto to create/use one collection per top-level KeePass folder, or --bitwarden-collection nested to create/use collections from full KeePass paths such as Work/Servers.

Because folders only duplicate the collection tree in your personal vault, --bitwarden-org defaults to not creating personal folders — items are filed only into collections. Pass --folder (or KP2BW_CREATE_FOLDERS=1) to restore the personal folder tree alongside the collections.

URL handling

A KeePass(XC) entry's additional URLs (KP2A_URL/KP2A_URL_n, plus the plainer URL/URL_n convention) and Android packages (AndroidApp/AndroidApp_n, incl. the no-underscore AndroidApp1 variant) are migrated as real Bitwarden login URIs — not inert custom fields — so one login autofills across every site and app it covered in KeePass. Free-text URL labels (API Url, Alt. URL, Website, …) are left as custom fields, since folding those would wrongly autofill API endpoints and other metadata. Each URI gets a per-URI match mode reproducing KeePassXC's behaviour: a plain URL → account default (match unset, what Bitwarden itself writes; pass --uri-match domain to force base-domain and replicate KeePassXC's host-based matching), a double-quoted URL → exact, and a * wildcard → starts-with (trailing path) or regex. Non-web schemes (keepassxc://, cmd://, kdbx://, file://) and unresolved {REF:…} URLs are dropped. --no-interpret-uri-syntax disables the quote/wildcard interpretation and imports every URL as a plain string.

Already imported before this existed? Two ways to upgrade: re-run a normal migration (the change is detected and the items are updated in place), or — if you don't want to re-import — run kp2bw --migrate-uris, a Bitwarden-only one-shot pass that re-folds the legacy fields into URIs on every existing item. Both honour --uri-match/--interpret-uri-syntax and -o/-c. The one-shot pass preserves stamped items edited in Bitwarden and establishes or refreshes KP2BW_SYNC on each migrated item.

Too many subdomains autofilling together? Under base-domain matching, every login under *.example.com surfaces on any example.com subdomain. kp2bw --report-uris keepass (or bitwarden) prints a read-only collision report — registrable domains with multiple hosts — so you can see which entries pile up and switch those to Host match (or flip your Bitwarden account's default URI match detection to Host). It changes nothing; it just lists.

Because kp2bw is KeePass-authoritative, a re-run would normally overwrite any difference on an existing item — quietly undoing a title, note or URI you fixed in Bitwarden. It doesn't: every item kp2bw writes carries a KP2BW_SYNC content signature, and a re-run that finds an item's current content no longer matching that stamp knows you edited it (kp2bw's own writes restamp, so they never self-trip) and preserves your edit, listing it as protected in the summary. Pass --force-update (env KP2BW_FORCE_UPDATE) to make KeePass win regardless. Unchanged re-runs stay idempotent, and items imported before this existed are adopted normally on their first re-run.

Every migrated item carries a plain-text KP2BW_ID custom field — the KeePass UUID kp2bw uses to match entries on re-runs so nothing duplicates. Once you're satisfied the migration is complete and you're ready to fully adopt Bitwarden, run

kp2bw --strip-ids            # personal vault; add -o/-c to scope to an org/collection

to remove that stamp from every migrated item and exit. No KeePass database is read.

[!WARNING] This is irreversible and makes future migration re-runs unreliable. Without the stamp, a re-run can only match by folder + name — the exact collision the stamp exists to prevent — so entries that share a folder and title may be duplicated or mismatched on a later migration. --strip-ids confirms before changing anything (skip with -y). Only run it once you're truly done migrating.

.env file

kp2bw automatically loads a .env file, searched upward from the current working directory, so you can keep your settings (including the database path via KP2BW_KEEPASS_FILE) out of your shell history.

Copy .env.example to .env and uncomment what you need:

KP2BW_KEEPASS_FILE=passwords.kdbx
KP2BW_BITWARDEN_ORG=00000000-0000-0000-0000-000000000000
KP2BW_SKIP_EXPIRED=1

Then just run kp2bw with no arguments. A real shell environment variable still overrides any value in .env (precedence is unchanged: CLI flag > env var > default), and .env is gitignored so secrets stay local.

Troubleshooting

Every run writes a full DEBUG log to a per-user file even when the console stays quiet, so a failed run leaves a complete record to share. On Windows that is %LOCALAPPDATA%\kp2bw\logs; override the file with KP2BW_LOG_FILE or the directory with KP2BW_LOG_DIR. bw serve errors include the server's actual message, and a slow or dropped request no longer aborts the run — failed entries are counted in the summary and a re-run safely picks up where it left off. Against a slow self-hosted server (e.g. Vaultwarden) where individual writes time out, raise the per-request HTTP timeout with KP2BW_HTTP_TIMEOUT (seconds; default 180, capped at 3600) to let those creates finish in the first place.

See TROUBLESHOOTING.

  1. Bitwarden has a 10k character limit for text fields.

    kp2bw can offload any field exceeding that limit (hidden OTP secrets, passkey attributes, KeePass-protected fields) to a .txt file attachment instead of dropping it, so you don't lose data. This applies to long notes as well as custom fields. ↩

Metadata

Release files for kp2bw 3.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kp2bw 3.8.1
File Size Uploaded
kp2bw-3.8.1.tar.gz 85.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kp2bw 3.8.1
File Interpreter ABI Platform
kp2bw-3.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 176.7 kB

Release files / kp2bw-3.8.1.tar.gz

Download URL kp2bw-3.8.1.tar.gz
Size 85.4 kB
Tags Source
SHA-256 checksum
How to use checksums
21c69173509738ec7b629c9bc8ecdc3e295e305dbd9f6045a05057e8efa79171
BLAKE2b-256 checksum
How to use checksums
2ed647ccdfef3f4a1d3f19caf8efa89c6f6bb0be990c01ac54deffd83c5c22bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / kp2bw-3.8.1-py3-none-any.whl

Download URL kp2bw-3.8.1-py3-none-any.whl
Size 91.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
49919d887f1c0c166043ac454d8f07cb8671c4e61570b899c0edd28efebe5c34
BLAKE2b-256 checksum
How to use checksums
f0cebd55f52b6da17f6be9d8126744569f77748782c22f1504a70fbc16059b4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page