Skip to content

SecretSpec 0.18: Secret lifecycle, Bitwarden, Keeper, AWS Parameter Store, and Swift

SecretSpec 0.18 ships:

The new SecretSpec document-and-keyhole logo

SecretSpec has always kept the declaration in secretspec.toml separate from the stored value. 0.18 brings both sides of that lifecycle into the CLI.

secretspec add adds a declaration to the selected profile while preserving the manifest’s comments, formatting, and unrelated tables:

Terminal window
$ secretspec add STRIPE_API_KEY --description "Stripe API access token"
✓ Added secret 'STRIPE_API_KEY' to profile 'default' in secretspec.toml
Set its value with: secretspec set STRIPE_API_KEY --profile default
$ secretspec set STRIPE_API_KEY
Enter value for STRIPE_API_KEY: ********
✓ Secret 'STRIPE_API_KEY' saved to keyring (profile: default)

add never asks for or stores the value. The declaration can be reviewed and committed before each developer or deployment supplies its own value.

secretspec delete does the inverse on the storage side: it removes a value without changing the declaration. The next check therefore reports the secret as missing instead of quietly removing the application’s requirement:

Terminal window
$ secretspec delete STRIPE_API_KEY
Deleted 'STRIPE_API_KEY'
Deleted 1 secret value; 0 already absent

Deletion is idempotent, invalidates an associated cache entry, and follows the same primary-write-provider routing as set. delete --all requires an interactive confirmation, or an explicit --yes in non-interactive use.

Provider migrations can now remove each source value after proving the move succeeded:

Terminal window
secretspec import dotenv:~/.config/payments/.env --delete-source

import --delete-source reads the destination back and compares it with the source before deleting anything. An identical value already at the destination is safe to remove from the source; a conflicting value leaves the source intact. SecretSpec also rejects a source without deletion support before writing the destination and recognizes equivalent provider spellings as the same store, so a migration cannot delete the value it just wrote through another alias.

Together these commands keep the distinction explicit: add changes what the application declares, set and delete change one environment’s stored value, and import --delete-source moves that value between stores.

The first SecretSpec command in an existing project is often secretspec init --from .env. In 0.18, init --from accepts every provider that can discover declarations, including age, AWS Parameter Store, and Bitwarden Password Manager.

Hierarchical stores also receive an explicit project and profile so SecretSpec looks only inside the namespace the new manifest will use:

Terminal window
$ secretspec init \
--from 'awsps://production@us-east-1?template=/{profile}/{project}/{key}' \
--project payments \
--profile production
✓ Created secretspec.toml with 12 secrets

For a password-manager vault, scope discovery to the collection and item type that belong to the application:

Terminal window
$ secretspec init --from 'bw://Acme%20Inc@dev-secrets?type=login'
✓ Created secretspec.toml with 8 secrets

Discovery writes names and generated descriptions, never secret values. After reviewing the manifest, keep the discovered provider as the profile’s source or use secretspec import to copy the now-declared values somewhere else.

0.18 brings SecretSpec to 24 providers, with four additions spanning personal password managers, machine-oriented vaults, and cloud parameter storage.

Bitwarden Password Manager is separate from the existing Bitwarden Secrets Manager provider. The new bw:// provider uses the official bw CLI to read and write regular vault items: logins, secure notes, cards, identities, and SSH keys. It can address organizations and collections by name or ID, restrict a provider to one item type or field, discover declarations, and point ref secrets at existing items. A ?server= guard verifies that the CLI is logged into the expected self-hosted instance instead of silently reading the wrong vault.

Keeper Secrets Manager uses Keeper’s official Rust SDK, so it does not need a separate CLI. A keeper://FOLDER_UID provider reads, writes, batches, and deletes convention records shared with a KSM application; refs can select an existing record and field. Its client configuration can come from KSM_CONFIG, a protected configuration file, or SecretSpec provider credentials.

AWS Systems Manager Parameter Store stores every value as a KMS-encrypted SecureString. The awsps:// provider uses the standard AWS credential and region chains and supports shared-config profiles, hierarchy prefixes, complete {project} / {profile} / {key} templates, customer-managed KMS keys, and parameter tiers. Refs can select an existing parameter by name, version, label, or ARN; unversioned name refs are writable, while pinned revisions remain read-only. Its bounded hierarchy discovery uses GetParametersByPath without decrypting values.

Dashlane reads secrets, secure notes, and logins through the dcli CLI. It is intentionally read-only because dcli cannot create or edit vault items. A ref can address an existing item by title or identifier and select one of its fields. CI can provide DASHLANE_SERVICE_DEVICE_KEYS directly or source the same service_device_keys input from another SecretSpec provider.

A project can route different secrets through any combination of them:

secretspec.toml
[providers]
team_vault = "bw://Acme%20Inc@dev-secrets"
keeper_ci = "keeper://SHARED_FOLDER_UID"
parameters = "awsps://production@us-east-1?prefix=/platform"
dashlane_notes = "dashlane://note"

Provider choice still stays outside application code. The CLI and every SDK resolve the same declaration regardless of which of these aliases supplies a value.

The new Swift SDK brings the shared SecretSpec resolver to macOS 12 or later on Intel and Apple silicon. Add the repository as a Swift package:

dependencies: [
.package(
url: "https://github.com/cachix/secretspec",
from: "0.18.0"
),
]

Then use the same builder vocabulary as the other SDKs:

import SecretSpec
let resolved = try SecretSpec.builder()
.withProfile("production")
.withScope("api")
.withReason("boot web app")
.load()
defer { try? resolved.close() }
print(resolved.secrets["DATABASE_URL"]?.get() ?? "")
try resolved.setAsEnvironment()

The SDK exposes fluent and one-shot resolution, typed failures, value-free preflight reports, scopes, provenance, environment export, and JSON input for generated Swift models. Calling close() deterministically removes temporary files created for as_path secrets.

The SwiftPM release contains a checksummed XCFramework with the Rust resolver, so an application needs neither a Rust toolchain nor a separately installed SecretSpec library.

Vault and OpenBao deployments do not always use the default approle and jwt mount names. Their provider URIs can now choose a mount relative to /v1/auth:

vault://vault.example.com:8200/secret?auth=approle&auth_mount=platform-approle
openbao://bao.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=deploy

AppRole authentication can omit secret_id when the server role is configured with bind_secret_id=false. JWT authentication can likewise omit its role when the selected mount has a server-configured default_role. Explicit URI, environment, or provider-credential inputs continue to take precedence.

Terminal window
cargo install secretspec

0.18 also makes two local workflows less dependent on machine-specific setup:

  • custom dotenv paths accept a leading ~, resolved to the current user’s home directory;
  • Linux keyring builds use keyring 4’s Rust-native Secret Service transport, so SecretSpec binaries no longer require system libdbus.

Existing manifests and providers continue to work unchanged. The new commands, providers, and SDK are opt-in.

See the full changelog for every change and fix in this release.

Questions or feedback? Join us on Discord.