SecretSpec 0.19: Moving and importing secrets between providers

domenkozar1 pts0 comments

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,...

provider item secretspec providers production field

Related Articles