basecamp/kamal · error · RuntimeError

Failed to login to LastPass

Error message

Failed to login to LastPass

What it means

Raised in LastPass#login when `lpass login <account>` exits non-zero. The adapter first checks loggedin? by comparing `lpass status --color never` output to "Logged in as <account>."; if that exact string does not match, it attempts an interactive lpass login, which requires a TTY and pinentry for the master password. Any mismatch or non-interactive failure triggers this RuntimeError.

Source

Thrown at lib/kamal/secrets/adapters/last_pass.rb:6

class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
  private
    def login(account)
      unless loggedin?(account)
        `lpass login #{account.shellescape}`
        raise RuntimeError, "Failed to login to LastPass" unless $?.success?
      end
    end

    def loggedin?(account)
      `lpass status --color never`.strip == "Logged in as #{account}."
    end

    def fetch_secrets(secrets, from:, account:, session:)
      secrets = prefixed_secrets(secrets, from: from)
      items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
      raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?

      items = JSON.parse(items)

      {}.tap do |results|
        items.each do |item|
          results[item["fullname"]] = item["password"]
        end

View on GitHub (pinned to eee0083b38)

Solutions

  1. Log in once interactively on the machine as the SAME account: `LPASS_DISABLE_PINENTRY=1 lpass login user@example.com` (reads password from stdin) or normal `lpass login`, so `lpass status --color never` prints exactly 'Logged in as user@example.com.'.
  2. Ensure --account matches the logged-in account byte-for-byte (the adapter's loggedin? does an exact string compare including the trailing period).
  3. For automation, use lpass's non-interactive login: `echo "$LPASS_PASSWORD" | LPASS_DISABLE_PINENTRY=1 lpass login user@example.com` in a pre-step before kamal runs.
  4. Install/verify pinentry if interactive login aborts (brew install pinentry, set LPASS_PINENTRY).

Example fix

# before: CI has no lpass session
#   -> RuntimeError: Failed to login to LastPass

# after: non-interactive pre-login step in CI
- run: echo "$LPASS_PASSWORD" | LPASS_DISABLE_PINENTRY=1 lpass login team@company.com
- run: kamal secrets pull --account team@company.com
Defensive patterns

Strategy: try-catch

Validate before calling

require "open3"

def lpass_logged_in_as?(account)
  out, _err, _st = Open3.capture3("lpass", "status", "--color", "never")
  out.strip == "Logged in as #{account}."
end

abort "Not logged into LastPass as #{ACCOUNT}" unless lpass_logged_in_as?(ACCOUNT)

Try / catch

begin
  secrets = adapter.fetch(names, account: ACCOUNT)
rescue RuntimeError => e
  if e.message == "Failed to login to LastPass"
    raise "lpass login failed: pre-authenticate (LPASS_DISABLE_PINENTRY=1 lpass login #{ACCOUNT}) and re-run"
  end
  raise
end

Prevention

When it happens

Trigger: adapter.fetch(secrets, account: 'user@example.com') when: not logged in at all (lpass status shows 'Not logged in'); logged in as a DIFFERENT account than --account (string mismatch causes a pointless re-login attempt); lpass login cannot prompt (no TTY/pinentry in CI/ssh); or the master password entry fails/aborts. Note $?.success? is checked after the backtick, so a failed or cancelled login raises.

Common situations: CI containers with no pinentry and no cached session (~/.lpass); account email typo or changed corporate email so status string never matches; trusted-machine session expired; running kamal from a script where lpass cannot open the prompt and exits non-zero.

Related errors


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