basecamp/kamal · error · RuntimeError

Failed to login to and unlock Bitwarden

Error message

Failed to login to and unlock Bitwarden

What it means

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.

Source

Thrown at lib/kamal/secrets/adapters/bitwarden.rb:16

class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
  private
    def login(account)
      status = run_command("status")

      if status["status"] == "unauthenticated"
        run_command("login #{account.shellescape}", raw: true)
        status = run_command("status")
      end

      if status["status"] == "locked"
        session = run_command("unlock --raw", raw: true).presence
        status = run_command("status", session: session)
      end

      raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"

      run_command("sync", session: session, raw: true)
      raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?

      session
    end

    def fetch_secrets(secrets, from:, account:, session:)
      {}.tap do |results|
        items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|
          item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
          raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
          item_json = JSON.parse(item_json)
          if fields.any?
            results.merge! fetch_secrets_from_fields(fields, item, item_json)
          elsif item_json.dig("login", "password")
            results[item] = item_json.dig("login", "password")
          elsif item_json["fields"]&.any?

View on GitHub (pinned to eee0083b38)

Solutions

  1. In an interactive shell, run bw status; if locked run bw unlock, export BW_SESSION, and retry — Kamal will then reuse the unlocked session
  2. For CI/non-interactive use, unlock once and export BW_SESSION as a secure CI variable so the adapter finds the vault already unlocked
  3. Make sure --account is the exact Bitwarden account email that is logged in (bw status shows it)
  4. If 2FA blocks scripted login, pre-provision BW_SESSION (bw unlock once) instead of letting Kamal log in

Example fix

# before (CI, non-interactive)
kamal secrets fetch -a bitwarden --account me@example.com RAILS_MASTER_KEY
# => RuntimeError: Failed to login to and unlock Bitwarden

# fix (interactive once, then CI)
bw unlock                    # prints: export BW_SESSION="xxx..."
export BW_SESSION="xxx..."   # or store as CI secret
kamal secrets fetch -a bitwarden --account me@example.com RAILS_MASTER_KEY
Defensive patterns

Strategy: validation

Validate before calling

# Ensure the vault is usable non-interactively before invoking kamal
status = JSON.parse(`bw status 2>/dev/null`)
case status["status"]
when "unlocked" then nil
when "locked"   then abort "run `bw unlock` and export BW_SESSION first"
else                 abort "run `bw login #{status.dig('user', 'email')}` first (or fix 2FA/session)"
end

Type guard

def bitwarden_ready?
  JSON.parse(`bw status 2>/dev/null`)['status'] == 'unlocked'
rescue StandardError
  false
end

Try / catch

begin
  adapter.fetch(names, account: account)
rescue RuntimeError => e
  if e.message.include?("Failed to login to and unlock Bitwarden")
    abort "Bitwarden locked: run `bw unlock`, export BW_SESSION, and retry"
  end
  raise
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21). Data as JSON: /api/errors/ab7fca7d28054a6e. Report an issue: GitHub.