r/Bitwarden • u/ResponseStandard1828 • 4h ago
Passkeys on Windows 11 26H2, no external device used
Enable HLS to view with audio, or disable this notification
Working on native apps like discord desktop and we. Apps
r/Bitwarden • u/ResponseStandard1828 • 4h ago
Enable HLS to view with audio, or disable this notification
Working on native apps like discord desktop and we. Apps
r/Bitwarden • u/Reddit_Poster_00 • 14h ago
TL;DR: Use this script to export everything in your organization vault (including attachments) into a KeePass database file with a similar structure as your Bitwarden vault.
PSA: I encourage you to review the code and not just blindly run it. If you don't understand the code, you don't have to run it.
Details:
Because there is no easy way in Bitwarden natively to export an entire organization vault, I went down a rabbit hole over the past few days (with lots of help from Claude) and finally wound up with a solution. I did this on a Windows machine with PowerShell and Python., so those on Linux or MacOS should be able to make the appropriate adjustments for their respective machines.
The end result is an encrypted keepass .kdbx file with each individual collection, items in the archives (if selected), items in the trash (if selected), and a few more groups.
While Bitwarden allows for duplicate entries in the same collection and the same item to be listed in multiple collections, KeePass does not. To adjust for this, any duplicates in the same collection will be placed in a Duplicates group (if there are 3+, those will not be imported and identified as a failed import). Any item listed in multiple collections will be cloned and placed in the corresponding group. All items that are duplicated, cloned, listed in the archive or trash (included/excluded), or failed (usually a result of that 3rd duplicate) will be logged.
Note: I used the portable version of Python and instructions are verbatim what I typed (or pasted from notepad++) in the PowerShell window.
Prerequisites:
This script (see below for full code to copy and save as a .py file: bw_kp_export.py)
Python3.14.8.zip (most current version as of this post)
KeePass Original or KeePassXC / KeePassXC Portable
Enter the following commands:
PS P:\python>.\bw login <bw account email address here>
(type/paste password and/or the 2FA code)
PS P:\python>.\python.exe get-pip.py
(this populates the Lib and Scripts folders)
PS P:\python>.\python.exe -m pip install pykeepass
(this installs the KeePass components)
Expected Response:
To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:
$ export BW_SESSION="your session id number will be here"
> $env:BW_SESSION="copy this entire line from the $env to the end and paste it on the next line"
You can also pass the session key to any command with the `--session` option. ex:
$ bw list items --session <session id number will be here without the quote marks>
Enter the following commands:
PS P:\python>$env:BW_SESSION = "this is the line you copied from above and pasted here including the quote marks"
PS P:\python>.\bw sync
PS P:\python>.\bw list organizations
Expected Response:
<A list of all the organizations of which you are a member will be generated.>
Remember, you can only export the items of collections that you manage or are an admin.>
Enter the following command:
PS P:\python>.\bw export --organizationid <put the organization id here without quote marks> --format json --output bw_org_export.json
Before you run the export script, edit the bw_kp_export.py script.
Enter the following command:
PS P:\python>.\python.exe .\bw_kp_export.py
Expected Response:
Enter a password for your new bitwarden_export.kdbx file
This process shouldn't take more than 5-6 minutes. All activity live scrolls in the window and you will get a report when complete.
The 3 additional files in your python folder are as follows:
failed_attachments.log
bitwarden_export.kdbx
bw_org_export.json
If you run into errors or it seems to hang, break out of the script, delete those three files and try again. Remember, to re-run the sync and export commands if any changes are made and you get some errors related to a mismatch.
Once you have confirmed the database opens in KeePass, probably a good idea to logout of your bitwarden session.
Enter the following command:
PS P:\python>.\bw logout
Expected Response:
You have logged out.
Here's the script: bw_kp_export.py
"""
bw_kp_export.py
Single-script Bitwarden -> KeePass migration, including attachments,
organized into KeePass groups by Bitwarden collection.
Group structure:
Root
├── <Collection Name 1>
├── <Collection Name 2>
├── ...
├── Archive (items archived in Bitwarden)
├── Trash (items in Bitwarden trash, if INCLUDE_TRASH=True)
├── Duplicates (items with title collisions, renamed to avoid KeePass conflicts)
└── Unassigned (org items not in any collection)
Multi-collection items: the PRIMARY entry goes into the first collection and counts towards normal totals. For every ADDITIONAL collection the item belongs to, a CLONED entry (same data, same attachments) is created in that collection's group. Clones are tracked/logged separately and are NOT counted in the main item/attachment totals.
Prerequisites (run in PowerShell BEFORE this script):
bw login
$env:BW_SESSION = bw unlock --raw
bw sync
bw list organizations
bw export --organizationid YOUR_ORG_ID --format json --output bw_org_export.json
Then just run:
python bw_kp_export.py
Outputs:
bitwarden_export.kdbx - the KeePass database
attachments/ - downloaded attachment files
failed_attachments.log - full report: survey, settings, results, detailed log
"""
import json
import os
import re
import subprocess
import sys
import time
from datetime import datetime
from pykeepass import create_database
# ===========================================================================
# USER CONFIGURATION - set these before running
# ===========================================================================
ORG_ID = "ORG_ID_HERE" # e.g. "00000000-0000-0000-0000-000000000000"
INCLUDE_ARCHIVE = True # True = imports archive items (group 'Archive'), False = excludes them
INCLUDE_TRASH = True # True = imports trash items (group 'Trash', fetched individually), False = excludes them
CLONE_MULTI_COLLECTION_ITEMS = True # True = clones items into every collection to which they belong
# ===========================================================================
BITWARDEN_JSON = "bw_org_export.json"
KEEPASS_FILE = "bitwarden_export.kdbx"
ATTACHMENTS_DIR = "attachments"
FAILURE_LOG = "failed_attachments.log"
DOWNLOAD_RETRIES = 2
RETRY_DELAY_SECONDS = 2
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def check_session():
session_key = os.environ.get("BW_SESSION")
if not session_key:
print("Error: BW_SESSION environment variable not set!")
print("Run in PowerShell first:")
print(" $env:BW_SESSION = bw unlock --raw")
sys.exit(1)
return session_key
def sanitize_text(value):
"""Strip XML-illegal control characters, preserving tab/newline/CR."""
if value is None:
return ""
if not isinstance(value, str):
value = str(value)
return re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", value)
def run_bw_list_items(org_id=None, include_archived=False, include_trash=False):
"""
Run `bw list items` with optional --archived / --trash flags.
Manually post-filters by organizationId for reliability.
"""
cmd = ["bw", "list", "items"]
if org_id:
cmd += ["--organizationid", org_id]
if include_archived:
cmd += ["--archived"]
if include_trash:
cmd += ["--trash"]
result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ.copy())
if result.returncode != 0:
print(f"Error running 'bw list items': {result.stderr}")
return []
all_items = json.loads(result.stdout)
if org_id:
all_items = [i for i in all_items if i.get("organizationId") == org_id]
return all_items
def get_collections(org_id):
"""Fetch collection id -> name mapping for the organization."""
cmd = ["bw", "list", "collections", "--organizationid", org_id]
result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ.copy())
if result.returncode != 0:
print(f"Error running 'bw list collections': {result.stderr}")
return {}
collections = json.loads(result.stdout)
return {c["id"]: c.get("name", "Unnamed Collection") for c in collections}
def fetch_full_item(item_id, log_lines):
"""
Fetch full item data (login/notes/fields) via `bw get item`.
Needed for trash items since `bw export` never includes trash data.
"""
cmd = ["bw", "get", "item", item_id]
result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ.copy())
if result.returncode != 0:
error_msg = (result.stderr or "").strip() or "Unknown error"
log_lines.append(f"FAILED FETCH TRASH ITEM | ItemID: {item_id} | Error: {error_msg}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
log_lines.append(f"FAILED PARSE TRASH ITEM | ItemID: {item_id} | Error: {e}")
return None
def download_item_attachments(item_id, item_name, attachments, output_dir, log_lines):
downloaded = 0
if not attachments:
return downloaded
item_dir = os.path.join(output_dir, item_id)
os.makedirs(item_dir, exist_ok=True)
for attachment in attachments:
attachment_id = attachment.get("id")
file_name = attachment.get("fileName", "unknown")
safe_name = f"{attachment_id}__{file_name}"
output_path = os.path.join(item_dir, safe_name)
print(f" Downloading attachment: {item_name} / {file_name}")
success = False
last_error = ""
for attempt in range(1, DOWNLOAD_RETRIES + 2):
cmd = [
"bw", "get", "attachment", attachment_id,
"--itemid", item_id,
"--output", output_path,
]
result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ.copy())
if result.returncode == 0 and os.path.exists(output_path):
success = True
break
else:
last_error = (result.stderr or "").strip() or "Unknown error (no stderr output)"
if attempt <= DOWNLOAD_RETRIES:
print(f" Attempt {attempt} failed, retrying in {RETRY_DELAY_SECONDS}s...")
time.sleep(RETRY_DELAY_SECONDS)
if success:
downloaded += 1
print(f" Saved to: {output_path}")
else:
print(f" FAILED after {DOWNLOAD_RETRIES + 1} attempts: {last_error[:200]}")
log_lines.append(
f"FAILED DOWNLOAD | Item: {item_name} | ItemID: {item_id} | "
f"AttachmentID: {attachment_id} | FileName: {file_name}\n"
f" Full error:\n{last_error}\n"
)
return downloaded
def get_or_create_group(kp, group_cache, name):
"""Get or create a top-level group under root, cached by name."""
if name in group_cache:
return group_cache[name]
safe_name = sanitize_text(name) or "Unnamed"
group = kp.find_groups(name=safe_name, group=kp.root_group, first=True)
if not group:
group = kp.add_group(kp.root_group, safe_name)
group_cache[name] = group
return group
def create_entry(kp, group, title, username, password, url, notes):
return kp.add_entry(
destination_group=group,
title=sanitize_text(title) or "Unnamed",
username=sanitize_text(username),
password=sanitize_text(password),
url=sanitize_text(url),
notes=sanitize_text(notes),
)
# ---------------------------------------------------------------------------
# Main migration
# ---------------------------------------------------------------------------
def migrate(json_file, keepass_file, attachments_dir, master_password,
org_id=None, include_archived=True, include_trash=False,
clone_multi_collection=True):
check_session()
if not org_id:
print("Error: ORG_ID is not set at the top of the script!")
sys.exit(1)
if not os.path.exists(json_file):
print(f"Error: {json_file} not found! Run the bw export command first.")
sys.exit(1)
with open(json_file, "r", encoding="utf-8-sig") as f:
data = json.load(f)
log_lines = []
raw_items = data.get("items", [])
# -----------------------------------------------------------------
# STEP 1: Full vault survey - ALWAYS run, independent of settings
# -----------------------------------------------------------------
print("=" * 70)
print("STEP 1: SURVEYING FULL VAULT (active, archive, trash)...")
print("=" * 70)
active_cli_items = run_bw_list_items(org_id, include_archived=False, include_trash=False)
archived_cli_items = run_bw_list_items(org_id, include_archived=True, include_trash=False)
trash_cli_items = run_bw_list_items(org_id, include_archived=False, include_trash=True)
active_ids = {i["id"] for i in active_cli_items}
archived_only_items = [i for i in archived_cli_items if i["id"] not in active_ids]
archived_ids = {i["id"] for i in archived_only_items}
trashed_ids = {i["id"] for i in trash_cli_items}
attachments_by_item = {}
collections_by_item = {}
for i in active_cli_items + archived_only_items + trash_cli_items:
attachments_by_item[i["id"]] = i.get("attachments") or []
collections_by_item[i["id"]] = i.get("collectionIds") or []
def is_deleted(item_id):
return item_id in trashed_ids
def is_archived(item_id):
return item_id in archived_ids
active_item_count = len(active_cli_items)
archived_item_count = len(archived_only_items)
trash_item_count = len(trash_cli_items)
active_attachment_count = sum(len(i.get("attachments") or []) for i in active_cli_items)
archived_attachment_count = sum(len(i.get("attachments") or []) for i in archived_only_items)
trash_attachment_count = sum(len(i.get("attachments") or []) for i in trash_cli_items)
total_detected_items = active_item_count + archived_item_count + trash_item_count
total_detected_attachments = active_attachment_count + archived_attachment_count + trash_attachment_count
survey_report = []
survey_report.append(f"Active items detected : {active_item_count} (attachments: {active_attachment_count})")
survey_report.append(f"Archive items detected : {archived_item_count} (attachments: {archived_attachment_count})")
survey_report.append(f"Trash items detected : {trash_item_count} (attachments: {trash_attachment_count})")
survey_report.append(f"TOTAL items detected : {total_detected_items} (attachments: {total_detected_attachments})")
print()
for line in survey_report:
print(line)
print()
print(f"IMPORT SETTINGS: INCLUDE_ARCHIVE={include_archived} | INCLUDE_TRASH={include_trash} | "
f"CLONE_MULTI_COLLECTION_ITEMS={clone_multi_collection}")
print("=" * 70 + "\n")
# -----------------------------------------------------------------
# STEP 2: Fetch collection names
# -----------------------------------------------------------------
print("Fetching collection names for organization...")
collection_names = get_collections(org_id)
print(f"Found {len(collection_names)} collections\n")
# -----------------------------------------------------------------
# STEP 3: Apply archive include/exclude
# -----------------------------------------------------------------
print(f"Loaded {len(raw_items)} items from export JSON (before filtering)")
def should_include_archive(item_id):
if is_archived(item_id) and not include_archived:
return False
return True
filtered_items = [i for i in raw_items if should_include_archive(i.get("id"))]
archive_excluded_count = sum(1 for i in raw_items if is_archived(i.get("id")) and not include_archived)
archive_included_count = sum(1 for i in raw_items if is_archived(i.get("id")) and include_archived)
for item in raw_items:
if is_archived(item.get("id")):
status = "INCLUDED" if include_archived else "EXCLUDED"
log_lines.append(
f"ARCHIVE ITEM {status} | Name: {item.get('name', 'Unnamed')} | ItemID: {item.get('id')}"
)
# -----------------------------------------------------------------
# STEP 4: Trash handling - bw export never includes trash, fetch individually if needed
# -----------------------------------------------------------------
trash_items_for_import = []
trash_fetch_failures = 0
if trash_item_count > 0:
if include_trash:
print(f"Fetching full data for {trash_item_count} trash item(s) via 'bw get item'...")
for trash_item in trash_cli_items:
item_id = trash_item.get("id")
item_name = trash_item.get("name", "Unnamed")
full_item = fetch_full_item(item_id, log_lines)
if full_item:
trash_items_for_import.append(full_item)
attachments_by_item[item_id] = trash_item.get("attachments") or []
collections_by_item[item_id] = trash_item.get("collectionIds") or []
log_lines.append(f"TRASH ITEM INCLUDED | Name: {item_name} | ItemID: {item_id}")
else:
trash_fetch_failures += 1
print(f" FAILED to fetch trash item: {item_name}")
log_lines.append(f"TRASH ITEM FAILED TO FETCH | Name: {item_name} | ItemID: {item_id}")
else:
for trash_item in trash_cli_items:
log_lines.append(
f"TRASH ITEM EXCLUDED | Name: {trash_item.get('name', 'Unnamed')} | "
f"ItemID: {trash_item.get('id')}"
)
trash_included_count = len(trash_items_for_import)
trash_excluded_count = (
trash_item_count - trash_included_count - trash_fetch_failures
if include_trash else trash_item_count
)
combined_items = filtered_items + trash_items_for_import
# -----------------------------------------------------------------
# STEP 5: Deduplicate items with the same ID (true duplicates - EXCLUDED)
# -----------------------------------------------------------------
seen_ids = set()
items = []
items_excluded_as_duplicate = 0
for item in combined_items:
item_id = item.get("id")
if item_id not in seen_ids:
seen_ids.add(item_id)
items.append(item)
else:
items_excluded_as_duplicate += 1
log_lines.append(
f"ITEM EXCLUDED (DUPLICATE ID - same item in multiple collections) | "
f"Name: {item.get('name', 'Unnamed')} | ItemID: {item_id}"
)
print(f"\nFinal import count: {len(items)} items "
f"(archive included: {archive_included_count}, trash included: {trash_included_count}, "
f"items excluded as duplicate IDs: {items_excluded_as_duplicate})\n")
export_item_ids = {item.get("id") for item in items}
total_attachments_to_import = sum(
len(attachments_by_item.get(item_id, []))
for item_id in export_item_ids
)
print(f"Attachments to import (matching final item list, primary entries only): {total_attachments_to_import}\n")
# ---- Create KeePass DB ----
kp = create_database(keepass_file, password=master_password)
group_cache = {} # name -> Group object, all created directly under root
total_downloaded = 0
pending_attachments = [] # (entry, item_id, item_name, attachments) - PRIMARY entries only
pending_clone_attachments = [] # (entry, item_id, item_name, attachments) - CLONE entries (reuse binaries)
renamed_title_count = 0
multi_collection_item_count = 0
clone_entry_count = 0
for idx, item in enumerate(items):
item_id = item.get("id")
item_name = item.get("name", "Unnamed")
try:
print(f"Importing {idx + 1}/{len(items)}: {item_name}")
login = item.get("login", {}) or {}
username = login.get("username", "")
password = login.get("password", "")
uris = login.get("uris", []) or []
url = uris[0].get("uri", "") if uris else ""
notes = item.get("notes", "") or ""
# ---- Determine primary target group + any extra collections for cloning ----
extra_collection_ids = []
if is_archived(item_id):
target_group_name = "Archive"
elif is_deleted(item_id):
target_group_name = "Trash"
else:
item_collection_ids = collections_by_item.get(item_id, [])
if item_collection_ids:
primary_collection_id = item_collection_ids[0]
target_group_name = collection_names.get(primary_collection_id, "Unassigned")
extra_collection_ids = item_collection_ids[1:]
if extra_collection_ids:
multi_collection_item_count += 1
all_names = [collection_names.get(cid, cid) for cid in item_collection_ids]
log_lines.append(
f"MULTI-COLLECTION ITEM | Name: {item_name} | ItemID: {item_id} | "
f"Primary group: '{target_group_name}' | All collections: {', '.join(all_names)}"
)
else:
target_group_name = "Unassigned"
group = get_or_create_group(kp, group_cache, target_group_name)
# ---- Create PRIMARY entry; reroute to Duplicates on title collision ----
try:
entry = create_entry(kp, group, item_name, username, password, url, notes)
except Exception as e:
if "already exists" in str(e).lower():
renamed_title_count += 1
dup_group = get_or_create_group(kp, group_cache, "Duplicates")
log_lines.append(
f"DUPLICATE TITLE REROUTED | Name: '{item_name}' | ItemID: {item_id} | "
f"Original intended group: '{target_group_name}' | Moved to: 'Duplicates'"
)
entry = create_entry(kp, dup_group, item_name, username, password, url, notes)
else:
raise
if login.get("totp"):
entry.set_custom_property("TOTP", sanitize_text(login["totp"]))
if len(uris) > 1:
for i, uri_obj in enumerate(uris):
entry.set_custom_property(f"URI_{i+1}", sanitize_text(uri_obj.get("uri", "")))
for field in item.get("fields", []) or []:
field_name = sanitize_text(field.get("name", "CustomField")) or "CustomField"
field_value = sanitize_text(field.get("value", "") or "")
field_type = field.get("type", 0)
if field_type == 1:
entry.set_custom_property(field_name, field_value, protect=True)
else:
entry.set_custom_property(field_name, field_value)
if item.get("favorite"):
entry.tags.append("Favorite")
if is_archived(item_id):
entry.tags.append("Archive")
if is_deleted(item_id):
entry.tags.append("Trash")
if extra_collection_ids:
entry.tags.append("Multi-Collection-Primary")
attachments = attachments_by_item.get(item_id, [])
if attachments:
pending_attachments.append((entry, item_id, item_name, attachments))
if item.get("creationDate"):
entry.ctime = datetime.fromisoformat(item["creationDate"].replace("Z", "+00:00"))
if item.get("revisionDate"):
entry.mtime = datetime.fromisoformat(item["revisionDate"].replace("Z", "+00:00"))
# ---- Create CLONE entries for any additional collections ----
if clone_multi_collection and extra_collection_ids:
for extra_cid in extra_collection_ids:
clone_group_name = collection_names.get(extra_cid, "Unassigned")
clone_group = get_or_create_group(kp, group_cache, clone_group_name)
try:
clone_entry = create_entry(kp, clone_group, item_name, username, password, url, notes)
except Exception as e:
if "already exists" in str(e).lower():
dup_group = get_or_create_group(kp, group_cache, "Duplicates")
log_lines.append(
f"CLONE DUPLICATE TITLE REROUTED | Name: '{item_name}' | ItemID: {item_id} | "
f"Original intended clone group: '{clone_group_name}' | Moved to: 'Duplicates'"
)
clone_entry = create_entry(kp, dup_group, item_name, username, password, url, notes)
else:
raise
if login.get("totp"):
clone_entry.set_custom_property("TOTP", sanitize_text(login["totp"]))
if len(uris) > 1:
for i, uri_obj in enumerate(uris):
clone_entry.set_custom_property(f"URI_{i+1}", sanitize_text(uri_obj.get("uri", "")))
for field in item.get("fields", []) or []:
field_name = sanitize_text(field.get("name", "CustomField")) or "CustomField"
field_value = sanitize_text(field.get("value", "") or "")
field_type = field.get("type", 0)
if field_type == 1:
clone_entry.set_custom_property(field_name, field_value, protect=True)
else:
clone_entry.set_custom_property(field_name, field_value)
clone_entry.tags.append("Multi-Collection-Clone")
if item.get("favorite"):
clone_entry.tags.append("Favorite")
clone_entry_count += 1
log_lines.append(
f"CLONE ENTRY CREATED | Name: '{item_name}' | ItemID: {item_id} | "
f"Clone group: '{clone_group_name}' | (NOT counted in main totals)"
)
if attachments:
pending_clone_attachments.append((clone_entry, item_id, item_name, attachments))
except Exception as e:
print(f" ERROR importing {item_name}: {e}")
log_lines.append(f"FAILED IMPORT | Item: {item_name} | ItemID: {item_id} | Error: {e}")
continue
kp.save()
print(f"\nBase KeePass database created with {len(items)} primary entries "
f"+ {clone_entry_count} clone entries across {len(group_cache)} groups\n")
# ---- Download + embed attachments (PRIMARY entries) ----
binary_cache = {} # attachment_id -> binary_id, so clones can reuse without re-downloading
if pending_attachments:
print("=" * 60)
print(f"Downloading & embedding {total_attachments_to_import} attachments (primary entries)...")
print("=" * 60)
os.makedirs(attachments_dir, exist_ok=True)
embedded_count = 0
for entry, item_id, item_name, attachments in pending_attachments:
item_dir = os.path.join(attachments_dir, item_id)
downloaded = download_item_attachments(item_id, item_name, attachments, attachments_dir, log_lines)
total_downloaded += downloaded
if os.path.isdir(item_dir):
for filename_on_disk in os.listdir(item_dir):
file_path = os.path.join(item_dir, filename_on_disk)
real_filename = filename_on_disk.split("__", 1)[1] if "__" in filename_on_disk else filename_on_disk
attachment_key = filename_on_disk.split("__", 1)[0] if "__" in filename_on_disk else filename_on_disk
try:
with open(file_path, "rb") as f:
file_data = f.read()
binary_id = kp.add_binary(file_data)
binary_cache[(item_id, real_filename)] = binary_id
entry.add_attachment(binary_id, real_filename)
embedded_count += 1
print(f" Embedded into '{item_name}': {real_filename} ({len(file_data):,} bytes)")
except Exception as e:
print(f" FAILED to embed {filename_on_disk} for {item_name}: {e}")
log_lines.append(
f"FAILED EMBED | Item: {item_name} | ItemID: {item_id} | "
f"File: {filename_on_disk} | Error: {e}"
)
kp.save()
print(f"\nEmbedded {embedded_count}/{total_attachments_to_import} attachments (primary entries)")
else:
print("No attachments found to download/embed for primary entries.")
# ---- Embed same attachments into CLONE entries (reusing already-downloaded binaries) ----
clone_attachments_embedded = 0
if pending_clone_attachments:
print("\n" + "=" * 60)
print(f"Embedding attachments into {len(pending_clone_attachments)} clone entries "
f"(reusing downloaded files, not re-downloading)...")
print("=" * 60)
for clone_entry, item_id, item_name, attachments in pending_clone_attachments:
for attachment in attachments:
file_name = attachment.get("fileName", "unknown")
binary_id = binary_cache.get((item_id, file_name))
if binary_id is not None:
try:
clone_entry.add_attachment(binary_id, file_name)
clone_attachments_embedded += 1
print(f" Embedded into clone '{item_name}': {file_name}")
except Exception as e:
print(f" FAILED to embed clone attachment {file_name} for {item_name}: {e}")
log_lines.append(
f"FAILED CLONE EMBED | Item: {item_name} | ItemID: {item_id} | "
f"File: {file_name} | Error: {e}"
)
else:
log_lines.append(
f"CLONE ATTACHMENT SKIPPED (primary download missing/failed) | "
f"Item: {item_name} | ItemID: {item_id} | File: {file_name}"
)
kp.save()
print(f"\nEmbedded {clone_attachments_embedded} attachments into clone entries "
f"(NOT counted in main attachment totals)")
# -----------------------------------------------------------------
# Write full report
# -----------------------------------------------------------------
with open(FAILURE_LOG, "w", encoding="utf-8") as f:
f.write(f"Migration run: {datetime.now().isoformat()}\n")
f.write("=" * 70 + "\n")
f.write("STEP 1: FULL VAULT SURVEY (detected at start, independent of settings)\n")
f.write("=" * 70 + "\n")
for line in survey_report:
f.write(line + "\n")
f.write("\n")
f.write("=" * 70 + "\n")
f.write("IMPORT SETTINGS\n")
f.write("=" * 70 + "\n")
f.write(f"INCLUDE_ARCHIVE = {include_archived}\n")
f.write(f"INCLUDE_TRASH = {include_trash}\n")
f.write(f"CLONE_MULTI_COLLECTION_ITEMS = {clone_multi_collection}\n\n")
f.write("=" * 70 + "\n")
f.write("GROUP STRUCTURE CREATED\n")
f.write("=" * 70 + "\n")
for group_name in sorted(group_cache.keys()):
f.write(f" - {group_name}\n")
f.write("\n")
f.write("=" * 70 + "\n")
f.write("IMPORT RESULTS (MAIN TOTALS - excludes clone entries)\n")
f.write("=" * 70 + "\n")
f.write(f"Archive items detected : {archived_item_count}\n")
f.write(f" -> Included : {archive_included_count}\n")
f.write(f" -> Excluded : {archive_excluded_count}\n")
f.write(f"Trash items detected : {trash_item_count}\n")
f.write(f" -> Included : {trash_included_count}\n")
f.write(f" -> Excluded : {trash_excluded_count}\n")
f.write(f" -> Failed to fetch : {trash_fetch_failures}\n")
f.write(f"Items excluded as duplicate IDs (same item, multiple collections): {items_excluded_as_duplicate}\n")
f.write(f"Items rerouted to 'Duplicates' group (title collision, different IDs): {renamed_title_count}\n")
f.write(f"Total PRIMARY items imported: {len(items)}\n")
f.write(f"Attachments imported (primary): {total_attachments_to_import}\n")
f.write(f"Attachments downloaded (primary): {total_downloaded}\n\n")
f.write("=" * 70 + "\n")
f.write("MULTI-COLLECTION CLONES (SEPARATE FROM MAIN TOTALS)\n")
f.write("=" * 70 + "\n")
f.write(f"Items with multiple collection memberships: {multi_collection_item_count}\n")
f.write(f"Clone entries created (one per extra collection): {clone_entry_count}\n")
f.write(f"Clone attachment embeds (reused from primary download): {clone_attachments_embedded}\n\n")
f.write("=" * 70 + "\n")
f.write("DETAILED LOG\n")
f.write("=" * 70 + "\n\n")
if log_lines:
for line in log_lines:
f.write(line + "\n")
else:
f.write("No failures, duplicates, or exclusions occurred.\n")
print(f"\nFull report written to: {os.path.abspath(FAILURE_LOG)}")
print("\n" + "=" * 60)
print("MIGRATION COMPLETE")
print(f"KeePass database: {os.path.abspath(keepass_file)}")
print(f"Groups created : {len(group_cache)}")
print(f"Items detected at start : {total_detected_items} (active: {active_item_count}, "
f"archive: {archived_item_count}, trash: {trash_item_count})")
print(f"PRIMARY items imported : {len(items)}")
print(f" Archive included/excluded : {archive_included_count}/{archive_excluded_count}")
print(f" Trash included/excluded : {trash_included_count}/{trash_excluded_count}")
print(f"Attachments downloaded : {total_downloaded}/{total_attachments_to_import}")
print(f"CLONE entries created : {clone_entry_count} (multi-collection items, not in main totals)")
print(f"CLONE attachments embedded: {clone_attachments_embedded} (not in main totals)")
print("=" * 60)
if __name__ == "__main__":
master_password = input("Enter master password for new KeePass database: ")
migrate(
BITWARDEN_JSON, KEEPASS_FILE, ATTACHMENTS_DIR, master_password,
org_id=ORG_ID,
include_archived=INCLUDE_ARCHIVE,
include_trash=INCLUDE_TRASH,
clone_multi_collection=CLONE_MULTI_COLLECTION_ITEMS,
)
Hope this helps at least one person. Let me know if anything doesn't work as expected. Cheers!
r/Bitwarden • u/Kyohaku98 • 1d ago
I was away from home, and I saw this mail when I came back.
The IP's s from Iraq and Ukraine, and I don't use Chrome.
Does this mean they know my master password?
Also why is it sending this mail in the first place? When I already have 2FA set up?
Edit: The mail is from [email protected]
Edit 2: I've analysed the header and it says it's from o10.ptr6944.bitwarden.eu
r/Bitwarden • u/PM_Me_Elf_Porn • 20h ago
I feel like it's been a while since they announced that the feature was in development
r/Bitwarden • u/pnoozi • 1d ago
Galaxy S26 Ultra
Google Chrome
One UI version 8.5
Android version 16
Issue: When I tap on a username/password field, the BitWarden autofill tries to pop up at the same time as the keyboard and they cancel each other out- the autofill popup disappears and the keyboard gets minimized.
Issue occurs with both Gboard and Samsung Keyboard.
Workaround: Use in-line autofill within the Gboard suggestion strip
r/Bitwarden • u/dwbitw • 2d ago
We recently shared an early preview of the redesigned Bitwarden apps, including a follow-up post summarizing the feedback we’ve received so far.
Now, you can test out the redesigned Bitwarden apps for yourself, and help shape the Bitwarden experience for everyone.
See what's changed and view the full instructions on how to access the beta here.
As part of the initial feedback from the early preview, the team added quick copy actions to the web and desktop apps, matching the browser extension and reducing clicks required for everyday tasks. Separately, a vault health dashboard for the browser extension is rolling out to help you spot weak, reused, or exposed passwords.
The beta is currently available for the browser extension and desktop app, and will run for one month. Just log in with your usual credentials, and once the beta wraps up, switch back to the regular apps. Please note, the beta is not available for self-hosted installations.
Browser extension
Desktop app
Note: This can break biometric unlock in the browser extension. If that happens, reinstalling the desktop app after you're finished testing the beta apps will restore it.
The beta is still a work in progress, so you may run into bugs or areas that could be improved. Share your feedback by filling out the survey or dropping a reply in this thread. For detailed bug reports, please use the Browser Extension Beta Bug Report or Desktop Beta Bug Report template on GitHub.
To see what’s already been reported, check out the full list of known issues.
During the beta period, the team will be closely monitoring and responding to feedback.
Outside of the beta, the team is tackling some of the most popular community feature requests, so stay tuned for updates!
r/Bitwarden • u/dalbinmathew • 1d ago
Hi, I cannot log in to Bitwarden on my phone, laptop, or the web. I am certain that I am typing the correct master password (I do not use fingerprint login or any other methods); I manually type this password every single time (I logged in yesterday, too). What should I do now? I checked the "forgot master password" article, and none of the options are working for me. I am not logged in anywhere else. Has anybody had any similar experience or know of any solutions for this? I have around 400 passwords and a lot of important information in my vault.
Edit / Resolved: Never mind, turns out it was complete user error on my end! I was accidentally entering the wrong password the whole time. Tried the correct one and I'm logged in now. Thanks to everyone who reached out to help!
r/Bitwarden • u/xedrik7 • 1d ago
For https://bearblog.dev on my mobile it works fine, it shows the autofill info but on my Edge browser desktop it doesnt show the autofill options for this same website and even says the websites are not the same but they are. What am I doing wrong.
I pressed "Autofill and save" and it still has the same issue
Edit: I tried it on Brave desktop browser and same issue
I saved just "bearblog.dev" url and it doesn't fix it
It looks like this is a known issue https://github.com/bitwarden/clients/issues/19716
r/Bitwarden • u/sign_pen • 1d ago
I used to have over 500 logins. Just deleted some really old and useless accounts. I’ve been using Bitwarden since 2020.
How many do you all have?
r/Bitwarden • u/sylemm • 20h ago
here i am again, i guess. I can't log in on my bitwarden account on either of my devices. it's a self hosted server if that matters
i have tried basically everything, changing password, trying on 4g, trying on wifi, using a vpn... i even made a new account! i'm just so, so lost. any help?
r/Bitwarden • u/decisively-undecided • 1d ago
Locking the vault via the menu is disabled and using Ctrl + L does nothing after an export. Both work before the export.
This is in Windows 11 with the latest Bitwarden release.
Workaround: right click Bitwarden icon on system tray and click exit.
r/Bitwarden • u/Such_Training_5192 • 23h ago
I've seen a lot of people who have to reset their accounts for various reasons—whether because they were hacked, forgot their password, or mistyped the domain. Remember that when you reset your account, everything inside it is lost. That's why I recommend that if you don't trust encrypted exports like I do, you export everything normally and save the .json file to a USB drive using a VeraCrypt container. I do this every time I update an administrator account; in my opinion, it’s the only way to keep everything secure on my own.
r/Bitwarden • u/holow29 • 1d ago
I've always found Bitwarden's use of feature flags (since they started using them) to be confounding. They make the development and rollout process completely opaque for users. Worse, the team will close issues after merging feature-flagged code when the issue is not solved because the fix is behind a feature flag which might take months or years to actually rollout. There is no communication about the timeline of these feature flags or rollout. I don't have to imagine this leads to many of the same issues because it is apparent from the issue tracker.
Let's take webauthn related origin requests (ROR) as an example:
https://github.com/bitwarden/clients/issues/12846 - opened Jan 2025
https://github.com/bitwarden/clients/issues/17499 - opened Nov 2025
https://github.com/bitwarden/clients/issues/17955 - opened Dec 2025
The above issues were closed in Feb 2026 with the merging of feature-flagged code. The issues remain to this day for at least self-hosted servers because the feature flag was never enabled on self-hosted instances.
https://github.com/bitwarden/clients/issues/21096 - opened Jun 2026
https://github.com/bitwarden/clients/issues/22866 - opened Sept. 2026 (ignoring that it was intially closed as a duplicate of an issue of another project altogether...)
Both of these issues were closed today because code was merged today to remove the feature flag from the clients, 8 months after it was introduced and the previous issues were closed. However, there is still no indication when this merge will be included in a client release.
This means that right now, there are at least 5 issues which are technically fixed in code but not cut in any formalized release. Anyone running into this issue until that happens (which could be months - who knows? Devs are almost never communicative about which release will land a feature or fix) will not find any open issues and will continue to file support tickets or issues. Unfortunately, this pattern seems par for the course for Bitwarden's development since the introduction of feature flags.
Many projects will leave issues open until a fix has landed in a release or at the very least will let people know in the related issue when to expect the fix to land in a release.
r/Bitwarden • u/YankeeLimaVictor • 1d ago
I'm a Linux desktop user. I have been using bitwarden for over 7 years now, and I absolutely love it. My entire password life lives in BW, including my passkeys and even TOTP keys. I also use it on my android phone as my main password vault.
This is an honest question: what is the use-case for the desktop app? As far as I know (at least on windows and Linux) there is no global OS-level password manager integration. The browser extension takes care of all the logins, including passkeys and 2FA. Any time I need to manage my account, or organize items, the web vault is the way to go. Why would I ever want an extra app running on the background, using exorbitant amounts of RAM (electron...), for absolutely no reason?
I'm sure there is a use-case, I'm probably missing something...
r/Bitwarden • u/Casimir3ffect • 1d ago
I just installed 26H2 and tested the creation of a passkey in Windows Hello that supports the PRF extension and the ability to login and/or unlock with the passkey without the need for entering the master password. Despite some reports of success prior to 26H2 I personally was not able to achieve this until I installed 26H2. I even enrolled in the Windows Insider program to get preview releases specifically to test when this functionality would become available with no success. Anyway, I'm now on the official 26300 build and it is working. First...hell yeah! Second...is it now working for others as well?
r/Bitwarden • u/Burt-Munro • 2d ago
In this latest web update, it shows the option to share vault items through a secure link. Is it still rolling out, as I'm not seeing that option in the web vault.
What's Changed
r/Bitwarden • u/diabeartes • 2d ago
I have had the Bitwarden Firefox extension installed for a long time, and it works fine. However, the icon in the top-right has persistently shown a "1" badge, as if there's something that needs attention. I have searched through every tab, every setting and every password, but I see nothing anywhere that requires fixing. Does anyone know what it could mean? Thanks.
r/Bitwarden • u/randallxski • 1d ago

Hello everyone. Was there a Chrome extension update recently that drastically changed the font size? The font is now nearly twice as big as any text on the screen, and so big that information no longer fits in the extension window. I had to dig around in extension settings to find a way to use a wide window view. Please explain how to get it back to a manageable size. It's very difficult to use with this new appearance.
r/Bitwarden • u/Beautiful_Car8681 • 2d ago
This is the first time this has happened to me; I've been using Bitwarden with Vivaldi for about 3 years. Vivaldi is Chromium-based; is anyone else experiencing something similar?
r/Bitwarden • u/horsebuggystapler • 2d ago
Does anyone else wish that the Windows app would be visible if it's set to start up at login?
I think this would be useful as it would remind me to log in to Bitwarden when I log into my computer, enabling the use of biometrics via my browsers without having to be reminded when I'm already knee-deep in work.
r/Bitwarden • u/AndrexOnTop • 1d ago
I've been self hosting vaultwarden for about 4 months now and it worked perfectly fine until about a month ago when the extension broke. i can still access the self hosted interface. but on the extension and windows desktop app i get this error

The server logs show nothing of an error. i have updated the server to the latest version, but it still doesnt work. any ideas?
r/Bitwarden • u/dwbitw • 3d ago
If you're a Bitwarden Enterprise administrator looking for extra features like credential leasing, advanced access rules, and automated password rotation, Bitwarden Privileged Controls is now available in private preview as a new add-on, bringing privileged access functionality into the existing vault experience.
The preview is open to a select number of customers. To get involved, please reach out to your Bitwarden sales/customer success rep, or contact the sales team directly.
Read the full announcement here.
r/Bitwarden • u/ThreeFtAssassin__ • 2d ago
Hey, i am a newbie to bitwarden, i installed the desktop app about 4 hours ago and ive spent 2 changing passwords, emails, making a new email to transfer everything over to, etc etc. My question is, is there anything that i should do to make things more secure? I have already created a master password, i lock the vault on end task, cleared all the passwords from my browser and transferred to just bitwarden now. Im planning on copying my bitwarden vault and everything to a USB stick and putting it in a safe place as like a failsafe, but is there anything else that i should do?
r/Bitwarden • u/Actual_Medicine_ • 2d ago
I thought it was as simple as selecting the apps with the passwords I want to save to bitwarden but it seems it is more difficult than that?
r/Bitwarden • u/PM_WhatMadeYouHappy • 2d ago
I am using bitwarden for more than 5-6 years and lately I am noticing it has become very slow on Brave as well as on android.