Metadata-Version: 2.4
Name: kp2bw
Version: 3.9.0
Summary: Imports and existing KeePass db with REF fields into Bitwarden
Keywords: bitwarden,cli,keepass,migration,password-manager
Author: Kaj Kowalski
Author-email: Kaj Kowalski <info@kajkowalski.nl>
License-Expression: MIT
License-File: LICENSE
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Dist: cryptography>=50.0.2
Requires-Dist: httpx>=0.28.0
Requires-Dist: pykeepass>=4.1.1.post1,<4.3
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: rich>=13.0.0
Requires-Python: >=3.14
Project-URL: Changelog, https://github.com/kjanat/kp2bw/blob/master/CHANGELOG.md
Project-URL: Issues, https://github.com/kjanat/kp2bw/issues
Project-URL: Repository, https://github.com/kjanat/kp2bw
Description-Content-Type: text/markdown

# KP2BW - KeePass to Bitwarden Converter

<a href="https://pypi.org/project/kp2bw/">
  <img
    src="https://img.shields.io/pypi/v/kp2bw?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMS42OTYgMzAuMDI0Ij48ZyBzdHJva2U9IiNjY2MiIHN0cm9rZS1saW5lam9pbj0iYmV2ZWwiIHN0cm9rZS13aWR0aD0iLjM1NSI%2BPHBhdGggZmlsbD0iI2Y3ZjdmNCIgZD0ibS4xNzggNS45MTIgMTUuNTU1IDUuNjYyTDMxLjUxOSA1LjgzIDE1Ljk2My4xNjd6Ii8%2BPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1LjczMyAxMS41NzR2MTguMjgzbDE1Ljc4Ni01Ljc0NlY1LjgzeiIvPjxwYXRoIGZpbGw9IiNlZmVlZWEiIGQ9Im0uMTc4IDUuOTEyIDE1LjU1NSA1LjY2MnYxOC4yODNMLjE3OCAyNC4xOTV6Ii8%2BPC9nPjwvc3ZnPg%3D%3D&color=3775A9"
    alt="PyPI"
  >
</a>
<a href="https://github.com/sponsors/kjanat">
  <img
    src="https://img.shields.io/badge/Sponsor-ea4aaa?logo=githubsponsors&logoColor=white"
    alt="GitHub Sponsors"
  >
</a>
<a href="https://github.com/kjanat/kp2bw/actions/workflows/integration-docker.yml?query=branch%3Amaster">
  <img
    src="https://github.com/kjanat/kp2bw/actions/workflows/integration-docker.yml/badge.svg?branch=master"
    alt="Compatibility tests"
  >
</a>

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

- <details>
    <summary><b>Encrypted in-memory transfer</b></summary>
    Data never hits disk unencrypted (except attachments, which are cleaned up after upload).
  </details>
- <details>
    <summary><b>KeePass REF resolution</b></summary>
    Username/password references are resolved.<br> 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.
  </details>
- <details open>
    <summary><b>Passkey migration</b></summary>
    Valid KeePassXC P-256 passkeys (<code>KPEX_PASSKEY_*</code>) are converted to Bitwarden <code>fido2Credentials</code>. If conversion fails, source attributes remain as hidden fields; oversized secrets still require explicit opt-in.
  </details>
- <details>
    <summary><b>Custom properties &amp; attachments</b></summary>
    Imported as Bitwarden custom fields or attachments (oversized values upload as files).
    Same-named files are retained with distinct output names and tracked by attachment ID.
    Uploads run serially per item and concurrently across items. Uploaded files are synced and downloaded to verify their bytes before success is reported or an old copy is deleted. A verification failure makes the migration exit nonzero and retains the old copy.
  </details>
- <details>
    <summary><b>Long notes handling</b></summary>
    Oversized notes are uploaded as <code>notes.txt</code> attachments.
  </details>
- <details>
    <summary><b>Idempotent re-runs that sync changes</b></summary>
    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 <code>KP2BW_ID</code> 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 <b>protected</b>: a re-run preserves them instead of reverting to KeePass (a <code>KP2BW_SYNC</code> content stamp tells your edit apart from kp2bw's own writes); <code>--force-update</code> overrides.<br> Disable updates with <code>--no-update</code>.
  </details>
- <details>
    <summary><b>Nested folders</b></summary>
    KeePass folder hierarchy is recreated in Bitwarden.
  </details>
- <details>
    <summary><b>Recycle Bin filtering</b></summary>
    Deleted entries are automatically excluded.
  </details>
- <details>
    <summary><b>Expiry awareness</b></summary>
    Expired entries are marked <code>[EXPIRED]</code> in notes; optionally skip them entirely with <code>--skip-expired</code>.
  </details>
- <details>
    <summary><b>Metadata preservation</b></summary>
    KeePass tags and expiry date are folded into a single <code>KP2BW_META</code> custom field (JSON), omitted when an entry has neither. Existing YAML metadata upgrades on the next safe migration; manually edited items remain protected. Created/modified timestamps are not migrated — Bitwarden manages its own creation/revision dates.
  </details>
- <details>
    <summary><b>Tag filtering</b></summary>
    Import only entries matching specific tags.
  </details>
- <details>
    <summary><b>Organization &amp; collection support</b></summary>
    Upload into a Bitwarden organization with automatic or manual collection assignment.
  </details>
- <details>
    <summary><b>Full UTF-8 &amp; cross-platform</b></summary>
    Works on Windows, macOS, and Linux.
  </details>

> Fork of [jampe/kp2bw].

## Container

Release images support Linux amd64 and arm64. The default image uses `@kjanat/vaultwarden-cli`; choose a `-bw` tag for
`@bitwarden/cli`. Both variants are tested as production images on both architectures.

| Tags                                         | CLI             | Updates                        |
| -------------------------------------------- | --------------- | ------------------------------ |
| `<version>`, `<version>-vw`                  | Vaultwarden CLI | Never overwritten              |
| `<version>-bw`                               | Bitwarden CLI   | Never overwritten              |
| `latest`, `vw`, `<major>`, `<major>.<minor>` | Vaultwarden CLI | Stable releases                |
| `<major>-vw`, `<major>.<minor>-vw`           | Vaultwarden CLI | Stable releases in that series |
| `bw`, `<major>-bw`, `<major>.<minor>-bw`     | Bitwarden CLI   | Stable releases in that series |

Images are published under `ghcr.io/kjanat/kp2bw`. OCI labels identify the kp2bw version, source revision, CLI package
and CLI version. Inspect the installed CLI with `docker run --rm --entrypoint bw ghcr.io/kjanat/kp2bw:latest --version`.
Use a separate state volume for each CLI variant.

Keep login state and logs in a persistent volume; mount the database directory read-only. No published ports are needed.
The default command is `kp2bw`; use `--entrypoint bw` to configure the server or log in with password/2FA prompts.

Bash:

```bash
docker volume create kp2bw-state
# Optional: use your server instead of Bitwarden Cloud.
docker run --rm -it --mount type=volume,src=kp2bw-state,dst=/data \
  --entrypoint bw ghcr.io/kjanat/kp2bw:latest config server https://vault.example.com
docker run --rm -it --mount type=volume,src=kp2bw-state,dst=/data \
  --entrypoint bw ghcr.io/kjanat/kp2bw:latest login
docker run --rm -it --mount type=volume,src=kp2bw-state,dst=/data \
  --mount "type=bind,src=$PWD/vault,dst=/source,readonly" \
  ghcr.io/kjanat/kp2bw:latest /source/vault.kdbx --keepass-keyfile /source/key.key
```

PowerShell:

```powershell
docker volume create kp2bw-state
docker run --rm -it --mount "type=volume,src=kp2bw-state,dst=/data" `
  --entrypoint bw ghcr.io/kjanat/kp2bw:latest config server https://vault.example.com
docker run --rm -it --mount "type=volume,src=kp2bw-state,dst=/data" `
  --entrypoint bw ghcr.io/kjanat/kp2bw:latest login
docker run --rm -it --mount "type=volume,src=kp2bw-state,dst=/data" `
  --mount "type=bind,src=$($PWD.Path)/vault,dst=/source,readonly" `
  ghcr.io/kjanat/kp2bw:latest /source/vault.kdbx --keepass-keyfile /source/key.key
```

Omit `--keepass-keyfile` if the database has none. The examples import into the personal vault; append `--bitwarden-org
<ID> --bitwarden-collection <ID>` for an organization. Other kp2bw arguments pass through unchanged. For unattended
runs, pass existing host variables with Docker's `--env KP2BW_KEEPASS_PASSWORD`, `--env KP2BW_BITWARDEN_PASSWORD` and
`--env KP2BW_YES` before the image name; omit `-it`.

The image runs as UID/GID **10001** in `/w`, with `BITWARDENCLI_APPDATA_DIR=/data/bw` and logs in `/data/logs`. Docker
initializes named volumes with the required ownership. For bind-mounted state, give UID 10001 write access; for the
source directory and keyfile, give it traversal/read access (for example through a per-user ACL). Replacing the
container retains authentication when `/data` is reused. `docker stop` forwards termination to kp2bw and its child
server. `--help`, `--version` and `--doctor` work through the normal entrypoint.

## Usage/Installation

```bash
# 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):

```bash
# 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`:

```bash
# 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
```

```jsonc
// 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

```text
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]
[--entry-id UUID] [--item-id UUID]
[--metadata | --no-metadata] [--update | --no-update] [--force-update]
[--map-templates] [--template-map JSON] [--ssh-map JSON]
[--include-oversize-secrets] [--folder | --no-folder] [--uri-match MODE]
[--totp-pps | --no-totp-pps]
[--interpret-uri-syntax | --no-interpret-uri-syntax]
[--fixup] [--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`                  |
| `--map-templates` / `--no-map-templates` | Map referenced templates to native cards, identities and secure notes (default: off)                                                                                                                | `KP2BW_MAP_TEMPLATES`                 |
| `--template-map JSON`                    | [Override template types and field mappings](docs/template-mapping.md); requires `--map-templates`                                                                                                  | `KP2BW_TEMPLATE_MAP`                  |
| `--ssh-map JSON`                         | [Map selected entries to native SSH keys](docs/ssh-mapping.md)                                                                                                                                      | `KP2BW_SSH_MAP`                       |
| `--include-oversize-secrets`             | Offload over-limit secret fields[^offload] 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                                                                                       | -                                     |
| `--entry-id UUID`                        | Sync only this KeePass entry; intersects existing import and destination filters                                                                                                                    | —                                     |
| `--item-id UUID`                         | Sync only this existing Bitwarden item using its KeePass UUID stamp                                                                                                                                 | —                                     |
| `--fixup`                                | Upgrade known legacy URI fields and YAML metadata in Bitwarden without reading KeePass                                                                                                              | `KP2BW_FIXUP`                         |

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

Use `--preview` for a read-only migration plan, or `--preview json --preview-target` to compare with your vault. See
[migration preview](docs/migration-preview.md) for output details and limitations.

`--password-history` (`KP2BW_PASSWORD_HISTORY`, default: on) imports up to five distinct previous passwords, excluding
the current password. Each date is the modification time of the next version with a different password; note-only edits
do not change it. Existing unrelated Bitwarden history is preserved. If the combined records exceed five, the update
fails rather than dropping records. Use `--no-password-history` to omit native history migration.

The official CLI accepts history when creating an item but ignores supplied history changes during updates. Such updates
require `@kjanat/vaultwarden-cli` through a `bw` adapter; kp2bw stops them before writing with the official CLI.
Unchanged reruns and edits that preserve history remain supported. Native history writes are verified after sync.

`--archive-history` (`KP2BW_ARCHIVE_HISTORY=true`) saves full KeePass historical records as an encrypted vault
attachment. It includes old passwords, protected fields, notes, source timestamps and available historical attachment
bytes. The generated filename avoids collisions with your files. Missing historical bytes are reported and their
references retained. The option is off by default; it does not change Bitwarden's creation or revision timestamps.

Use `--checkpoint PATH` (or `KP2BW_CHECKPOINT`) to resume interrupted attachment uploads and avoid downloading verified,
unchanged files on later runs. See [attachment checkpoints](docs/attachment-checkpoints.md) for usage and filename
changes.

### 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.

`kp2bw --fixup` upgrades both legacy URL/app fields and YAML `KP2BW_META` to JSON without reading KeePass. It defaults
to your personal vault; use `-o <org>` and optionally `-c <collection>` for an organization scope. Modified stamped
items are preserved. Recognized unstamped fields are upgraded and stamped; malformed or ambiguous data stays untouched
with a warning. The summary reports scanned, migrated, skipped and failed items; failures return exit code 1. Repeated
runs write nothing once eligible items are upgraded. `--migrate-uris` remains URI-only.

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.

To retry just one item, use `kp2bw vault.kdbx --entry-id <KeePass-UUID>` or `--item-id <Bitwarden-UUID>`. Add
`--force-update` to overwrite an edit on that item alone. When both IDs are supplied, they must identify the same
source. Unknown, ambiguous or excluded IDs stop before writes. A new source selected by UUID can be created, but
selection never adopts another item by title. A selected REF alias is imported separately; its referenced entries are
read without being written. `--no-update` still preserves existing content and attachments; collection membership may
sync. Selectors apply only to migration, not maintenance modes.

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

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

to remove migration stamps and attachment associations from every migrated item and exit. No KeePass database is read.

Source fields named `KP2BW_ID`, `KP2BW_SYNC`, `KP2BW_META` or beginning with `KP2BW_FILE_` are preserved under names
such as `KeePass:KP2BW_ID:1`, with their values and hidden/text status intact. Duplicate names and source fields already
beginning with `KeePass:` are escaped too. These source fields survive `--strip-ids`. Rerunning from the original
KeePass database can restore source values overwritten by older imports; Bitwarden edits remain protected.

> [!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:

```env
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

### Compatibility

If the `bw serve` process exits during migration, kp2bw stops and reports the CLI version and exit code. Check the
[compatibility test results](https://github.com/kjanat/kp2bw/actions/workflows/integration-docker.yml) before rerunning;
a server may have accepted the last write before the CLI crashed. Normal stable-ID reconciliation handles that on a
rerun. A temporary connection drop while the process is still running retains the normal retry behaviour.

### Logs and timeouts

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].

## Security

For supported versions and private vulnerability reporting, see the [security policy](SECURITY.md). Use synthetic
fixtures and redact evidence; do not share real vaults, credentials or production attachments.

[^offload]: Limits apply to encrypted fields, including UTF-8 and encryption overhead. Because `bw serve` does not
    report the effective encryption format, kp2bw conservatively uses structured-field limits for both personal and
    organization vaults. Oversized notes, custom values and generated metadata become attachments. Secret custom values
    require `--include-oversize-secrets`; unsupported titles or login fields fail before migration without truncation.

["🧪 Test this PR"]: https://github.com/kjanat/kp2bw/pull/34#issuecomment-4763225590 "Example"
[Bitwarden CLI]: https://bitwarden.com/help/cli/
[TROUBLESHOOTING]: https://github.com/kjanat/kp2bw/blob/HEAD/TROUBLESHOOTING.md
[`.env.example`]: https://github.com/kjanat/kp2bw/blob/HEAD/.env.example
[jampe/kp2bw]: https://github.com/jampe/kp2bw
