basecamp/kamal · error · RuntimeError

Could not find #{missing_items.join(", ")} in LastPass

Error message

Could not find #{missing_items.join(", ")} in LastPass

What it means

Raised in LastPass#fetch_secrets after `lpass show ... --json` SUCCEEDED: the JSON is parsed into a {fullname => password} map, then `secrets - results.keys` is computed. If any requested name was not returned under an identical key (the item's 'fullname'), the missing list is joined into this RuntimeError. It is a post-fetch completeness check — the failure mode is name mismatch between your secrets list and the returned fullnames, not a CLI failure.

Source

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

    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

        if (missing_items = secrets - results.keys).any?
          raise RuntimeError, "Could not find #{missing_items.join(", ")} in LastPass"
        end
      end
    end

    def check_dependencies!
      raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
    end

    def cli_installed?
      `lpass --version 2> /dev/null`
      $?.success?
    end
end

View on GitHub (pinned to eee0083b38)

Solutions

  1. List the exact fullname: `lpass ls --format "%an"` (or plain `lpass ls`) and copy the path-qualified name into your kamal secrets list (e.g. 'Shared-Prod/RAILS_MASTER_KEY').
  2. If you prefer short names, move the entry to the vault root (no group) in the LastPass UI so its fullname equals the name.
  3. Use the numeric ID as the requested key — IDs are stable across renames and folder moves.

Example fix

# before
secrets: [ "DB_PASSWORD" ]           # lives in group 'Prod'
# -> RuntimeError: Could not find DB_PASSWORD in LastPass

# after: use the fullname as printed by `lpass ls`
secrets: [ "Prod/DB_PASSWORD" ]
Defensive patterns

Strategy: validation

Validate before calling

require "open3"
require "json"

def lpass_fullnames(names)
  names.map do |n|
    out, _err, st = Open3.capture3("lpass", "show", n, "--json")
    next nil unless st.success?
    items = JSON.parse(out)
    items.flat_map { |i| i["fullname"] }
  end.compact.flatten
end

missing = names - lpass_fullnames(names)
abort "LastPass fullname mismatch for: #{missing.join(', ')} — use paths from `lpass ls`" if missing.any?

Try / catch

begin
  secrets = adapter.fetch(names, account: ACCOUNT)
rescue RuntimeError => e
  if (m = e.message.match(/Could not find (.+) in LastPass/))
    raise "#{m[1]} not returned by lpass: request the path-qualified fullname (see `lpass ls`) or move the entry to vault root"
  end
  raise
end

Prevention

When it happens

Trigger: adapter.fetch([...]) where lpass returns items whose 'fullname' differs from the requested string: entry stored in a group/shared folder has fullname 'GroupName/EntryName' while you requested 'EntryName'; item type mismatch (site URL vs name); duplicates where lpass returns only one match; or requesting a name that exists but whose returned fullname includes a path prefix.

Common situations: Shared-folder or grouped entries referenced by short name; renaming a folder in LastPass so fullnames shift; mixing ID-based and name-based references across team members.

Related errors


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