basecamp/kamal · error · RuntimeError

Missing project or config from '--from=project/config' optio

Error message

Missing project or config from '--from=project/config' option

What it means

Raised in Doppler#secrets_get_flags when no service token is set (ENV['DOPPLER_TOKEN'] absent or not dp.st-prefixed) and the first requested secret does not decompose into project/config via split('/'). Without a service token, the doppler CLI needs -p and -c flags, which kamal derives from the `--from=project/config` option (Base#prefixed_secrets joins from + name). If either component is nil, the options cannot be built and this RuntimeError fires before any CLI call.

Source

Thrown at lib/kamal/secrets/adapters/doppler.rb:38

      secrets = prefixed_secrets(secrets, from: from)
      flags = secrets_get_flags(secrets)

      secret_names = secrets.collect { |s| s.split("/").last }

      items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{flags}`
      raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?

      items = JSON.parse(items)

      items.transform_values { |value| value["computed"] }
    end

    def secrets_get_flags(secrets)
      unless service_token_set?
        project, config, _ = secrets.first.split("/")

        unless project && config
          raise RuntimeError, "Missing project or config from '--from=project/config' option"
        end

        project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
      end
    end

    def service_token_set?
      ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
    end

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

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

View on GitHub (pinned to eee0083b38)

Solutions

  1. Pass a complete from option with both slugs: `kamal secrets pull --from=myproject/myconfig` (or set the equivalent in your deploy configuration).
  2. Or export a Doppler service token so project/config flags are unnecessary: export DOPPLER_TOKEN=dp.st.xxxx.
  3. Verify the slugs: project and config come from the Doppler dashboard URLs (e.g. /projects/<project>/config/<config>).

Example fix

# before
kamal secrets pull --from=prod            # only one segment

# after
kamal secrets pull --from=myblog/prod     # project/config
# or: export DOPPLER_TOKEN="dp.st.1...." and omit --from
Defensive patterns

Strategy: validation

Validate before calling

def valid_doppler_from?(from)
  return true if ENV["DOPPLER_TOKEN"]&.start_with?("dp.st.") # service token: --from not required
  from.to_s.split("/").length == 2 # "project/config"
end

abort "Pass --from=project/config or set a dp.st.* DOPPLER_TOKEN" unless valid_doppler_from?(ARGV[0])

Try / catch

begin
  adapter.fetch(names, from: from_option)
rescue RuntimeError => e
  retry_with_from = e.message.include?("Missing project or config")
  raise e unless retry_with_from && from_option.nil?
  adapter.fetch(names, from: "myproject/prod") # explicit correct from
end

Prevention

When it happens

Trigger: adapter.fetch(secrets, from: nil) or from: 'project-only' with no dp.st.* token in ENV: secrets.first.split("/") yields only one part, so project or config is nil; also triggered when from: contains extra segments in the wrong order. Note only the FIRST secret is inspected, so a valid first secret can mask bad later ones (they then fail at fetch_secrets instead).

Common situations: Forgetting `--from=myproject/myconfig` when calling `kamal secrets pull`; setting DOPPLER_TOKEN to a personal dp.pt token (service_token_set? false) and assuming --from is optional; passing from: 'prod' instead of 'blog/prod' in deploy.yml.

Related errors


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