SecretSpec 0.19: Moving and importing secrets between providers | SecretSpec<br>Skip to content
SecretSpec 0.19: Moving and importing secrets between providers
Aug 11, 2026<br>Domen Kožar
Secret storage changes as a project grows. Values move from local files to<br>password managers, from one naming convention to another, and sometimes<br>between providers with completely different data models. The application still<br>expects the same API_KEY or DATABASE_URL at the end.
SecretSpec 0.19<br>treats those changes as a normal workflow instead of a one-off migration<br>script.
This release includes:
Provider-specific storage<br>layouts : give each provider its own<br>address, transform stored values, import existing files, and preview exact<br>write references.
Config belongs in secrets : resolve<br>profile-specific config alongside stored secrets, generate ephemeral values,<br>and securely prompt during secretspec run.
Passbolt provider : read and write secrets in a<br>self-hosted Passbolt server, with credentials supplied by another provider<br>when needed.
Faster remote-provider<br>workflows : attach a cache directly to<br>an authoritative provider and batch 1Password field reads.
Smaller improvements : create standalone profiles<br>and install complete pkg-config metadata for native SDK consumers.
Provider-specific storage layouts<br>Section titled “Provider-specific storage layouts”
API_KEY is the name used by your application. In 1Password, the same value<br>might be the token field of an item named old-api-item.
A ref gives SecretSpec this store address. item<br>names the entry. Coordinates such as field, section, and vault locate a<br>value inside structured stores. The providers list still decides which<br>stores to try.
Before 0.19, every provider in a secret’s route received the same ref, even<br>though stores such as 1Password and dotenv organize secrets differently. Now<br>each provider alias can template its usual layout, while refs. handles<br>exceptions:
secretspec.toml[providers]
legacy = "onepassword://Legacy"
production = {
uri = "onepassword://Production",
ref = { item = "{project}-{profile}", field = "{key}" }
local = { uri = "dotenv://.env", ref = { item = "{key}" } }
[profiles.production]
API_KEY = {
description = "API key",
providers = ["production", "local"],
refs = { legacy = { item = "old-api-item", field = "token" } }
production reads the API_KEY field from a -production 1Password<br>item. The local fallback reads the dotenv key API_KEY. If legacy is<br>selected explicitly, refs.legacy reads the token field from old-api-item.
For each provider, refs. takes precedence over the alias’s ref<br>template, which takes precedence over the provider convention. Templates<br>accept {project}, {profile}, and {key} in every address field. Existing<br>route-wide ref declarations remain supported.
Because scoped references also apply to imports, that exception can describe a<br>migration source without joining the normal fallback route:
Terminal windowsecretspec import legacy --profile production --delete-source
This reads from refs.legacy and writes through the production template. It<br>also works between distinct entries in one physical store. SecretSpec rejects<br>the import if both addresses resolve to the same entry.
Transform stored values<br>Section titled “Transform stored values”
Two new secret fields transform a stored value before it reaches the<br>application.
extract selects a value from JSON with an<br>RFC 6901 JSON Pointer:
secretspec.toml[providers]
runtime = "file:///run/secrets"
[profiles.production]
DATABASE_PASSWORD = {
description = "Database password",
providers = ["runtime"],
ref = { item = "application.json" },
extract = { format = "json", pointer = "/database/password" }
JSON strings become their unquoted contents. Numbers, booleans, objects, and<br>arrays keep their JSON representation. Extracted declarations are read-only.<br>set, delete, prompting, generation, and import cannot overwrite the source<br>document.
encoding defines the textual representation in provider storage:
secretspec.toml[profiles.production]
TEXT_CONFIG = { description = "Encoded configuration", encoding = "base64" }
CLIENT_KEYSTORE = {
description = "Binary client keystore",
providers = ["runtime"],
ref = { item = "client.p12.b64" },
encoding = "base64",
as_path = true
Supported encodings are standard Base64, URL-safe Base64, and hexadecimal.<br>Writes encode the logical value. Reads decode the stored value. Decoded UTF-8<br>can be returned directly. Set as_path = true to materialize arbitrary bytes<br>in a file.
Transforms run in this order:
provider or cache → encoding decode → JSON extraction → as_path
This allows, for example, one declaration to decode a Base64-encoded JSON<br>document and select one field from it.
Import without reshaping the source<br>Section titled “Import without reshaping the source”
File stores one plaintext UTF-8 file per secret beneath<br>a required root. Convention paths use {project}/{profile}/{key}. ref.item<br>selects an existing relative path,...