{"record":{"id":"ab7fca7d28054a6e","repo":"basecamp/kamal","slug":"failed-to-login-to-and-unlock-bitwarden","errorCode":null,"errorMessage":"Failed to login to and unlock Bitwarden","messagePattern":"Failed to login to and unlock Bitwarden","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"lib/kamal/secrets/adapters/bitwarden.rb","lineNumber":16,"sourceCode":"class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base\n  private\n    def login(account)\n      status = run_command(\"status\")\n\n      if status[\"status\"] == \"unauthenticated\"\n        run_command(\"login #{account.shellescape}\", raw: true)\n        status = run_command(\"status\")\n      end\n\n      if status[\"status\"] == \"locked\"\n        session = run_command(\"unlock --raw\", raw: true).presence\n        status = run_command(\"status\", session: session)\n      end\n\n      raise RuntimeError, \"Failed to login to and unlock Bitwarden\" unless status[\"status\"] == \"unlocked\"\n\n      run_command(\"sync\", session: session, raw: true)\n      raise RuntimeError, \"Failed to sync Bitwarden\" unless $?.success?\n\n      session\n    end\n\n    def fetch_secrets(secrets, from:, account:, session:)\n      {}.tap do |results|\n        items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|\n          item_json = run_command(\"get item #{item.shellescape}\", session: session, raw: true)\n          raise RuntimeError, \"Could not read #{item} from Bitwarden\" unless $?.success?\n          item_json = JSON.parse(item_json)\n          if fields.any?\n            results.merge! fetch_secrets_from_fields(fields, item, item_json)\n          elsif item_json.dig(\"login\", \"password\")\n            results[item] = item_json.dig(\"login\", \"password\")\n          elsif item_json[\"fields\"]&.any?","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/basecamp/kamal/blob/eee0083b38661c3707c6b6052cc89e85038a096c/lib/kamal/secrets/adapters/bitwarden.rb#L1-L34","documentation":"The Bitwarden adapter drives the bw CLI through a status → login → unlock cycle using the --account email; if the final `bw status` is still not \"unlocked\", it raises RuntimeError. Causes include a failed master-password prompt (login/unlock are interactive), wrong credentials, required 2FA, or an invalid BW_SESSION.","triggerScenarios":"kamal secrets fetch -a bitwarden --account me@example.com ... where the interactive `bw login`/`bw unlock` prompt fails or is unavailable (non-interactive CI shell); account email not matching the Bitwarden account; 2FA challenge on login; a stale BW_SESSION env var so unlock returns an unusable key.","commonSituations":"Running kamal secrets fetch in CI where bw cannot prompt for the master password; bw logged into a different account than --account; 2FA enabled and no session pre-provisioned; expired session on a long-lived agent.","solutions":["In an interactive shell, run bw status; if locked run bw unlock, export BW_SESSION, and retry — Kamal will then reuse the unlocked session","For CI/non-interactive use, unlock once and export BW_SESSION as a secure CI variable so the adapter finds the vault already unlocked","Make sure --account is the exact Bitwarden account email that is logged in (bw status shows it)","If 2FA blocks scripted login, pre-provision BW_SESSION (bw unlock once) instead of letting Kamal log in"],"exampleFix":"# before (CI, non-interactive)\nkamal secrets fetch -a bitwarden --account me@example.com RAILS_MASTER_KEY\n# => RuntimeError: Failed to login to and unlock Bitwarden\n\n# fix (interactive once, then CI)\nbw unlock                    # prints: export BW_SESSION=\"xxx...\"\nexport BW_SESSION=\"xxx...\"   # or store as CI secret\nkamal secrets fetch -a bitwarden --account me@example.com RAILS_MASTER_KEY","handlingStrategy":"validation","validationCode":"# Ensure the vault is usable non-interactively before invoking kamal\nstatus = JSON.parse(`bw status 2>/dev/null`)\ncase status[\"status\"]\nwhen \"unlocked\" then nil\nwhen \"locked\"   then abort \"run `bw unlock` and export BW_SESSION first\"\nelse                 abort \"run `bw login #{status.dig('user', 'email')}` first (or fix 2FA/session)\"\nend","typeGuard":"def bitwarden_ready?\n  JSON.parse(`bw status 2>/dev/null`)['status'] == 'unlocked'\nrescue StandardError\n  false\nend","tryCatchPattern":"begin\n  adapter.fetch(names, account: account)\nrescue RuntimeError => e\n  if e.message.include?(\"Failed to login to and unlock Bitwarden\")\n    abort \"Bitwarden locked: run `bw unlock`, export BW_SESSION, and retry\"\n  end\n  raise\nend","preventionTips":["In CI, store a pre-unlocked BW_SESSION as a secret variable instead of relying on interactive login","Ensure the --account email matches the account bw is logged into (check bw status)","Script a `bw status` pre-check in deploy pipelines for a clear, early error message"],"tags":["kamal","secrets","bitwarden","authentication","cli"],"backgroundTag":"vault-authentication-failed","analyzedSha":"eee0083b38661c3707c6b6052cc89e85038a096c","analyzedAt":"2026-08-21T15:17:22.045Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}