{"record":{"id":"a3dc95e1cbb76323","repo":"basecamp/kamal","slug":"could-not-read-secrets-from-doppler","errorCode":null,"errorMessage":"Could not read #{secrets} from Doppler","messagePattern":"Could not read #(.+?) from Doppler","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"lib/kamal/secrets/adapters/doppler.rb","lineNumber":26,"sourceCode":"      unless loggedin?\n        `doppler login -y`\n        raise RuntimeError, \"Failed to login to Doppler\" unless $?.success?\n      end\n    end\n\n    def loggedin?\n      `doppler me --json 2> /dev/null`\n      $?.success?\n    end\n\n    def fetch_secrets(secrets, from:, **)\n      secrets = prefixed_secrets(secrets, from: from)\n      flags = secrets_get_flags(secrets)\n\n      secret_names = secrets.collect { |s| s.split(\"/\").last }\n\n      items = `doppler secrets get #{secret_names.map(&:shellescape).join(\" \")} --json #{flags}`\n      raise RuntimeError, \"Could not read #{secrets} from Doppler\" unless $?.success?\n\n      items = JSON.parse(items)\n\n      items.transform_values { |value| value[\"computed\"] }\n    end\n\n    def secrets_get_flags(secrets)\n      unless service_token_set?\n        project, config, _ = secrets.first.split(\"/\")\n\n        unless project && config\n          raise RuntimeError, \"Missing project or config from '--from=project/config' option\"\n        end\n\n        project_and_config_flags = \"-p #{project.shellescape} -c #{config.shellescape}\"\n      end\n    end\n","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/basecamp/kamal/blob/eee0083b38661c3707c6b6052cc89e85038a096c/lib/kamal/secrets/adapters/doppler.rb#L8-L44","documentation":"Raised in Kamal::Secrets::Adapters::Doppler#fetch_secrets when the backtick command `doppler secrets get <names> --json <flags>` exits non-zero. Login already succeeded (or was skipped via service token); this failure is about reading the requested secret names from the resolved Doppler project/config. The message interpolates the full requested list, including any `from` prefix.","triggerScenarios":"adapter.fetch(...) after successful auth when: one or more secret names do not exist in the project/config (doppler secrets get fails on unknown names); -p/-c flags derived from `--from=project/config` point at a nonexistent project or config; the service token lacks read scope for the project; Doppler API unreachable (network/proxy); or a mixed secrets list where the first entry has project/config but later entries resolve against a different config.","commonSituations":"Secret renamed or deleted in Doppler but still listed in deploy.yml secrets; --from default in deploy.yml pointing at an old project slug; personal token works locally but CI service token is scoped to another workplace; corporate proxy blocking api.doppler.com.","solutions":["Run the same command the adapter builds to see the real CLI error: `doppler secrets get <NAME> -p <project> -c <config> --json`.","Verify every name in your kamal secrets list exists in that project/config (`doppler secrets list -p X -c Y`) and fix typos/renames in deploy.yml.","Confirm --from matches the actual Doppler project/config slugs (not names) shown in the dashboard.","If using DOPPLER_TOKEN, check the service token's project/config scope and workplace access."],"exampleFix":"# before: deploy.yml requests a secret Doppler does not have\n#   secrets:\n#     - RAILS_MASTER_KEY   # absent in doppler project/blog/prod\n\n# after: add it in Doppler (or remove from deploy.yml)\n#   doppler secrets set RAILS_MASTER_KEY=... -p blog -c prod\nkamal secrets pull","handlingStrategy":"try-catch","validationCode":"require \"open3\"\n\ndef doppler_secrets_exist?(names, project:, config:)\n  out, _err, status = Open3.capture3(\"doppler\", \"secrets\", \"get\", *names, \"-p\", project, \"-c\", config, \"--json\")\n  return false unless status.success?\n  JSON.parse(out).keys.slice_when { true }.to_a.flatten # available names\n  JSON.parse(out).key?(names.first)\nrescue Errno::ENOENT\n  false\nend","typeGuard":null,"tryCatchPattern":"begin\n  secrets = adapter.fetch(%w[RAILS_MASTER_KEY], from: \"blog/prod\")\nrescue RuntimeError => e\n  if e.message.include?(\"Could not read\") && e.message.include?(\"from Doppler\")\n    raise \"Doppler fetch failed: verify each name exists in project/config — run `doppler secrets list -p blog -c prod`\"\n  end\n  raise\nend","preventionTips":["Keep deploy.yml's secret list and Doppler's project/config in sync via a lint step that cross-checks `doppler secrets list`.","Prefer a service token scoped to exactly the deploy project/config so wrong-project reads fail loudly and early.","Print the adapter's exact CLI command in debug logs to speed up diagnosis."],"tags":["ruby","kamal","doppler","secrets","secret-not-found","project-config","cli-exit-code"],"backgroundTag":"secret-fetch-failed","analyzedSha":"eee0083b38661c3707c6b6052cc89e85038a096c","analyzedAt":"2026-08-21T15:17:22.045Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}