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 into one entry; differing ones create new entries. -
Passkey migration
KeePassXC FIDO2/passkey credentials (KPEX_PASSKEY_*) are converted to Bitwardenfido2Credentials. -
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 asnotes.txtattachments. -
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 aKP2BW_IDfield, 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 (aKP2BW_SYNCcontent stamp tells your edit apart from kp2bw's own writes);--force-updateoverrides.
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 singleKP2BW_METAcustom 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.
# run directly
uvx kp2bw@latest
# install globally with:
uv tool install kp2bw
# update with:
uv tool update kp2bw
kp2bw --version
kp2bw passwords.kdbxor 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.kdbxInstall 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 outputkp2bw [-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]
[--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 |
--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 |
- |
Configuration precedence is always: CLI flag > environment variable > built-in default.
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.
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.
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/collectionto 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.
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=1Then 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.
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.
Footnotes
-
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
.txtfile attachment instead of dropping it, so you don't lose data. This applies to long notes as well as custom fields. β©