Releases: kjanat/kp2bw
Release list
v3.7.0
Added
-
--bitwarden-collection nested-- recreate the full KeePass folder
hierarchy as nested collections (issue #33, PR #34).autoonly ever used
the top-level folder name, soWork/Servers/sshcollapsed onto a single
Workcollection and the nesting was lost.nestedbuilds collections from
the full/-joined path (Work/Servers), which Bitwarden renders as a nested
tree. Collections are matched by org + name and seeded from the existing set
on connect, so an unchanged tree is a no-op and entries sharing a path never
duplicate a collection. -
--folder/--no-folder(envKP2BW_CREATE_FOLDERS) -- control whether
personal-vault folders are created from KeePass groups. Defaults on for
personal imports, so existing behaviour is unchanged there (issue #33, PR
#34).
Changed
- An organization import (
--bitwarden-org) no longer also builds a personal
folder tree by default (issue #33, PR #34). Bitwarden folders are a
personal-vault concept; the org-side equivalent is a collection, so creating
both double-filed every item -- the collection and a redundant
personal-folder copy. With an org set,--no-folderis now the default and
items are filed into collections only. Pass--folder(or
KP2BW_CREATE_FOLDERS=1) to restore the personal folder tree alongside the
collections. Personal imports (no--bitwarden-org) are unaffected.
v3.6.0
Fixed
-
bw serveis no longer orphaned on teardown (POSIX), which previously hung
the process. On Linux/macOSbwis commonly a node launcher that spawns a
worker; teardown signalled only the tracked PID, leaving the worker alive --
it kept the port and, when kp2bw's stdout was a pipe, held the pipe open so
the parent pipeline never reached EOF (a multi-minute "still running" hang)
and accumulated orphanedbw serveprocesses across runs.bw serveis now
started in its own session (start_new_session=True) and torn down by
signalling the whole process group (SIGTERM, then SIGKILL after a timeout), so
the launcher and worker die together. Windows teardown (taskkill /T + port
reap) is unchanged. -
An empty environment variable no longer shadows the matching
.enventry.
KP2BW_KEEPASS_FILE=""(or any empty-string export) used to override the
.envvalue --load_dotenv(override=False)treats an empty export as "set"
-- producing a baffling "KeePass database path is required" even when.env
clearly had it._load_dotenvnow fills any variable that is unset or empty
from the file, while a real non-empty shell variable still wins (the
documented CLI flag > env var > default precedence is preserved for meaningful
values).
Added
-
Manual edits made in Bitwarden are no longer silently reverted on re-run
(issue #30). kp2bw is KeePass-authoritative, so any difference on an existing
item used to be overwritten with the KeePass value -- quietly undoing a title
you fixed, a note you added or a URI you corrected in Bitwarden. Every item
kp2bw writes now carries a hidden-from-noiseKP2BW_SYNCcontent signature; a
re-run that finds an item's current content no longer matching that stamp
knows a user edited it (kp2bw's own writes restamp, so they never self-trip)
and preserves the edit, reporting it asprotectedin the summary instead
of clobbering it.--force-update(envKP2BW_FORCE_UPDATE) makes KeePass
win regardless. Unchanged re-runs stay idempotent (the stamp is excluded from
the content diff), legacy/ unstamped items keep current behaviour, and
--strip-idsnow removesKP2BW_SYNCalongsideKP2BW_ID. -
--report-uris keepass|bitwarden-- a read-only URI collision report (env
KP2BW_REPORT_URIS). Groups every login URL by registrable domain (a curated
two-level public-suffix heuristic, so10bis.co.ilstays whole) and lists the
domains with more than one host -- exactly the logins that all surface
together under Bitwarden's base-domain matching.keepassreads the database
(previewing post-migration collisions);bitwardenreads the live vault
(honouring-o/-c). It changes nothing -- it just prints, so you can decide
which entries to switch to Host match (or flip your account's default URI
match detection). -
Additional URLs and Android packages migrate as Bitwarden login URIs, not
custom fields -- a KeePass(XC) entry's additional URLs
(KP2A_URL/KP2A_URL_n, plus the plainerURL/URL_nconvention) and
Android packages (AndroidApp/AndroidApp_n, including the no-underscore
AndroidApp1variant) were copied verbatim into custom fields, where they
were inert. They now become real entries inlogin.uris, so one login
autofills across every site and app it covered in KeePass; free-text URL
labels (API Url,Alt. URL,Website, …) are deliberately left as custom
fields. Each URI gets a per-URI match mode reproducing KeePassXC's behaviour:
a plain URL → the account default (matchleft unset -- what Bitwarden itself
writes on export;--uri-match/KP2BW_URI_MATCHoverrides, e.g.domain
forces base-domain to replicate KeePassXC's host-based matching), a
double-quoted URL → exact, and a*wildcard → starts-with (trailing path) or
regex.AndroidAppbecomes anandroidapp://URI. Non-web schemes
(keepassxc://,cmd://,kdbx://,file://) and unresolved{REF:…}URLs
are dropped rather than left as dead URIs.--no-interpret-uri-syntax
(KP2BW_INTERPRET_URI_SYNTAX) disables the quote/wildcard interpretation for
a literal import. Bitwarden applies a regex to the whole URL (unlike
KeePassXC's separate host/path regexes), so complex wildcards are emitted as a
best-effort whole-URL regex with a warning to review. Items imported before
this are upgraded by a normal re-run (the change is detected and the item
updated in place); for users who don't want to re-import,
kp2bw --migrate-uris(envKP2BW_MIGRATE_URIS) is a Bitwarden-only one-shot
pass that re-folds the legacy fields into URIs on every existing item. Both
honour--uri-match/--interpret-uri-syntaxand-o/-cscope. -
Configurable per-request HTTP timeout via
KP2BW_HTTP_TIMEOUT-- the
timeout for a singlebw serverequest is now overridable through the
KP2BW_HTTP_TIMEOUTenvironment variable (seconds), so a slow self-hosted
server (e.g. Vaultwarden) where an individual item write outlasts the default
no longer times out. Non-numeric or non-positive values are ignored with a
warning and the default is used; values above the 3600s sanity ceiling are
clamped (with a warning) so a typo like999999can't silently hang a
migration for hours. The value is applied as httpx's single timeout, which
stretches the connect/read/write/pool phases together rather than the
slow-write phase alone. The built-in default is also raised from 60s to 180s,
sincebw serveforwards writes to the (possibly remote) Bitwarden server and
a single create can legitimately take longer than local work. This complements
the existing non-fatal-failure handling: raising the ceiling avoids the
slow-request failure in the first place, rather than only tolerating it on a
re-run. (#27) -
--strip-idsfinalize mode (envKP2BW_STRIP_IDS) to remove kp2bw's
KP2BW_IDdedup stamps -- every migrated item carries a plain-text
KP2BW_IDcustom field (the KeePass UUID kp2bw matches on for idempotent
re-runs). Once a user is satisfied the migration is complete and ready to
fully adopt Bitwarden,kp2bw --strip-idsremoves that stamp from every
migrated item and exits -- no KeePass database is read, no migration runs.
Scope follows-o/-cexactly as a migration would; other vault data is
untouched. The strip itself is safe to repeat (a second pass finds nothing),
but it is irreversible and makes future migration re-runs unreliable:
without the stamp a re-run falls back to folder + name matching -- the exact
collision the stamp disambiguates -- so entries sharing a folder and title can
be duplicated or mismatched. Because of that it confirms before changing
anything (skippable with-yfor callers who know what they want) -- a
deliberate final step, not a routine flag. (#28)
v3.5.0
Added
- Automatic
.envloading and aKP2BW_KEEPASS_FILEenv var for the database
path --kp2bwnow loads a.envfile (searched upward from the current
working directory) into the environment on startup, so settings can live in a
file instead of your shell history; a real shell variable still overrides a
.enventry, keeping the documented CLI flag > env var > default precedence.
The KeePass database path -- previously CLI-only -- can now be supplied via
KP2BW_KEEPASS_FILE, making the positionalFILEoptional. A new
.env.exampledocuments every supported variable. - Always-on DEBUG log file -- a complete DEBUG trace (including third-party
httpx/bw servedetail) is now always written to a per-user log file
regardless of console verbosity --%LOCALAPPDATA%\kp2bw\logson Windows, the
platform data dir elsewhere -- so a failed run leaves a full record to share
without re-running with-v/-d. Override the file withKP2BW_LOG_FILEor
the directory withKP2BW_LOG_DIR. The console stays as quiet as before by
default. - A heads-up when the
bw serveserver and CLI versions disagree -- a
server/CLI version mismatch (a common source of confusingbw servefailures)
is flagged up front instead of surfacing later as an opaque error.
Changed
- KeePass tags and expiry now fold into a single
KP2BW_METAfield;
Created/Modifiedare no longer migrated -- the metadata Bitwarden has no
native slot for is serialised as YAML into oneKP2BW_METAcustom field
(PyYAMLsafe_dump, so control characters and the U+0085/2028/2029 line
breaks are escaped, not silently corrupted) instead of several separate
Tags/Expires/date fields, and the field is omitted entirely when an entry
has neither tags nor an expiry. Creation/modification timestamps are dropped:
Bitwarden manages its own creation/revision dates and the API cannot backdate
them (a client-supplied date is ignored on create and rejected on update), so
they had no real home.
Removed
- The unused legacy
bitwardenclientandbw_importmodules -- the
deprecated subprocess-per-operation CLI wrapper and the file-basedbw import
path, both long superseded by thebw serveHTTP transport and reachable from
no supported entry point, were removed (git history retains them for
reference).
Fixed
kp2bw --version(and usage/error messages) now printkp2bw, not the
launcher path. Python 3.14's argparse derives the default program name from
how the script was launched, so a console-script run through uv's trampoline
printedpython.exe C:\...\Scripts\kp2bw 3.4.1. The program name is now
pinned tokp2bw.- Distinct entries sharing a title no longer collapse onto one Bitwarden item
(silent data loss) -- deduplication keyed on(folder, title), so several
different logins that happened to share a title (e.g. four accounts all named
192.168.2.67) merged into a single item and re-runs churned
non-idempotently. Every migrated item now carries its source KeePass entry
UUID in aKP2BW_IDfield and dedup keys on that: a match by UUID stays
idempotent across title/folder edits, an unstamped legacy item is adopted once
and back-stamped, and only a genuinely new entry creates a new item. - A single slow or dropped
bw serverequest no longer aborts the whole
migration (#24) -- a create that timed out or hit a dropped keep-alive
connection (httpx.ReadTimeout/ReadError) crashed the run and stranded
every entry after it. Idempotent requests (including the startup sync/unlock)
are now retried on a transient transport error, and a per-entry create,
folder, or collection failure is reported and skipped rather than fatal -- so
the migration finishes, the summary counts what failed, and a re-run safely
adopts anything a timed-out request already created server-side. bw serveHTTP errors now carry the server's actual message -- a failed
request surfaces the response body (Bitwarden/Vaultwarden's realmessage/
validation error) instead of an opaqueHTTP 400.- Windows: orphaned
bw serveprocesses are reaped reliably -- a
shim-launchedbw serveruns as anodegrandchild thattaskkill /Tdid
not always reap, leaving orphans that deadlocked the sharedbwapp-data on
later runs. Teardown now also kills whatever still listens on the serve port,
regardless of process-tree shape.
Full Changelog: v3.4.1...v3.5.0
v3.4.1
Fixed
- A newly created item could lose an attachment on re-run -- when
kp2bw
created a fresh Bitwarden item and immediately uploaded its attachment,
bw servecould fail to resolve the just-created item (it looks the id up in
its local vault cache, not on the server), reportNot found, and silently
drop that one attachment. The vault is now synced after items are created and
before attachments upload, and an upload that still hits a not-found error
syncs and retries once.
Full Changelog: v3.4.0...v3.4.1
v3.4.0
Added
- Reproducible snapshot-backed Vaultwarden e2e suite (#20) -- the
integration test migrates a comprehensive KeePass seed (every TOTP shape, text
and hidden custom fields, real file/image attachments, nested folders,
unicode, empty password) into a real Vaultwarden and captures the result as
normalized golden snapshots undertests/__snapshots__/, so migration drift
surfaces as a reviewable diff; idempotency is proven at the snapshot level
(pass 1 == pass 2). CI runs a host-mode@bitwarden/cliversion matrix via a
newsetup-bwcomposite action -- the pinned leg gates the golden,latest
is a behavioral-only canary -- with Vaultwarden andbwversion-pinned and
Dependabot-managed. --include-oversize-secretsto recover over-limit secret fields (#21) --
a hidden OTP secret (e.g. an HOTPHmacOtp-Secret), a passkey attribute, or a
KeePass-protected custom field whose value tops the 10k inline limit survives
nowhere else, so it was dropped. The new flag (env:
KP2BW_INCLUDE_OVERSIZE_SECRETS, default off) offloads it to a<key>.txt
attachment like any other long field. Off by default so a secret is never
written to a readable attachment without consent.
Fixed
- Oversize custom fields routed to their attachment, not inline (#21) -- a
custom field whose value exceeds the 10k inline limit is offloaded to its
<key>.txtattachment only, decided at the source when the Bitwarden item is
built (symmetric with how a long note goes tonotes.txt), instead of being
left among the inline fields. Regression coverage locks the long-field →
<key>.txtpath into the golden e2e snapshots. - Over-limit secret custom fields were dropped silently (#21) -- a hidden
OTP secret, passkey attribute, or KeePass-protected field longer than the 10k
inline limit was filtered out of the inline fields and excluded from the
.txtattachment offload, vanishing with no log line. Such a field is now
warned-and-dropped by default
(pointing at--include-oversize-secretsto keep it), so data is never lost
without notice. A consumed OTP key over the limit is still dropped silently --
it is already preserved inlogin.totp, so dropping the raw field is
deduplication, not loss.
What's Changed
- Reproducible Vaultwarden e2e with golden snapshots by @kjanat in #20
- fix: route oversize custom fields to .txt; stop silently dropping oversize secrets by @kjanat in #23
Full Changelog: v3.3.0...v3.4.0
v3.3.0
Added
- In-place updates for changed entries -- re-running
kp2bwnow syncs
KeePass edits onto existing Bitwarden items instead of skipping them. Changed
notes, passwords, usernames, URIs and custom fields are written via an
idempotentPUT(an unchanged entry issues no request). Collection
membership is only ever added, never removed, and a Bitwarden-sidefavorite
flag or a passkey absent from KeePass is preserved. Opt out with--no-update
(env:KP2BW_UPDATE=0) to restore the previous skip-only behavior.
Fixed
- Updated KeePass notes never reached Bitwarden (#11) -- editing an entry's
notes (e.g. pasting in new recovery keys) without touching credentials left
the existing Bitwarden item unchanged, forcing a full vault purge to
re-import. Existing items are now updated in place on re-run. - Long notes not attached to (or refreshed on) previously imported entries
(#11) -- notes over 10k chars migrate to anotes.txtattachment, but
previously imported (skipped) entries never received it, and an edited
attachment that kept the same filename was never updated. Re-runs now upload
any attachment an existing item is missing and refresh one whose content
changed (the stale copy is removed only after the replacement uploads), all
without creating duplicates. Applies to every attachment kp2bw manages --
long notes, long custom fields, and real KeePass file attachments. - A single rejected attachment aborted the whole migration (#11) -- an
attachment the server refused (for example a.jpgrejected for premium or
storage-quota reasons) raised an opaqueHTTP 400that stopped everything.
Upload failures are now non-fatal: the real server message is surfaced
instead of just the status code, and the migration continues with the
remaining entries.
What's Changed
- ci(deps): bump actions/checkout from 5 to 6.0.2 by @dependabot[bot] in #18
- fix: update existing Bitwarden entries on re-run (#11) by @kjanat in #19 — thanks @gamix-git for reporting #11 🙏
Full Changelog: v3.2.0...v3.3.0
v3.2.0
Added
- Windows shim support (
bw.cmd/bw.bat/bw.ps1) -- Installer methods put
different shims onPATHandCreateProcesscan only run real.exe/.com
images.resolve_bw_commandnow resolves all flavours, most-reliable first:
nativebw.exe/bw.comrun directly;bw.cmd/bw.batare routed through
cmd.exe /c(invoked by basename from their own directory to avoid
shell-quoting issues); and abw.ps1(which isn't inPATHEXT, so
shutil.whichcan't see it) is found onPATHand run through PowerShell. So
when npm ships bothbw.cmdandbw.ps1, thecmdshim is preferred.
terminate_servetears thebw serveprocess tree down withtaskkill /F /T
so the real server isn't orphaned behind acmd.exe/PowerShell wrapper. A
windows-bw-cmdCI job installsbwvia npm and runs a live smoke test
(tests/windows_bw_cmd_smoke.py) covering shim invocation (bw --version) and
cmd.exe-wrapped process-tree teardown. Fixes #8.
Fixed
- Missing
bwCLI traceback -- When the Bitwarden CLI (bw) was not on
PATH,kp2bwcrashed with a long, intimidatingFileNotFoundError
traceback fromsubprocess. The CLI now checks forbwup front (before
prompting for passwords) and exits cleanly with an actionable message; any
BitwardenClientError/ConversionErrorraised during conversion is reported
the same way instead of as a stack trace.BitwardenServeClientraises a
BitwardenClientErrorrather than lettingFileNotFoundErrorescape.
Detection usesshutil.which, so Windowsbw.exe/bw.cmdshims are found via
PATHEXT; thebwsubprocess calls also catchFileNotFoundError, so a
genuinely missing CLI still yields the friendly message. Fixes #5. - Chained
{REF:...}references -- a reference whose target was itself
another reference entry (a chainA -> B -> C) raisedKeyErrorin
_resolve_entries_with_references, logged aCould not resolve entry
warning, and dropped the referencing entry from the import even though
KeePass resolves such chains correctly. Unresolved targets that are
themselves REF entries are now resolved transitively and on demand, with
memoization and cycle detection, so the chain collapses onto whatever it
ultimately maps to. Fixes #6. - Malformed
{REF:...}tokens no longer abort the run -- a reference whose
field/lookup part lacked the@separator (e.g.{REF:UI:...}) raised an
uncaughtValueErrorin_parse_kp_ref_stringthat stopped the whole
migration. Such tokens are now reported and the offending entry is skipped,
consistent with other unresolvable references.
What's Changed
- fix: resolve chained REF references by @kjanat in #14 — thanks @szotsaki for reporting #6 🙏
- fix: friendly error when bw CLI is missing, and support Windows npm bw.cmd shims by @kjanat in #16 — thanks @szotsaki (#5) and @Joly0 (#8) 🙏
Full Changelog: v3.1.0...v3.2.0
v3.1.0
Added
- KeePass2/KeePassXC native TOTP migration -- entries that store TOTP in the
TimeOtp-*custom fields (rather than theotpfield) now migrate to
Bitwarden'slogin.totp. All four KeePass secret encodings are supported
(TimeOtp-SecretUTF-8,-Hex,-Base32,-Base64), and non-default
TimeOtp-Length/-Period/-Algorithmsettings are emitted as a full
otpauth://URI so Bitwarden generates correct codes instead of silently
defaulting to 6 digits / 30 s / SHA-1. A default-config Base32 secret still
migrates as a bare secret;entry.otpkeeps precedence when both are present.
Logic lives in a new pure, unit-testedkp2bw/otp.pymodule.
Fixed
- Lossy/leaky TOTP fallback -- the initial
TimeOtp-Secret-Base32fallback
dropped non-default OTP configuration (producing wrong 2FA codes), ignored the
other three secret encodings, and stripped the Base32 secret from custom
fields even when it was not the value migrated. Secrets are now removed from
custom fields only when actually folded intologin.totp; any OTP secret left
behind (HOTP, an undecodable value, or one shadowed byentry.otp) is
preserved as a hidden custom field rather than dropped or exposed. - Silent HOTP loss -- counter-based HOTP (
HmacOtp-Secret*) has no
time-based target in Bitwarden. It is now reported with a warning and its
secret kept as a hidden field, instead of silently becoming a visible
plaintext custom field.
What's Changed
- feat: lossless KeePass TOTP/HOTP migration by @kjanat in #15 — thanks @Eryniox for the original implementation in #12 🙏
Full Changelog: v3.0.1...v3.1.0
v3.0.1
Fixed
Noneusername/password in REF resolution --_resolve_entries_with_referencesraisedTypeError: argument of type 'NoneType' is not iterablewhen an entry with a{REF:...}field also had aNoneusername or password. The field is now guarded before theKP_REF_IDENTIFIERmembership test, andNonevalues are normalized to""for the username/password match so a resolved entry merges its URI instead of spawning a duplicate. Fixes #9, #4.
What's Changed
- chore(deps): bump black from 26.1.0 to 26.3.1 in the uv group across 1 directory by @dependabot[bot] in #7
- chore(deps): bump the uv group across 1 directory with 3 updates by @dependabot[bot] in #13
- Handle None values in reference resolution by @BrainStone in #10 — thanks @szotsaki for reporting #4 🙏
New Contributors
- @BrainStone made their first contribution in #10
Full Changelog: v3.0.0...v3.0.1
v3.0.0
Highlights
Stable release of the bw serve HTTP transport rewrite. All features from the 3.0.0a1/a2 prereleases, plus:
- Strict TypedDict API layer —
BwItemCreate,BwItemResponse,BwItemLogin,BwUri,BwField,BwFido2Credential,BwFolder,BwCollection. Alldict[str, Any]eliminated from transport code. Generated_bw_api_types.pyis the canonical source for spec-derived shapes. - Org-scoped dedup index — Fixes collection-blind imports where personal-vault entries shadowed an empty org vault. Dedup is now scoped to
organizationIdand optionallycollectionId. - Collection-aware import — Items already in the org but in a different collection get a
PUTto add the target collection instead of being silently skipped. - Rich progress bars — Live progress for processing, creating, and uploading phases, plus a final migration summary.
- FIDO2/passkey migration — KeePassXC
KPEX_PASSKEY_*attributes converted to Bitwardenfido2Credentials. - Codegen drift CI —
codegen-check.ymlfails PRs when_bw_api_types.pydrifts fromspecs/vault-management-api.json.
New since v2.0.0
bw serveHTTP transport (persistent process, automatic port, health polling, signal-safe cleanup) by @kjanat in #2- Batch import via
bw importfor bulk creation - Async parallel attachment uploads (bounded concurrency)
- O(1) dedup index (no more per-item API lookups) by @kjanat in #3
-d/--debugflag for full DEBUG logging-vnow shows kp2bw operational detail only (httpx silenced)- KeePass metadata migration (tags, expiry, timestamps) as custom fields
--skip-expired/--include-recycle-bin/--metadataflags- CLI help metavars improved
- All env vars documented (
KP2BW_*)
Key fixes
- Shell injection in
bw import(replacedshell=True) bw serveIPv6 binding mismatch causing 60s timeoutorganizationIdquery param casing on collection list- Dedup cache stale after collection PUT
- Signal handler init race on early timeout
close()double-call crash- Sensitive secrets redacted from diagnostic output
Breaking changes
- Requires Python ≥ 3.14
- New dependencies:
httpx>=0.28.0,rich>=13.0.0 - Transport layer completely replaced — CLI behavior is the same but internal API changed
Full Changelog: v2.0.0...v3.0.0