All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- The published
secretspeccrate includes every fixture its tests need, so its test suite runs from the crates.io source without the repository. - The C resolver client reports why opening a session failed when the endpoint
writes non-protocol text before its first response, instead of sometimes
returning
UNAVAILABLEwithout an error message.
SecretSpec 0.21 lets other programs request only the secrets they need, lets providers run outside SecretSpec, and preserves binary values from storage to the consuming application. It also adds Doppler and Tailscale Setec providers, project-wide provider defaults, and a Claude Code credential integration.
-
Cached values use a new format to preserve binary secrets. Existing cache entries remain readable by 0.21. SecretSpec 0.20 and earlier cannot read new entries; they fall back to the original provider and leave those entries untouched. Avoid sharing a cache store between 0.20 and 0.21 clients.
-
Command-generated values now include all stdout bytes, including spaces and a final newline. For example,
openssl rand -hex 32produces a value ending in\n; trim it in the command if that is unwanted. Existing stored values do not change. Empty or whitespace-only output is still rejected. -
Piped
secretspec setinput remains trimmed text. Use--from-file -when whitespace or non-UTF-8 bytes must be preserved from stdin. -
secretspec getno longer adds a newline when writing to a pipe or file. This makes redirects preserve the exact value. Terminal output still ends in a newline; shell command substitution behaves as before. -
Rust secrets are byte values.
Secrets::setnow takesSecretBytes. Replaceset(name, Some(value))withset_text(name, &value)for text andset(name, None)withprompt_and_set(name). Custom providers must update theirget,set, andget_manyimplementations to useSecretBytes;Provider::namenow returns&str, andgenerator::generatereturns bytes. -
Building the new C resolver client requires system yyjson. Meson finds it through pkg-config and CMake through
find_package(yyjson CONFIG). Static consumers must link-lyyjson;secretspec-resolver.pcrecords the dependency.
-
Resolve secrets from another process.
secretspec serveexposes a private, versioned stdio protocol so an application can request one declared secret without loading the whole manifest into its own process. It can return a value or a temporary file that is cleaned up when the session ends. Clients can also answer prompts, store or delete declared secrets on the same provider route, and receive expiry and revision information for their own caches.secretspec serve --read-onlyrefuses operations that would write, including generation and prompting when they would store a value. Applications can use the Rustsecretspec-ipccrate (async or blocking), the Clibsecretspec-resolverlibrary, or the protocol directly. Rust clients can also connect over SSH or an existing authenticated stream. -
Install providers outside the SecretSpec release cycle. Trusted external executables can register as providers and serve a versioned provider protocol. They can describe their capabilities, request credentials for their own URI, and return an interaction reference that appears in the audit log for an approval flow.
secretspec config provider loginandsecretspec setcan collect missing credentials. SecretSpec passes each endpoint only the base environment plus the variables it declared, keeping other providers' tokens separate. -
Keep binary secrets intact. Providers, fallback chains, imports, and the cache now preserve arbitrary bytes. Use
secretspec set NAME --from-file FILE(or--from-file -for stdin) to store exact bytes, andas_path = trueto pass them to an application through a temporary file. The file, systemd credential, environment, keyring, Google Secret Manager, Kubernetes, and Scaleway providers support binary values; AWS Secrets Manager also supportsSecretBinary. Rust callers can useresolve_bytes()andresolve_named_bytes(). Text-only outputs report a UTF-8 error naming the secret. On Unix,runpasses non-UTF-8 values to child processes; it rejects NUL bytes before launching them. -
Doppler provider (
doppler://PROJECT[/CONFIG]): read, write, delete, and discover secrets in an existing Doppler project. It keeps names unchanged so secrets remain usable in the Doppler dashboard anddoppler run. The active SecretSpec profile selects the Doppler config unless the URI pins one. Authenticate withDOPPLER_TOKENor atokenprovider credential. Doppler restrictions and values it cannot store unchanged are reported as errors. -
Tailscale Setec provider (
setec://): read, write, delete, and discover secrets through a tailnet-authenticated Setec server. It supports binary values and reads pinned to a Setec version. -
Set a project-wide provider chain with
[defaults].providers. Secrets without their own or a profile-level chain use this default, so a checked-in manifest can refer to an alias that each developer maps to their preferred store. User-global aliasreftemplates can expand{project},{profile}, and{key}. Native SDK inline declarations support this through schema v2; v1 declarations still work. -
Use SecretSpec credentials with Claude Code.
secretspec claude configureinstalls anapiKeyHelperthat retrieves Anthropic API or gateway credentials from any provider.loginandlogoutmanage them by settings scope and API resource;unconfigureremoves the managed helper without replacing unrelated settings. Repository, user, worktree, andCLAUDE_CONFIG_DIRsettings are supported. -
Get editor help for configuration. Export JSON Schemas for
secretspec.tomland userconfig.tomlwithsecretspec schema --config projectorsecretspec schema --config globalfor autocomplete, field descriptions, and validation. -
Generate OpenPGP and OpenSSH private keys. Use
type = "openpgp_private_key"withgenerate.user_id, ortype = "ssh_private_key". Both generate modern keys by default and offer RSA for compatibility; OpenPGP keys can be limited to signing or encryption. -
Target a specific Bitwarden item by UUID when several items share a name. Reads and writes accept the UUID, and imports reject duplicate item fields before making changes.
-
Declare JVM SDK secrets inline with
withInlineSpecinstead of requiring a manifest file.
-
Stalled HTTP providers fail in bounded time. Vault, OpenBao, Infisical, Cloudflare, Scaleway, Azure App Configuration, Doppler, and Setec use a 10-second connection timeout and a 60-second request timeout. Vault and OpenBao now retry timed-out requests as configured.
-
macOS keyring access no longer asks for the login keychain password on every run after an upgrade. A new build may ask once per existing item; choose "Always Allow" to keep later runs of that build quiet. Reads preserve the item and its access settings, even if access is denied.
-
KeePassXC KDBX 4.0 files can be written. SecretSpec upgrades them to KDBX 4.1 on write while preserving their encryption and key-derivation settings.
-
Bitwarden reads are faster and more consistent. A batch uses one vault listing, and a single read falls back to the full listing if Bitwarden's search returns only similarly named items.
-
pass, gopass, and LastPass handle whitespace and multiline values more reliably. The pass provider removes one final newline added by the
passCLI on read. Existing gopass text entries still return their trimmed first line until rewritten; new values requiring exact bytes use its binary-entry format. Ordinary single-line gopass values remain readable by older tools. LastPass rejects NUL bytes before writing rather than silently truncating them. -
Cache planning avoids unnecessary provider reads and refuses a cache that would point to the same physical secret as its source under the active profile, preventing an overwrite or deletion of the source value.
-
Provider names remain registered when their Cargo feature is disabled, so selecting one now identifies the feature required by the build instead of incorrectly reporting that the provider does not exist. Provider discovery and enabled or disabled builds now share the same metadata catalog.
-
Interactive prompts now use the Console terminal backend, reducing CLI dependencies and allowing Unix signal handling to use Signal Hook 0.4.
-
SecretSpec now builds against Rand 0.10 and the TOML 1.1 ecosystem, improving compatibility with current Linux distribution Rust packages.
-
Secret resolution now applies provider fallbacks, generated and default values, compositions, scopes, and presence constraints through one ordered pipeline, keeping CLI and SDK resolution behavior aligned.
-
The embedded C ABI is named
libsecretspecin 0.20+, replacing thesecretspec-fficrate,secretspec_ffilibrary filenames, andsecretspec_ffi.pc. Its exportedsecretspec_*symbols andSECRETSPEC_FFI_LIBoverride are unchanged. Runtime-loading SDKs retain pre-0.20 shared-library filename fallbacks where supported. -
The Infisical provider now uses a separate HTTP connection for Universal Auth login, keeping the connection pool used for subsequent secret reads reliable.
-
The 1Password provider keeps batched reference resolution efficient when some items are missing: it lists missing items once per vault and retries the batch without them, instead of falling back to one
op readprocess per secret. Authentication and unavailable-CLI errors fail immediately, while missing optional items whose names resemble authentication diagnostics remain missing rather than aborting the batch. -
The value-free resolution surfaces —
secretspec check --json,check --explain, and the SDKs' report and no-values resolutions — no longer report a requiredgeneratesecret as resolved while no provider holds its value. Because these surfaces deliberately mint nothing, such a secret is now reported asmissing_requiredand the command exits non-zero, instead of passing a CI gate against an empty store. Runsecretspec checkorsecretspec runonce to generate and store the value; the preflight then reports it as resolved from its provider. An optionalgeneratesecret, or one routed to a provider that never retains generated values such asnull, is unaffected and still resolves.check --explainalso phrases such an entry aswill generaterather thangenerated, since nothing was minted. -
Git credential configuration now works on Windows, keeps repository-local includes valid when repositories move, distinguishes percent-encoded reserved path bytes, avoids persisting an ambient profile, and exits quietly when its output pipe closes on Unix.
-
Dotenv parsing and rendering now use dotenv-ng throughout the dotenv provider, age-encrypted dotenv blobs, and
secretspec export --format dotenv. Values containing$remain literal, output uses only the quoting needed to round-trip, and bcrypt-style strings containing$2a$10$...are no longer corrupted while reading (#73). Dotenv keys may include hyphens, leading digits, leading dots, and Unicode. Whitespace,=,#, and control characters remain invalid in keys. -
The Rust
secretspec-derivemacro now uses the support crates re-exported bysecretspec, so applications no longer need explicitserdeorsecrecydependencies just to compile generated types. -
secretspec checkwrites its human-readable report to stdout, consistently withcheck --jsonandcheck --explain, so it can be piped and redirected without mixing the report with diagnostics. Scripts that captured the report from stderr must capture stdout instead;2>&1continues to work. Rust SDK callers can select a report sink withSecrets::check_with_writer, while the existingSecrets::checkAPI and its stderr behavior remain unchanged.
-
The age provider supports deleting secrets in 0.20+:
secretspec delete,secretspec import --delete-source, and cache invalidation now work with it, so an age-encrypted file can serve as the local store of a cached provider alias. -
Native SDKs (0.20+) can resolve a strict, versioned inline secret declaration through the new
secretspec_callC ABI entry point. Inline declarations use an explicit logical base directory for relative providers, reject unknown fields, and require the new symbol so an older library cannot silently fall back to a filesystem manifest. The Go, Python, Node.js, Ruby, Haskell, PHP, C#, and Swift SDKs expose this as theirWithInlineSpec/with_inline_specbuilder method. -
Static musl CLI release binaries for x64 and arm64 Linux are available in 0.20+, so the standalone installer and
secretspec-updatework on Alpine without a glibc compatibility layer. -
Rust SDK (0.20+):
Spec::schema_jsonexposes the value-free JSON Schema generated bysecretspec schemato Rust SDK callers without enabling the CLI feature. -
Rust SDK (0.20+):
SpecBuilderpreserves comments, key order, quoting, and unrelated syntax when adding, replacing, or removing declarations from aSpecloaded from TOML.preserved_textexposes the exact edited root document, whileto_tomlrenders freshly formatted TOML when no preserved document is available. Inherited declarations remain in their parent files and are revalidated after every edit. -
The Node.js SDK publishes musl builds of the native addon in 0.20+, so
require("secretspec")works on Alpine images such asnode:alpine(#383). npm picks the build that matches the host libc, on both x64 and arm64. -
extractsupports INI documents in SecretSpec 0.20+, selecting an unsectioned key with/keyor a named-section key with/section/key. -
Rust SDK (0.20+):
SecretSpecBuilder::prompt_missinglets the typed loader generated bydeclare_secrets!prompt for and store missing required secrets interactively, matchingSecrets::ensure_secretson the untyped SDK. It is disabled by default (RequiredSecretMissingstill fails fast, exactly as before) and only prompts when stdin is a real terminal. -
In 0.20+, a read-only
git-credential-secretspechelper lets Git retrieve HTTPS usernames and tokens through SecretSpec providers without duplicating them in Git's credential store. Its built-in manifest keeps the default independent of the current directory and isolates values by protocol, host, and configured path; equivalent unreserved URL encodings select one canonical credential, including for flat-key providers.secretspec git loginandlogoutmanage those values explicitly, and manually registered embedded helpers can use the stablePASSWORDandUSERNAMEaliases. SMTP credential contexts supportgit send-emailwithout writingsendemail.smtpPass, with passwords isolated by case-insensitive server, port, and username. Path-scoped HTTP(S) credentials take precedence over host-wide fallbacks, and username-bearing Git requests select only the matching credential.configureandunconfiguresafely manage repository or global Git configuration without replacing existing helpers. The owner-only managed file is durably written, custom-manifest symlinks are preserved, and failed or repeated include removal leaves recoverable state. Only explicitly passed provider and reason options are persisted, while--fileretains the custom-manifest workflow (0.20+). -
Kubernetes provider (
k8s+<configmap|secret>://, 0.20+): store, read, delete, and discover values in a Kubernetes ConfigMap or Secret using the current cluster. -
EJSON provider (
ejson:, 0.20+): read string values from an encrypted EJSON file with RFC 6901 JSON Pointer references. The private key comes from an explicitprivate_keyprovider credential, so an existing provider such as Google Cloud Secret Manager can supply an exact key without putting it in the URI, environment, process arguments, or a local key file. Batch resolution decrypts each file once, and the initial provider is intentionally read-only. -
Azure App Configuration provider (
aac://, 0.20+): select direct values and Azure Key Vault references by label, prefix, and tags, with Entra ID or connection-string authentication and guarded writes, deletion, and declaration discovery. Azure Key Vault references can pin an exact secret version, cached-route validation compares canonical vault endpoints independently of authentication choice, and discovery rejects ambiguous or invalid convention keys. HTTP redirects are rejected so reads and secret-bearing writes remain confined to the configured store endpoint. -
Rust SDK (0.20+): describe secrets without TOML through the public
Spec,SpecBuilder,Profile, andSecretAPI. TOML parsing and code generation use the same validated model, so Rust-first and file-backed projects share inheritance, generation, and provider behavior, including profile-level requiredness defaults and explicit opt-outs from inherited path, prompt, and generation settings. Existing specs can be copied or consumed back into a builder to add, replace, or remove declarations before rebuilding a validated spec. This validated declaration API replaces the previously exposed raw configuration and code-generation implementation types. Custom provider implementations should now return declarations such asSecret::required(...)fromProvider::reflectinstead of constructing raw configuration secrets. -
Structured caller context (0.20+) lets CLI and SDK integrations identify the invoking software, version, operation, and non-secret resource independently of the user-supplied access reason. Audit records and providers receive the context, but it never satisfies the
require_reasonpolicy. -
Code generation (0.20+) carries each secret's declared
descriptioninto the generated JSON Schema as adescriptionkey on its property. quicktype turns that into a native docstring in every target language, so SDKs generated from a manifest carry the same descriptions the manifest already declares, instead of losing them at the schema boundary. -
The Fly.io
flyprovider (0.20+) publishes and deletes application secrets withsecretspec setandsecretspec delete, and discovers their names withinit --from. Fly.io never exposes plaintext secret values, so the provider clearly rejects read operations and CLI guidance recommends only supported workflows. Writes keep values off process arguments by streaming them toflyctl secrets setover stdin, refuse boundary whitespace thatflyctlwould silently trim, and scrub ambient Fly token variables before injecting the token selected through the provider credential mechanism. -
The Cloudflare
cloudflareprovider (0.20+) publishes, replaces, deletes, and discovers account-level Secrets Store entries through Cloudflare's API. Cloudflare never returns plaintext values through its management API, so the provider clearly reports its write-only behavior. Authentication can use a SecretSpecapi_tokencredential,CLOUDFLARE_API_TOKEN, or the current Wrangler OAuth, API-token, or legacy API-key session; secret values are sent only in HTTPS request bodies (#84). -
secretspec completions <shell>(0.20+) generates completion scripts for Bash, Elvish, Fish, Nushell, PowerShell, and Zsh directly from the CLI definition, including descriptions and contextual suggestions for profiles, scopes, secret names, providers, aliases, paths, and commands. Completion reads configuration metadata only; it never queries providers or reads secret values. -
In 0.20+, a read-only
docker-credential-secretspechelper lets Docker retrieve registry usernames and tokens through any SecretSpec provider.secretspec docker configureandunconfiguresafely manage per-registry Docker credential-helper settings without replacing existing helpers, whilesecretspec docker loginandlogoutmanage isolated embedded credentials; custom manifests remain available through--file(0.20+). -
JVM SDK (0.20+): use SecretSpec from languages such as Java or Kotlin.
-
1Password convention-secret batches now retrieve all matching items through one
op item getprocess instead of starting one process per secret, avoiding desktop-app connection timeouts when resolving larger manifests. Batch input uses the CLI's structured JSON interface so reads work consistently across 1Password CLI releases (#398). -
Bitwarden Password Manager convention secrets now use
secretspec/{project}/{profile}/{key}item titles, preventing a same-named secret in another project or profile from being read or overwritten. The existing?folder=option customizes that title prefix. Bare items created by releases through 0.19 can be renamed to the namespaced title or retained with an explicitref; declaration discovery emits those legacy bare items as refs automatically. Names remain isolated when a project or profile contains/, andsecretspec init --from bw://recognizes convention names case-insensitively while preserving legacy items' native references. (#369) -
On Unix,
secretspec runnow forwardsSIGTERM,SIGINT, andSIGHUPto the command it started, allowing graceful container shutdown even when SecretSpec is PID 1. Commands terminated by a signal now produce the conventional128 + signalexit status instead of always exiting 1 (#382). -
Format-preserving
Specedits now keep inherited declarations separate after semantic builder changes, resolve parent specs independently of later working directory changes, remove synthesized profile tables when additions are undone, and apply description validation consistently across builder origins. -
The Kubernetes provider now checks the correct Kubernetes Secret resource permission and refuses
import --delete-sourcebefore copying values when the source object cannot be patched. -
Closing the output pipe now ends the CLI quietly on Unix, so
secretspec export | headandsecretspec check --json | headbehave like any other Unix tool. PreviouslyexportreportedIO error: Broken pipeand exited 1, andcheck --jsonpanicked withfailed printing to stdout, because Rust ignoresSIGPIPEby default and surfacedEPIPEinstead. The entry point now restores the defaultSIGPIPEdisposition, which covers every command that writes to stdout. -
cargo runcontinues to launch the main SecretSpec CLI after installing the Docker credential-helper binary in the same package (0.20+). -
Embedded Docker credentials now remain isolated by registry and Docker configuration when used with flat providers such as Dotenv. Provider keys carry the same stable identity as the embedded project, preventing one registry's login or logout from affecting another (0.20+).
-
Docker now reports when
configurereplaces a registry's existing SecretSpec metadata and clarifies that the stored credential was not removed (0.20+). -
Docker can now manage the same registry independently in multiple
DOCKER_CONFIGdirectories. Helper lookup and embedded credential storage are isolated by both registry and Docker configuration (0.20+). -
Equivalent
DOCKER_CONFIGpaths that resolve through symlinked directories now share one Docker credential identity, so helper lookup and cleanup work regardless of which path spelling invokes them (0.20+). -
Default Docker audit caller context no longer reports the SecretSpec release as Docker's version. Docker remains identified as the caller while its unknown version is omitted (0.20+).
-
Docker's managed credential state is now restricted to owner-only permissions without changing the existing mode of Docker's own
config.json. Atomic updates and final-entry removal preserve a symlinked state file, and interrupted removal can be resumed when Docker's helper entry was already deleted (0.20+). -
Docker credentials configured through a symlinked custom manifest now retain that logical manifest path, so relative
extendsentries continue to resolve from the directory where the symlink was selected (0.20+). -
secretspec dockerno longer treats exportedSECRETSPEC_FILE,SECRETSPEC_PROFILE,SECRETSPEC_PROVIDER, orSECRETSPEC_REASONas typed configuration flags. Only an explicit--provideror--reasonis saved for later helper calls, and ambient manifest/profile settings no longer switch or block the embedded credential workflow (0.20+). -
Custom-manifest Docker credentials now pin a profile only when
--profileis explicitly supplied, preventing a shell's ambient profile from becoming a permanent helper setting (0.20+). -
Google Cloud Secret Manager convention names now use the readable, versioned
secretspec2--{project}--{profile}--{key}layout. Distinct logical addresses such asmy-app/prod/Kandmy/app-prod/Kcan no longer collide on one stored secret. When the new id holds no value, reads fall back to the matching 0.19secretspec-{project}-{profile}-{key}secret and warn once per run, so an upgraded project keeps working with no migration step and no new permissions: the fallback only reads, and credentials that cannot create secrets are unaffected. Writes always use the new id, sosecretspec setmoves a secret, after which reads stop consulting the legacy id. The 0.19 secret is left in place for rollback and should only be deleted once its value has been written under the new id. Names that releases through 0.19 accepted but the new layout cannot represent, such as a project containing--, keep reading their 0.19 secret with a warning; writing them requires renaming the component or addressing the secret with aref. Secret-level IAM bindings on 0.19 ids also keep working when an unbound new id returns permission denied, while other access failures remain errors instead of being mistaken for missing values. Explicitrefaddresses remain unchanged. (#219) -
Bare
bws://<project-uuid>provider URIs now target the Bitwarden US cloud vault instead of the public marketing site, restoring reads and writes while keeping the server pinned independently of ambientbwsconfiguration. (#359) -
Node SDK processes using
loadAsync()orreportAsync()with AWS Secrets Manager or Parameter Store now exit normally after resolution. Provider runtime and TLS state is torn down on a short-lived resolver thread instead of remaining attached to a persistent libuv worker during macOS process shutdown. (#343) -
The
awssmandscalewayproviders now treat a JSONnullin areffield as no value, the same as an absent key, so the provider chain continues. Previously it was rendered as the four-character stringnull, which satisfied a required secret and reached the program as a password or token spelledn-u-l-l. Thebwanddashlaneproviders already behaved this way. Anextractpointer is unchanged: it names one location and still reports anullthere, and the two policies now sit next to each other in one place. -
The Python and Ruby SDKs'
Resolved.close()/Resolved#closenow remove everyas_pathtemp file even when one of them cannot be removed, raising the first such error only after the rest are cleaned up. Previously the first failure aborted the loop and left the remaining secret files on disk, which is the outcomecloseexists to prevent. This matches the Go SDK'sfirstErrand the .NET SDK'sfirstError. The Ruby SDK also no longer skips a dangling symlink, whichFile.exist?reports as absent. -
The
awssmprovider now accepts a trailing slash in?prefix=without inserting a second slash into the AWS secret name. For example,?prefix=myteam/resolves tomyteam/secretspec/..., matching?prefix=myteam, and both spellings share one provider identity so import diagnostics still recognize alias-specific references. This avoids silently treating the secret as missing or writing to a distinct double-slash name (#344). -
import --delete-sourcenow rejects providers that cannot delete during preflight, before writing any destinations. Previously the import could copy a value successfully and fail only when source cleanup began. -
Infisical secret references no longer require
?env=in the provider URI: arefnames a folder and key but never an environment, so it now falls back to the profile the run resolves under. One alias can therefore serve every profile while naming secrets flat —ref = { item = "/{key}" }— instead of needing one alias per environment. An explicit?env=still pins the environment. When every requested secret gets Infisical's ambiguous 404, SecretSpec now checks the environment root once without requesting secret values and reports a missing environment or project, naming whether the profile or?env=selected it. A genuinely absent secret or folder in an existing environment remains unset so provider fallback still works. A credential declared with arefstill needs?env=, so it resolves the same way whichever profile is running. (#338) -
secretspec setagainst an Infisical secret names the environment in its pre-write preview, which the previous description left out. -
Infisical import collision checks now recognize when aliases target the same secret through a profile-derived versus explicit environment, or through an absolute ref that overrides different configured path defaults, preventing aliased destinations from overwriting one another.
Republishes 0.19.0's command-line artifacts. The library and CLI behave exactly as in 0.19.0.
- Windows ARM64 CLI release artifacts (
aarch64-pc-windows-msvc), attached to the GitHub Release assecretspec-aarch64-pc-windows-msvc.zipwith a checksum. The static installer keeps selecting the x86_64 build on Windows ARM64, which runs under emulation, so download the archive directly for a native binary.
- The 0.19.0 GitHub Release shipped without its CLI archives, its installer,
and the Swift XCFramework, so
curl https://install.secretspec.dev | shandswift packageresolution of 0.19.0 both failed. Every language registry (crates.io, PyPI, npm, RubyGems, Hackage, NuGet) published 0.19.0 normally and is unaffected. Install 0.19.1 instead; SwiftPM version ranges resolve to it automatically.
- A provider URI may no longer carry a credential. A URI with a password
(
scheme://user:PASSWORD@host) is rejected, andonepassword+token://no longer accepts the service account token in its userinfo (onepassword+token://token@vault). A URI is committed tosecretspec.toml, echoed into shell history, and printed by CI, so a credential written there is already disclosed and redacting it at the terminal cannot retract it. Keep the scheme and supply the credential through a provider credential (secretspec config provider login <alias>, orcredentials = { ... }on the alias) or the provider's environment variable; the errors name both. An unparseable provider specification is now also redacted before it is reported. secretspec getresolves through the same path as the SDK'sresolve_named, so a single-secret read makes exactly the decisions batch resolution makes. It continues to read the whole profile regardless of an active scope, and audits the coordinates it actually reached.- 1Password field references now resolve in one batched CLI call, reducing repeated unlocks and process startup when loading multiple secrets. If a missing reference requires individual reads, those reads remain bounded and concurrent.
- The Rust SDK's
ProviderAliasnow providesleaf,credentials, andcredentials_muthelpers so callers can construct and inspect leaf or inline-cached aliases without depending on their storage representation.
- The Rust SDK can resolve a single secret with
Secrets::resolve_named, which reads only that secret and the inputs it composes from. An unrelated missing required secret no longer fails the call, and the result distinguishes an undeclared name (including one the active scope hides) from a declared secret with no value, reporting whether that value was required. Secrets::with_default_reasonsets a session reason only when none is already in effect, so an embedding application can describe itself without discarding the reason its own caller supplied throughwith_reasonorSECRETSPEC_REASON.- Secrets can set
prompt = trueto request a hidden value from the controlling terminal whensecretspec runfinds no stored value. Writable providers save the answer for later runs; thenullprovider keeps it invocation-only. - Profiles can opt out of inheriting
[profiles.default]by settinginherit = falsein their profile defaults (0.19+), allowing standalone secret sets alongside profiles that still share the default declarations. - Passbolt provider (
passbolt://): store and read secrets in a self-hosted Passbolt server through the community-maintainedgo-passbolt-cli, with convention-based names, references to existing resources, and credentials supplied by the CLI configuration or SecretSpec provider environment variables. - Provider aliases can define native
reftemplates and secrets can override coordinates per leaf alias withrefs, so fallback providers and import sources/destinations resolve independently.import --delete-sourcenow preflights the whole migration, verifies all writes before cleanup, and can safely move between distinct entries in the same physical store. - A
nullprovider lets non-sensitive, version-controlled environment values use their manifest defaults and lets generated secrets stay ephemeral, with a fresh value returned for each resolution and nothing written to provider storage. secretspec setand interactivesecretspec checknow preview the resolved write destination before reading the value, including the exact file and selector for SOPS.- A
fileprovider stores each secret as one plaintext UTF-8 file beneath an explicitly configured relative or absolute directory, with project/profile isolation and support for existing file-mounted secrets throughref.item. - Secrets can select values from stored JSON documents with RFC 6901 pointers
using
extract. Extraction composes with provider-native references and storage decoding; selected values are read-only so sibling document data is never overwritten or deleted. - Secrets can store values as standard Base64, URL-safe Base64, or hexadecimal
using
encoding; writes encode logical text and reads decode stored values, whileas_path = truematerializes arbitrary decoded bytes. secretspec-ffiinstalls (viacargo cinstall) together with its C header and asecretspec_ffi.pc, so consumers can link it — statically or dynamically — without hand-written linker flags.- The Haskell SDK's new
use-pkg-configcabal flag (cabal build -f use-pkg-config) resolves an installed static or shared library through pkg-config. - The Ruby SDK's native extension accepts a new
--enable-pkg-configbuild flag (gem install secretspec -- --enable-pkg-config) that resolves an installed static or shared library through pkg-config. - The Go SDK has a new
pkgconfigbuild tag (go build -tags pkgconfig) that links an installed static or shared library, so it also works for ago getdependency. - The Haskell SDK declares the archive's macOS system frameworks
(
SystemConfiguration,Security,CoreFoundation) in its cabal file, so GHC passes them to every link on macOS. - A single provider can now attach its cache directly to the same alias with
uriandcache, avoiding a second wrapper alias while retaining provider credentials. Cachedfallbackroutes remain available for multiple authoritative providers.
- Ruby gems for Apple silicon now use the generic
arm64-darwinplatform instead of including the build runner's Darwin version. - Windows shared
secretspec-ffiinstalls now place the runtime DLL in the documentedPREFIX/libruntime library directory. importwarns when a literal source uses convention naming but a provider alias for the same storage container addresses active secrets differently through areftemplate or scopedrefs. Import output also retains the selected source alias, making alias-specific addressing visible without changing literal-provider semantics. (#312)- The error for a coordinate a provider does not support now points at
refs.<alias>and aliasreftemplates as well as at removing the coordinate, so afieldwritten for one store no longer has to be dropped to reach another store that organizes the secret differently. (#266) - The Proton Pass provider works with
pass-cli2.2.4 and later, which removed thepass-cli testsubcommand the provider ran to check the session before every read and write. The check now triespass-cli infoand falls back topass-cli test, so a single build works acrosspass-clireleases that disagree about which check exists. Apass-cliwith neither is reported as incompatible with the SecretSpec release, instead of passing the CLI's usage text through as the error. (#279) - SOPS write-target previews consistently use canonical physical paths on macOS and Windows, matching the files used for writes.
- Passbolt now updates UUID-addressed resources outside a configured folder, treats URI- and environment-selected forms of the same server as one import destination, rejects malformed provider query parameters, and avoids redundant resource listings during writes.
- Cache entries now store their absolute expiration time, allowing SecretSpec
to remove an expired entry whenever it encounters one, including at an
address previously used by another project or profile. Changing
max_ageinvalidates entries written under the previous policy. Fresh v2 entries remain usable during migration, while foreign v2 entries remain untouched. (#275) runpreserves non-UTF-8 environment values byte-for-byte when launching child processes on Unix.- Provider-scoped references now compare provider-defaulted coordinates before destructive imports, apply scoped address overrides before comparing stores, and recognize missing file destinations reached through symlinked parents. They also invalidate caches for every coordinate change without display-format collisions and retain the attempted native location in audit events when a provider read fails. Same-store import validation handles Windows provider paths without treating separators as TOML escapes.
- Profile overrides can switch between legacy
refand provider-scopedrefswithout retaining both inherited address models and failing validation. - JSON extraction from file-backed documents now handles Windows store paths without treating path separators as TOML escapes.
- SDK pkg-config setup now pins cargo-c's library and metadata install
directories, so Go, Ruby, and Haskell reliably discover
secretspec_ffi.pcacross environments. - The keyring provider no longer intermittently fails with a "No default store has been set" error when resolving multiple secrets concurrently.
- The SOPS provider no longer substitutes a second time into a rendered path
segment, so a project or profile literally named
{profile}or{project}resolves to the file you configured instead of a different one. - Invalid SOPS path templates are now rejected when loading serialized provider configurations instead of being accepted without validation.
- The LastPass provider now reports its full item template rather than only the
first segment. A multi-segment template such as
lastpass://Shared/{project}/{profile}/{key}used to be reported as plainlastpass, which reads back as the defaultsecretspec/{project}/{profile}/{key}template — a different folder — andlastpass://Work/TeamA/{key}read back as the literal itemWork, one item for every secret. Templates that differ below their first segment are now distinguished, so repointing a cached route at a new template invalidates its cached values instead of serving the old ones until they expire. Single-segment templates are unaffected; cached entries for a multi-segment template refetch once, silently, on first run. - Provider fallback chains now reuse each provider and resolve independent
primary misses concurrently. Azure Key Vault providers also reuse their
client and serialize its initial challenge-based authentication, so chains
such as
providers = ["keyring", "akv"]no longer fetch every fallback in series or launch separate Azure CLI processes for the same resolution. - Reusing a
Secretsinstance now refreshes fallback providers for each resolution, so provider-side caches observe rotated values and providers use the latest reason supplied withwith_reason.
- The keyring provider now uses keyring 4's Rust-native Secret Service transport on Linux, so source builds and binaries no longer require system libdbus.
secretspec init --fromnow accepts every provider with declaration reflection, including age, AWS Parameter Store, and Bitwarden Password Manager, and accepts--projectand--profileas explicit discovery context for hierarchical stores.- Custom Rust providers now pass discovery context to the
Provider::reflecthook so hierarchical stores can select the project and profile namespace.
- The Bitwarden provider now treats a locked vault or a missing session as a
clear authentication failure on
get/set, with the same "runbw loginandbw unlock, then setBW_SESSION" guidance in both cases, instead of surfacing the underlying CLI error text. - The Bitwarden provider now reports a missing
bwCLI with install instructions instead of an authentication error: a machine without the CLI is not an authentication state, and the install guidance ("…run 'bw login' and 'bw unlock' to authenticate") used to match the not-authenticated classifier and mask the real problem. - Vault and OpenBao JWT authentication now allows the role to be omitted when
the auth mount has a server-configured
default_role, while explicit URI or environment roles continue to take precedence. - Vault and OpenBao AppRole authentication now supports roles configured with
bind_secret_id=falseby omittingsecret_idfrom the login request when no SecretID credential is configured. secretspec import --delete-sourcenow compares resolved storage entries without conflating distinct cache address spaces, preventing equivalent provider configurations (including dotenv path aliases) from deleting the destination value. Sources without deletion support are also rejected before any destination is written.- The AWS Secrets Manager provider now authenticates with shared credentials
file profiles backed by an active AWS login session, which previously failed
because the required AWS SDK feature was not enabled.
BatchGetSecretValuefailures also report the full service error instead of a shortened message.
- The dotenv provider accepts a leading
~in custom paths, such asdotenv:~/.config/my-project/.env, and resolves it to the user's home directory. - Vault and OpenBao AppRole and JWT authentication can target non-default auth
method mounts, including printable Unicode mount names, with the
auth_mountprovider URI option. secretspec add NAME --description "..."(available in 0.18) adds a secret declaration to the active profile while preserving the manifest's existing comments, formatting, and unrelated configuration.- AWS Parameter Store convention templates and bounded
GetParametersByPathdiscovery can create declarations from the direct children of an existing hierarchy without decrypting their values. - Swift SDK (available in 0.18) for resolving SecretSpec manifests from macOS
12+ on Intel and Apple silicon. The SwiftPM package provides fluent and
one-shot resolution, typed failures, scopes, value-free reports, provenance,
environment export, codegen input, and deterministic
as_pathcleanup. Its checksummed XCFramework includes the shared Rust resolver, so applications do not need a Rust toolchain or separately installed native library. secretspec deleteremoves one or more stored secret values without changing their manifest declarations, while--allrequires explicit confirmation.secretspec import --delete-sourceverifies each destination value before deleting its source, and retains the source when an existing target differs.- Bitwarden Password Manager provider (
bw://,bwbuild feature) for reading and writing secrets in a personal or organization vault through thebwCLI. Collections and organizations are addressed by name or by id (bw://myorg@dev-secrets),?type=and?field=select an item type and field, and?server=asserts which self-hosted server the configuration expects. Every item type is supported (login, secure note, card, identity, SSH key), each with a default field shared by reads and writes. Item names are matched in full and case-insensitively, and an ambiguous name is refused with the colliding ids rather than resolved to an arbitrary item. - Dashlane provider (
dashlane://) for reading secrets from a Dashlane vault through thedcliCLI. Convention secrets read the item titledsecretspec/{project}/{profile}/{key}, and arefnames an existing item by title or identifier with an optionalfield.dashlane://note,dashlane://secret, ordashlane://passwordrestrict the search to one content type. The provider is read-only, becausedclihas no command that creates or edits a vault item;secretspec setfails with that reason. Non-interactive use is supported throughDASHLANE_SERVICE_DEVICE_KEYS, which can also be injected as theservice_device_keysprovider credential. Injected credentials read through a private, owner-onlydclistate directory of their own, becausedcliotherwise prefers a device already registered on the machine and reads that identity's vault instead. - Keeper Secrets Manager provider (
keeper://FOLDER_UID,keeperbuild feature) using Keeper's official Rust SDK, with convention-based records, references to existing records and fields, provider credentials, batch reads, writes, and cache-compatible deletion. SDK calls are safe from async Rust applications, and updates preserve the JSON types of Keeper fields such as dates, checkboxes, hosts, and names. - AWS Systems Manager Parameter Store provider (
awsps://,awspsbuild feature) for reading and writing KMS-encryptedSecureStringparameters. It supports AWS profiles and regions, an optional hierarchy prefix, customer-managed KMS keys, parameter tiers, batched reads, and references by parameter name, version, label, or ARN. Unversioned parameter-name references can be written in place; version-, label-, and ARN-pinned references are read-only. Writes reject unsupported reference coordinates before requesting a value, and versioned ARN errors point to writable parameter-name references. AWS service errors include their error codes and messages instead of onlyunhandled error. (#209)
- Vault and OpenBao AppRole and JWT authentication methods now reuse login tokens within each provider operation up to each token's reported use and lease limits, including time spent completing authentication, avoiding repeated logins without exhausting or outliving tokens during batches, writes, or deletes. Invalid batch addresses are rejected before login, and concurrent requests remain safe across Tokio runtime flavors while keeping pooled HTTP connections alive for the full operation.
- Cached provider routes now recognize Vault and OpenBao configurations that address the same endpoint, namespace, and mount as one store, even when they use different provider names or authentication methods, so a cache cannot target its own authoritative source.
- Provider and SDK errors now retain underlying causes such as authentication,
timeout, DNS, TLS, connection, and response-parsing failures. AWS Secrets
Manager errors also report AWS error codes and messages instead of only
unhandled error. - Prebuilt Linux Go SDK and
secretspec-ffilibraries now include libdbus instead of requiring the build host'slibdbus-1.so.3, so they load on NixOS and other systems without a matching system library. (#214) - The dotenv provider's "cannot store" error now tells you to rename the secret
in
secretspec.tomlwhen the name came from a manifest declaration, instead of always pointing at arefitem the config may not contain. - Typed loaders generated by
secretspec-derivenow keep temporary files foras_pathsecrets alive until the returned resolved secrets are dropped.
- Cache reads, refreshes, and clears now share one ownership and freshness policy, consistently handling expiration boundaries, clock rollback, corrupted SecretSpec entries, and values owned by another project or profile.
- Vault and OpenBao providers reuse one
reqwest::Clientper provider instance (sameOnceLockpattern as Infisical) instead of building a fresh client on every get/set/login. Concurrentget_manyof many secrets no longer opens one TCP(+TLS) handshake per secret against reverse-proxied deployments, which was observed to drop part of the burst withFailed to connect to Vault. get_each(defaultProvider::get_many) caps concurrent unique-address fetches at 8 by default, overridable withSECRETSPEC_PROVIDER_CONCURRENCY. Waves replace a single unboundedthread::scopefan-out.- Vault/OpenBao HTTP sends retry up to 3 times on connect/timeout errors only (not on HTTP 4xx/5xx), with a short backoff between attempts.
-
SOPS provider (
sops://,sopsbuild feature) for reading and writing YAML, JSON, dotenv, and INI files through the SOPS CLI, including templated per-project/profile paths and provider-credential injection for encryption keys and cloud authentication. Writes are serialized and atomically replace encrypted files, with secret values passed to SOPS over standard input. -
Scaleway Secret Manager provider (
scaleway://,scalewaybuild feature) for storing secrets in Scaleway's Secret Manager over its v1beta1 REST API. Authenticates with an API secret key (secret_keycredential orSCW_SECRET_KEY), targets a region (URI host orSCW_DEFAULT_REGION, defaultfr-par) and project (?project_id=orSCW_DEFAULT_PROJECT_ID), and stores convention secrets under the folder pathsecretspec/{project}/{profile}with the key as the secret name. Nativerefreferences may select a JSON key withfieldand a revision withversion, and are read-only. -
Cached provider aliases with ordered authoritative
fallbackroutes, configurable local cache freshness, cache-first reads, automatic refresh after reads and writes, andsecretspec cache clearinvalidation.A cache must be a distinct store from the route's own authoritative providers (compared by canonical provider URI, so equivalent spellings of one store cannot disguise a cache as its own source), must be a store SecretSpec can delete from (keyring, pass, gopass, dotenv, or a Vault/OpenBao KV v2 mount) so its entries can be invalidated, and must be the only entry in a
providerslist. All three are reported when the route is planned, and an unusablemax_agewhen the configuration loads.Every entry records the project and profile that own it, and SecretSpec only changes an entry it can show is its own: a value it did not write, or one belonging to another project or profile, is left alone by reads and refreshes and reported by
cache clearrather than deleted, since an address alone is not proof of ownership when a store is shared. An entry marked as SecretSpec's own but unreadable is replaced.A cached value never outlives the write that superseded it: a failed refresh, a cache that could not be constructed, and a write that bypassed the cache with
--providerall invalidate the entry. An entry no read can serve — expired, or written for a different route — is deleted when found rather than skipped, so an expired value does not keep its plaintext in a store that cannot expire anything. Where the store can expire a value itself,max_ageis applied server-side — Vault and OpenBao set the KV v2 path'sdelete_version_after— so a cached copy stops existing at that age even if SecretSpec is never run again, and clearing a KV v2 entry destroys its recoverable version history.cache clearreports how many entries it actually removed, ignores provider overrides, and clears what it can before reporting a cache store it could not. Cache writes are audited ascache_refreshrather thanset. (#199) -
secretspec config global init --provider <PROVIDER> --profile <PROFILE>can save explicitly user-global defaults without interactive prompts, including--profile noneto clear the default profile. Theglobalnamespace also supports config inspection and provider-alias commands; existing invocations without the namespace remain compatible. (#171) -
Secret scopes: a
[scopes]table names membership-only subsets of a profile's secrets, so a single service or task resolves only what it declares instead of the whole profile.check,run, andexporttake--scope(SECRETSPEC_SCOPE); the consumer-visible set is the intersection of the selected profile and the scope's secret list. Scopes are orthogonal to profiles and never change a secret'srequired/default/providers or its storage address. A composed secret in a scope still resolves its dependencies — even ones the scope leaves out — to build its value, but those dependencies are never exposed to the scope, and a provider warning about one calls it "a hidden composition input" rather than naming it; a secret that is neither in the scope nor a dependency of one is never fetched, and a scope whose intersection with the selected profile is empty contacts no provider at all (resolve and report results then carry an emptyprovider). A scope's own list must name at least one secret, with no blank or repeated entries.run --scoperemoves every manifest-declared secret the scope does not admit from the child environment — across all profiles, even one the parent already exported — so no value can leak into the launched process; a secret the scope lists is kept even when the selected profile does not declare it.export --scopeemits the scoped subset but unsets nothing, since no output format can express an unset.set, likeimport, ignores an ambientSECRETSPEC_SCOPEentirely, and a blank--scope(or a blankSECRETSPEC_SCOPE) clears an inherited scope instead of deferring to it. Under projectextends, a child scope replaces the parent scope of the same name outright rather than unioning their secret lists. Typed SDK loaders ignore an ambientSECRETSPEC_SCOPE, since a generated struct always expects the full profile;importlikewise ignores scope and always copies the whole profile. Untyped SDK/FFI builders expose explicit scope selection and return the active scope in resolve/report results. Audit events for scopedcheck,run, andexportoperations record the scope name as well as the keys accessed or exposed. -
age provider (
age://) for storing dotenv-style secret sets in an age-encrypted file, with ASCII armor by default, team recipient rosters, direct X25519 and SSH key support, native tagged recipients, and non-interactive age plugins. Hybrid ML-KEM-768 + X25519 keys are recommended for new setups to protect stored ciphertext against future quantum attacks. -
Read-only systemd credential provider (
systemd-credential://) for resolving secrets and provider authentication credentials from the current service's$CREDENTIALS_DIRECTORY, including exact-name references and strict filename, file-type, and text validation. -
KeePass KDBX provider (
kdbx:,kdbxbuild feature) for local encrypted databases. It reads KDBX 3 and KDBX 4, writes KDBX 4 with atomic file replacement, supports master passwords and key files, and can address standard or custom entry fields through secret references. -
The
requiredfield acceptsat_least_oneandexactly_onegroup tables, supporting overlapping alternative and mutually exclusive credentials acrosscheck,run, and SDK resolution. -
OpenBao provider (
openbao://,openbaobuild feature) with its own provider identity, documentation, and OpenBao CLI configuration throughBAO_ADDR,BAO_NAMESPACE,BAO_TOKEN, andBAO_TOKEN_PATH. The provider also has OpenBao-prefixed AppRole and JWT inputs; correspondingVAULT_*names remain compatibility fallbacks. Compatible KV and standard authentication mechanics are shared internally with the Vault provider. Vault-compatible addresses accept trailing slashes, and AppRole/JWT login exchanges now honor the configured namespace. Reported provider URIs strip endpoint credentials while retaining non-secret store and authentication attribution. -
Vault / OpenBao JWT/OIDC authentication (
?auth=jwt) logs in through a configured Vault role usingVAULT_JWT, or requests a short-lived OIDC token automatically in GitHub Actions and Forgejo Actions jobs withid-token: write. The role and optional audience can be set in the provider URI or withVAULT_JWT_ROLEandVAULT_JWT_AUDIENCE. -
The Python SDK now publishes a Windows x64 wheel to PyPI, so
pip install secretspecanduv add secretspecwork on Windows. (#177) -
The Ruby SDK now publishes a Windows gem (
x64-mingw-ucrt) to RubyGems, sogem install secretspecworks with RubyInstaller on Windows. -
The PHP SDK now publishes prebuilt Windows x64 extension binaries (
secretspec-php-native-<php>-nts-x86_64-pc-windows-msvc.dll) alongside the Linux and macOS builds on each release.
- The Bitwarden Secrets Manager provider now invokes the separately installed
official
bwsCLI instead of linking the Bitwarden SDK. This removes the SDK's restricted-license dependency from SecretSpec distributions while preserving project-scoped reads, writes, access-token credentials, and EU/self-hosted server selection. - Secret status output now emphasizes secret names, de-emphasizes descriptions,
and omits placeholder text when a description is unavailable, making long
checkandimportresults easier to scan. (#139)
- BWS CLI writes preserve secret keys and values that begin with
-, and hostless BWS provider URIs stay pinned to Bitwarden's default server even when ambient BWS profiles or server settings are configured. - The dotenv provider rejects variable names its parser cannot read back
(anything outside
[A-Za-z_][A-Za-z0-9_.]*, for example arefitem containing a dash) instead of writing a line that made every later read and write of the whole file fail to parse. The rejection happens before the CLI prompts for a value and names the offending item.
- Composed secrets derive read-only values such as connection strings from
other declared secrets using strict
${UPPERCASE_NAME}templates; names must match[A-Z][A-Z0-9_]*,$$produces a literal dollar sign, and ordinary braces remain literal. Dependencies are order-independent, may include other compositions, and are validated for unknown references and cycles before provider access; unlike dotenv expansion, values are substituted once without ambient environment lookup, fallback operators, recursive expansion, or silent empty replacements. - C# SDK (
Cachix.SecretSpec, available in 0.16): resolve secrets from .NET through the shared native resolver, with fluent builder and one-shot APIs, typed failure exceptions, value-free preflight reports, provenance, environment export, typed-codegen input, and deterministic cleanup ofas_pathfiles. The trimming-safe, NativeAOT-compatible NuGet package includes native resolver builds for glibc and musl Linux x64/Arm64, macOS x64/Arm64, and Windows x64/Arm64; Windows applications do not need a separate Visual C++ Redistributable. - Infisical provider (
infisical://), for Infisical Cloud and self-hosted instances. Authenticates as a machine identity via Universal Auth, whoseclient_idandclient_secretcan be sourced as provider credentials (withINFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRETfallbacks), or with a ready-madetoken/INFISICAL_TOKEN. A profile names the Infisical environment, so aproductionprofile reads theproductionenvironment; projects whose environments do not correspond to profiles pin one with?env=, and profiles stay separate either way. Secrets live at/secretspec/{project}/{profile}(?path=overrides the prefix), with keys stored verbatim, and secrets sharing a folder are fetched in one request. A folder's imported secrets resolve too, with Infisical's own precedence. A secret'srefcan name an Infisical secret by folder, key andversion. Self-hosted and EU instances are named by the URI host,INFISICAL_DOMAIN, or Infisical's legacyINFISICAL_API_URL. Provider selection and Rust API documentation identify Infisical as available from SecretSpec 0.16.
-
Gopass provider (
gopass://) for GPG-based password manager with git-synced password store. -
secretspec exportcommand that resolves every secret for the active profile and writes them to stdout without running a command, in a chosen--format:shell(export KEY='value', foreval "$(secretspec export)"),dotenv,json, orgha(appends to$GITHUB_ENVand emits::add-mask::for each value). Unlikerunit never prompts and exits non-zero on a missing required secret, so CI can gate on it. -
Azure Key Vault provider (
akv://). Authenticates via a service principal whosetenant_id,client_id, andclient_secretcan be sourced as provider credentials (withAZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRETfallbacks), falling back to a signed-in Azure CLI / Azure Developer CLI session when none are available; managed identity and AKS workload identity are also available via?auth=managed_identityand?auth=workload_identity. Sovereign clouds can be addressed with a full DNS hostname or an explicit?suffix=override. Project/profile/key components use lowercase, unpadded Base32 so case and punctuation remain distinct within Azure's restricted, case-insensitive secret-name namespace. -
The
awssmprovider acceptskms_key_idandtag.NAME=VALUEquery parameters (e.g.awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform). Both are applied only when secretspec creates a secret, so accounts that enforce a customer-managed KMS key or "tag-on-create" guardrails (an SCP requiringaws:RequestTag/*onCreateSecret) can now store secrets. A pre-existing secret keeps the key and tags it was created with. -
PHP SDK (
cachix/secretspec): resolve secrets from PHP, Laravel, and Symfony over the same shared resolver as the other language SDKs. It ships as a native PHP extension that embeds the resolver (works under PHP-FPM with noffi.enable, likeext-redis), with anext-ffifallback that dlopens the library at runtime for CLI and local development. -
Provider aliases can now source their own credentials from another provider. An alias in
[providers]may declare acredentialsmap binding a semantic, provider-specific name (such asaccess_token,token,role_id, orclient_secret) to a source: a bare provider spec, which reads the value at the convention path, or a table with arefgiving the exact coordinates. The credential is fetched from that provider and handed to the store, so a machine token can live in the OS keyring instead of a plaintext environment variable, and is never written into the environment of processes started bysecretspec run. A configured credential is authoritative; providers retain their conventional environment fallback when no explicit credential is supplied. Chains are limited to one hop, and that limit is enforced wherever the alias appears, as a chain fallback or the default provider included. Provider credentials also apply when the alias is selected with an explicit--provider <alias>orSECRETSPEC_PROVIDER, and they are fetched from their source once per invocation and profile, then reused across all secrets routed at the alias (convention-path credentials live under a profile, so switching profiles re-reads them). Each source read, and each credential stored throughlogin, is audited with acredentialmarker naming the semantic credential and the source store; a credential stored throughlogintakes effect immediately. Unsupported credential names fail validation before a source is accessed.[providers] bws = { uri = "bws://project-uuid", credentials = { access_token = "keyring" } } akv = { uri = "akv://myvault", credentials = { tenant_id = "keyring", client_id = "keyring", client_secret = "keyring" } } vault = { uri = "vault://kv/app?auth=approle", credentials = { role_id = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "role_id" } }, secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "secret_id" } }, } }
-
secretspec config provider login <alias>prompts for each provider credential a provider alias declares and stores it in its source provider, so it can be read back on the next resolution.secretspec config provider addgains a repeatable--credential NAME=PROVIDERflag for declaring credential sources from the command line.
- Rust SDK validation errors now store their detailed report out of line,
reducing the size of
SecretSpecErrorvalues while preserving diagnostics. - Generated types now describe the values resolution can actually return:
omitted
requiredstill means required, secrets supplied by a manifest default or generator are non-nullable, and profile-specific types include secrets inherited from thedefaultprofile. Profile JSON Schemas are now exhaustive (additionalProperties: false) for the same reason. - A
refrouted at a single store (an explicit--provider, a single-provider chain, or the default provider) is now checked up front, before any store is contacted, for coordinates that store cannot honor (e.g. afieldref pointed at a.envfile), failing fast with a clear message instead of at fetch time. Arefon a multi-store fallback chain is still validated per store as the chain is walked, so a coordinate a later store cannot express never blocks a provider earlier in the chain that can. - Provider chains accept bare provider names and
scheme:pathshorthand (e.g.providers = ["keyring"]), the same specs--provideraccepts. Previously a chain entry had to be a declared alias or a fullscheme://URI. - An explicitly empty
providers = []list now uses the default provider forgetas well, matching howcheckandrunalready treated it. - A
providerschain whose first entry misspellsonepasswordas1passwordnow fails up front with the corrective "useonepasswordinstead" message — the same hard error any other invalid primary gets — instead of warning and falling through to the rest of the chain. As a fallback entry it is still skipped with a warning, like any broken link. - Rust SDK:
ProviderAlias::credentialsis a plain map whose empty state means "no provider credentials", rather than anOption, so the two ways of spelling an alias without credentials cannot diverge.
- The unused public
Config::merge_withandProfile::merge_withmethods. Configuration inheritance (extends) is now applied entirely through the internal overlay used by the loader, so these self-wins merge helpers no longer had any callers.
- Configuration inheritance now loads an
extendshierarchy as a DAG. Shared ancestors in diamond-shaped graphs are applied once instead of being reported as cycles, later entries inextendscorrectly override earlier entries, and profile[defaults]are inherited across source files. - Runtime planning, semantic validation, Rust derive output, and JSON Schema
generation now share one compiled effective-manifest model and one
missing-value policy, preventing raw
required/defaultinterpretation from drifting between surfaces. - Profile overrides no longer need to repeat the secret's
description: validation now checks each secret's effective, merged configuration, so a partial override like[profiles.development] DATABASE_URL = { default = "sqlite:///dev.db" }inherits the description (andtype, forgenerate) from the default profile instead of failing with "missing description". The merged view is also validated for real conflicts, so ageneratesecret in the default profile combined with adefaultvalue from an override or a profile[defaults]table is now rejected at load instead of silently generating a random value and ignoring the default. Validation errors are reported deterministically, attributed to the profile that declares the offending field, andcheckandrunlist secrets in stable name-sorted order. - Provider fallback chains (
providers = [...]) are now tried strictly in order: each link is resolved only when a read actually reaches it, and a broken link (an undefined alias, an unreachable store) is skipped with a warning so a working provider later in the chain still answers.check,run, andgetall walk the chain the same way. getandsetnow record an audit event when a secret's provider routing fails to resolve (for example an undefined alias), matching howcheckandrunaudit every attempted read.- A provider chain entry that misspells
onepasswordas1passwordnow gets the same "useonepasswordinstead" correction that--provider 1passwordgives, instead of a generic undefined-alias error. - Blank or whitespace-only profile and provider overrides (
--profile,SECRETSPEC_PROFILE,--provider,SECRETSPEC_PROVIDER, and the Rust SDK builder) are now trimmed and treated as unset, so a padded value such as a trailing newline from$(cat file)can no longer select a nonexistent profile or provider. importprints its per-secret summary in a stable, name-sorted order.runno longer aborts when the environment contains a non-UTF-8 variable. Such variables are now passed through to the child process untouched, with resolved secrets overlaid on top.- The prebuilt Linux addons of the Node SDK are now built against glibc 2.28
(manylinux_2_28) with libdbus compiled in statically, so
npm install secretspecworks on Amazon Linux 2023, RHEL 8/9, and other distros with an older glibc, instead of the addon failing to load with "version `GLIBC_2.38' not found". (#136)
-
ref: native secret references on secrets: a secret can name one externally managed secret by its store's own coordinates, instead of SecretSpec's{project}/{profile}/{key}naming:[profiles.production] DATABASE_URL = { description = "...", ref = { item = "db", field = "password" }, providers = ["prod_op"] }
itemis the store's own name for the secret (1Password item title, Vault KV path, AWS secret name or ARN,.envkey, environment variable, ...); optional keys refine it where the store supports them:field(1Password field label, Vault KV field, AWS JSON key, keyring account),vaultandsection(1Password), andversion(Google Secret Manager). Every provider resolves refs; coordinates a store has no equivalent for are rejected with a clear error rather than guessed at.The coordinates supply naming only — which store resolves them follows the same routing as every other secret (the secret's
providerschain, the--provider/SECRETSPEC_PROVIDERoverride, or the default provider). That means refs compose withprovidersfallback chains, and an explicit override redirects them like any secret, e.g. at a.envfixtures file during tests. Writes are symmetric where the backend allows it:secretspec setandcheckprompting write through the coordinates in place (1Passwordop item edit, keyring, pass, dotenv, Bitwarden, Proton Pass, LastPass); Vault, AWS, and GCSM refs are read-only. Secrets sharing identical coordinates fetch once, and audit events record the coordinates in a newreffield. Arefalso composes withgenerate: a missing referenced secret is minted and written straight to its coordinates. -
Inline provider URIs in
providerschains: chain entries that are already URIs (providers = ["onepassword://Production", "keyring"]) now pass through without declaring a[providers]alias first.
- Faster multi-provider resolution:
check,run, and SDK resolution now group secrets by store and fetch the groups concurrently instead of one after another; within a group,refsecrets batch through the store's bulk surface where it has one (AWSBatchGetSecretValue, the single Bitwarden, Proton Pass, and 1Password listings) and otherwise resolve concurrently, each unique coordinate fetched once. CLI authentication (1Password, LastPass, Proton Pass) is probed once per account/session instead of once per provider instance. - Provider trait speaks one address vocabulary (affects custom providers
built on the Rust library): each provider now compiles SecretSpec's
{project}/{profile}/{key}convention into its native coordinates via a new requiredconvention_addressmethod, and reads resolve every address through the same coordinate path arefuses. The convention-onlyget_batchmethod is replaced byget_many, which takes addresses and so batchesrefsecrets too. A provider declares therefcoordinates it honors withsupported_coordsand the rest are rejected for it, andallows_setis replaced bycheck_writable, which returns the reason a write is refused rather than a barefalse. - Manifest validation runs on load: the semantic rules
secretspec.tomldocuments (a required secret cannot carry adefault,generateneeds atype,refcoordinates must be non-empty and non-whitespace) are now enforced whenever the config is loaded. Configs that silently violated them previously will now fail with a pointed error.
- onepassword: URIs carrying an item path (e.g. the
onepassword://vault/Productionform some older docs showed) previously discarded the path silently and targeted a vault literally namedvault. Item paths — including pastedop://vault/item/fieldreferences — now fail with an error spelling out the exactrefcoordinates to write instead. seton a read-onlyrefreported "Provider '' is read-only and does not support setting values", which is untrue of Vault, AWS, and GCSM — they write the conventional layout fine and refuse only refs. The store's own reason is now shown (e.g. writing one Vault field would clobber the sibling fields at the same KV path).
- Language SDKs for Python, Go, Ruby, Node.js / TypeScript, and Haskell
(
secretspec-py,secretspec-go,secretspec-rb,secretspec-node,secretspec-hs). Resolve the secrets declared in yoursecretspec.tomlfrom each language using the same providers, profiles, fallback chains, and generators as the CLI and the Rust SDK — no per-language configuration. Each mirrors the Rust derive crate's vocabulary: a builder taking a provider, profile, and access reason;load()returns the resolved secrets and can export them into the process environment, while a value-freereport()previews how each secret would resolve without reading any value. A missing required secret raises a typed error;as_pathsecrets are returned as a readable file path, with an explicit (or scope-based) cleanup that removes the backing temp file. secretspec-fficrate: a small, versioned C ABI for resolving secrets from any language, plus the public Rust building blocks the SDKs are built on (Secrets::resolve()andSecrets::report()). Use it to write a binding for a language we do not ship yet.secretspec schema: emits a JSON Schema for your manifest's typed shape (the union of all profiles, or one profile via--profile). Feed it to quicktype to generate idiomatic typed classes in any language, populated from each SDK'sfields()map — type-safe secret access without hand-writing a generator per language.secretspec check --json/--explain: a value-free report of how every declared secret resolves for the active profile — its status (resolved,missing_required,missing_optional), where the value would come from (a provider, with a credential-free URI; a generator; or a committed default), and whether it is exposedas_path. Values are never included, and both flags skip the interactive prompt and exit non-zero when a required secret is missing, so CI can gate on them. The same report is available to the Rust SDK viaValidatedSecrets::report()/ValidationErrors::report().
- A per-secret provider chain whose primary provider errors (e.g. an unreachable
vault) and whose fallback chain yields no value now surfaces that provider error
instead of silently reporting the secret as
missing_required, so a provider outage is distinguishable from an unprovisioned secret.
- The
passprovider accepts astore_dirquery parameter (e.g.pass://?store_dir=/path/to/store) to use a password store directory other than the default~/.password-store. It is applied asPASSWORD_STORE_DIRscoped to eachpassinvocation.
- Provider URIs now correctly round-trip query parameters whose values contain
characters that are significant in a query string (
&,+,#,%, and spaces). Previously such characters in theawssmprefix(and the newpassstore_dir) were emitted unescaped, so the value could be silently truncated or altered when the URI was parsed back. secretspec import <FROM>now accepts a provider alias (from[providers]or the global[defaults.providers]) as its source, not just a literal provider URI. Passing an unknown provider or alias now reports the available aliases.
- Windows: a
dotenv://provider URI built from an absolute path (e.g.dotenv://C:\path\.env) no longer fails to parse with "invalid port number". The drive-letter colon was being read as ahost:portseparator; such paths are now carried through the URL intact. - Windows: the audit log no longer fails to reset at its size cap. Truncation on the append-only handle was denied by the OS; it now truncates through a separate write handle.
- Relative
dotenvpaths (e.g.dotenv:.config/.env) now resolve against the directory containingsecretspec.tomlinstead of the current working directory. Runningsecretspec run --file ../secretspec.tomlfrom a subdirectory previously failed to find the referenced.envfile because it was looked up relative to the working directory rather than the project root (#59). Absolutedotenvpaths are unaffected. - The
protonpassprovider now works with Proton Pass CLIpass-cli >= 2.0.3. Theitem list --output jsonpayload changed shape in 2.0.3 (the item title moved from a nestedcontent.titleto a top-leveltitle, andcontentwas dropped from list output), which madesecretspecreport active secrets as missing. Both the old (<= 2.0.2) and new (>= 2.0.3) list shapes are now accepted. (#104)
- Audit logging for secret access, on by default. Every secret read and write,
from both the CLI and the Rust SDK, is appended to a local per-user log as JSON
Lines. Only metadata is recorded (secret names, the serving provider with any
embedded credentials redacted, outcome, reason, and actor including a detected
coding agent); secret values are never written. Each operation is recorded once:
getandsetper secret,checkas a single event,runwhen the child process starts, andimportper copied secret. Auditing never blocks secret access; if it cannot write the log it warns on stderr and continues. The log is a single file capped at 1 MiB. It is configured per machine via the[audit]table in~/.config/secretspec/config.toml(not the project'ssecretspec.toml), so a cloned repository cannot redirect or silence it. The newsecretspec auditcommand reads the log, with--project,--action,--tail/-n, and--jsonfilters. See Audit Logging for details. --reasonCLI flag (andSECRETSPEC_REASONenv var) records a human-readable reason for a session's secret access, forwarded to providers that support audit logging.SECRETSPEC_REASONis honored across the SDK/library too: it is resolved bySecrets::load/load_from(sosecretspec-derive-generated code and other library callers can satisfy therequire_reasonpolicy and supply an audit reason without code changes), andSecrets::with_reason(...)sets it explicitly, taking precedence. Thesecretspec-derive-generated typed builder also gains awith_reason(...)method, so SDK callers can satisfyrequire_reasonin code (not only via the env var). Blank or whitespace-only reasons are ignored so they cannot satisfy the policy. Backed by a newProvider::set_reasontrait method (default no-op).[project] require_reasonpolicy insecretspec.toml, controlling when secret access must supply an explicit reason. Accepts"agents"(the default — require a reason only when an AI agent is detected),true(require it from every caller), orfalse(never). Agent detection is delegated to thedetect-coding-agentcrate (Claude Code, Cursor, Codex, Gemini CLI, Copilot, ...), plus aSECRETSPEC_AGENTopt-in for harnesses it does not recognize. Because the tool enforces it and it is checked into the repo, the policy applies uniformly and cannot be bypassed by an individual tool's configuration. An invalidrequire_reasonvalue is rejected at config-parse time rather than silently falling back to the default. The policy is inherited throughextends: a shared base config'srequire_reasonapplies to every config that extends it, unless the child sets its own. Note: the default"agents"means AI agents must now pass a reason out of the box.bwsprovider now accepts an optional server base in the URI (bws://[server-base@]project-uuid) to target EU cloud or self hosted Bitwarden instances. When set, the identity and API endpoints are derived ashttps://<server-base>/identityandhttps://<server-base>/api; omitting it keeps thebitwarden.comUS cloud default.
- Minimum supported Rust version raised to 1.92 (required by the
detect-coding-agentdependency). The devenv toolchain is pinned accordingly.
- Proton Pass provider now works with
pass-cli>= 2.1.0 agent sessions. Since 2.1.0, audited item operations (item view,item create,item delete) fail unlessPROTON_PASS_AGENT_REASONis set, which made existing secrets appear missing under an agent session. The provider now sets this variable on everypass-cliinvocation. The reason is resolved as--reason/with_reason, thenPROTON_PASS_AGENT_REASON, then a secretspec-versioned default (secretspec/<version> (https://secretspec.dev)); each source is normalized first, so a blank reason falls through to the next rather than masking it. It is ignored by older releases and non-agent sessions. secretspec initnow serializes the generatedsecretspec.tomlwithtoml_editinstead of hand-interpolating strings. This fixes several cases that previously produced TOML that could not be parsed back: a project name, secret description, or default value containing a double-quote, backslash, control character (including U+007F), or newline; a secret name containing a dot (e.g.FOO.BAR, which dotenvy accepts and which silently collapsed to a nested key); and a configuredproject.extends, which was dropped entirely. Output is now also deterministically ordered.secretspec initno longer defines a conflicting-fshort flag for--from;-fis reserved for the global--fileoption. The duplicate short flag madesecretspec initpanic in debug builds and was ambiguous in release builds.
- AWS Secrets Manager (
awssm) provider: support for a?prefix=query parameter in the provider URI (e.g.,awssm://us-east-1?prefix=myteam). The prefix is prepended to all secret names (myteam/secretspec/{project}/{profile}/{key}). Closes #92. - Provider aliases can now be declared at the project level in a top-level
[providers]table ofsecretspec.toml. Aliases declared there are visible to per-secretproviders = [...]lists and to--provider/SECRETSPEC_PROVIDER, and are merged with the existing user-level[defaults.providers]map in~/.config/secretspec/config.toml. On name conflicts the project entry wins, so a team's checked-in mapping cannot be silently shadowed by a stale local config. Closes #79 and addresses the "share aliases via VCS" half of #90.
- Profile-not-found errors no longer surface as the confusing
Secret 'Profile 'X' not found' not found. They now use the dedicatedInvalidProfilevariant and include the list of profiles defined insecretspec.toml, e.g.Invalid profile: 'production' is not defined in secretspec.toml. Available profiles: default, dev. Affectscheck,run,get,set, andimport. Surfaced via #79.
secretspec check: optional secrets that aren't set no longer render with a green✓and aren't counted as "found" in the trailing summary. They now display with the same blue○ (optional)styling already used in the missing-required path, and the summary appends, N optionalwhenever optional secrets are absent (e.g.Summary: 4 found, 0 missing, 1 optional). If every optional secret is set, the summary line stays in its previousX found, Y missingform. Fixes #72.
- Proton Pass provider that stores secrets in a Proton Pass vault via the
proton-passCLI. Configured asprotonpass://<vault>; items are organized per project / profile and read / write both go through the CLI.
- OnePassword provider: the auth preflight now probes
op vault listinstead ofop whoami. Under the 1Password desktop app's delegated-session integration,op whoamireportsaccount is not signed ineven whenop item get/op vault listwork fine — so every secret read or write failed at preflight with a misleading "not signed in" error.op vault listexercises the actual access path and succeeds when the desktop app can serve secrets. Additionally,OP_SESSION_*environment variables (left over fromeval $(op signin)) are now stripped before spawningopso a stale shell session can't shadow the desktop integration. Auth failure and install hints now point users at desktop integration as the primary local-dev path. Fixes #80. - Vault / OpenBao provider: HTTPS requests now trust certificates from the
operating system trust store (and honor
SSL_CERT_FILE/SSL_CERT_DIR), so servers fronted by a private / internal CA work without modification. Previously the bundledwebpki-rootsset was the only trust anchor and any non-public CA producedFailed to connect to Vault ... error sending request. Switches thereqwestworkspace dependency fromrustls-tlstorustls-tls-native-roots. Fixes #85.
- Dropped the
serde-envfiledependency in favor of a small in-tree.envserializer. The previous git-pinned fork blocked publishing to crates.io; the new serializer applies the same escapes (backslash, double quote, dollar, newline) that the fork added and emits keys in sorted order for stable diffs.
- The
--providerCLI flag now correctly takes precedence over theSECRETSPEC_PROVIDERenvironment variable. Previously the env var was consulted before the value forwarded from--provider(viaset_provider), so users could not temporarily override the provider on the command line while the env var was set. Fixes #77. - Per-secret
providers = [...]chains now behave as a true fallback chain when an upstream provider errors (e.g. a 403 from a vault the current user cannot access). Previously the first provider's error short-circuited the whole operation; now the error is logged as a warning and the next provider in the chain is tried. The original error is only surfaced if every provider in the chain failed (so genuine outages still bubble up), or if the secret has no alternative to fall back to. Fixes #83. secretspec runnow removes the temporary files it creates foras_path = truesecrets after the child process exits. Previously the files were leaked under/tmpbecausestd::process::exitskipped the destructors that own them. Fixes #71.- Provider URIs now support spaces and special characters in names
(e.g.,
onepassword://Home Lab). All providers receive automatically percent-decoded values via a newProviderUrlwrapper type. - dotenv provider: setting a secret no longer corrupts neighboring values
that contain double quotes, backslashes, dollar signs, or newlines
(e.g. JSON values). The underlying
serde-envfileserializer did not escape these characters; fix is pinned via a fork until lucagoslar/serde-envfile#6 lands upstream. Fixes #74. --provider(andSECRETSPEC_PROVIDER) is now honored on every command even when aproviders = [...]chain is configured for the secret or profile. Previouslyset,get,check,import, andrunsilently used the first provider in the chain and ignored the explicit override, makingsecretspec set --provider <alias>a no-op against the requested target. The flag now consistently takes precedence:set/import/ generation write only to the chosen provider, andget/validateread only from it (no chain fallback). Provider aliases declared in~/.config/secretspec/config.tomlcan now be passed directly to--provider. Fixes #81.
- BWS (Bitwarden Secrets Manager) provider with async SDK integration, secret caching, and full read-write support (requires
--features bws)
secretspec-derivenow depends onsecretspecwithdefault-features = false, avoiding pulling in CLI and provider features when only the derive macro is used.
- All provider features (
gcsm,awssm,vault) are now enabled by default - AWS Secrets Manager (
awssm) provider: batch fetching viaBatchGetSecretValueAPI, reducing N sequential API calls to ceil(N/20) batched calls. For 30 secrets this means 2 API calls instead of 30. Note: requires thesecretsmanager:BatchGetSecretValueIAM permission in addition to existing permissions.
rsa_private_keysecret generation type: generates RSA private keys in PKCS1 PEM format, defaults to 2048 bits, configurable viagenerate = { bits = 4096 }
- Check provider authentication (e.g. OnePassword, LastPass) before prompting
user for secrets, via a
PreflightGuardthat runs the check exactly once per provider instance
- HashiCorp Vault / OpenBao (
vault) provider for Vault KV v1/v2 secret storage, with support for namespaces, TLS configuration, and OpenBao compatibility (requires--features vault) - AWS Secrets Manager (
awssm) provider for AWS secret storage integration (requires--features awssm) - Support running secretspec from subdirectories: the CLI now walks up the directory tree to find the nearest
secretspec.toml, similar tocargoandgit. Also adds a-f/--fileflag (andSECRETSPEC_FILEenv var) to explicitly specify the config file path (#59)
- Extract shared
block_onasync helper from AWSSM and GCSM providers intoprovider::block_on
- GCSM provider no longer panics when called from within an existing tokio runtime
- Keyring and pass providers now support
folder_prefixvia URI (e.g.,keyring://secretspec/shared/{profile}/{key}) to share secrets across projects, matching the existing OnePassword and LastPass behavior
- Support
XDG_CONFIG_HOMEon macOS by switching fromdirectoriestoetceteracrate. Existing macOS configs at~/Library/Application Support/secretspec/are automatically migrated to~/.config/secretspec/(#28)
- Reject empty values when setting a secret
- Improved interactive prompt for missing secrets: lists all missing secrets upfront with descriptions, adds step counter (
[1/3]), and usesinquire::Passwordfor consistent masked input. Removedrpassworddependency.
- Use a fork of inquire to support setting multi-line secrets (#32)
- Declarative secret generation: secrets can now be auto-generated when missing by adding
typeandgeneratefields to secret config. Supported types:password,hex,base64,uuid, andcommand(for arbitrary shell commands). Generation triggers duringcheck/runwhen a secret is missing, and the generated value is stored via the configured provider.
- OnePassword provider: Significant performance improvement by caching authentication status and using batch fetching with parallel threads. Reduces CLI calls from 2N sequential to ~2 sequential + N parallel for N secrets.
- CLI: Add
--no-prompt(-n) flag tosecretspec checkcommand for non-interactive mode. When used, the command exits with non-zero status if secrets are missing instead of prompting for values. Useful for CI/CD pipelines, scripts, and automation. (#55)
- OnePassword provider: Fix duplicate item creation when existing item has no extractable value.
Now uses
op item listfor existence checks and updates by item ID to avoid ambiguity. - OnePassword provider: Handle "More than one item matches" error gracefully by falling back to ID-based lookup.
- Google Cloud Secret Manager (GCSM) provider for GCP secret storage integration (#53)
- LastPass provider: Fix creating new secrets by using correct
lpass addcommand instead of non-existentlpass set(#54)
- CI: Updated macOS runners from deprecated macos-13 to macos-15 (Intel) and macos-latest (ARM)
- Pass (password-store) provider for Unix password manager integration
ensure_secrets()method is now public in the Rust SDK- Support specifying full file paths (ending in
.toml) inextendsfield, in addition to directory paths
- Performance: avoid double validation in
check()for happy path
- Display correct error message when extended config file is not found, instead of the misleading "No secretspec.toml found in current directory" error
- OnePassword provider: Support for
SECRETSPEC_OPCLI_PATHenvironment variable to specify custom path to the OnePassword CLI - OnePassword provider: Automatic detection of Windows Subsystem for Linux 2 (WSL2) and use of
op.exeon that platform - Documentation for
as_pathoption in configuration reference, Rust SDK docs, and landing page - Documentation for per-secret providers with fallback chains on landing page
- OnePassword provider: Use stdin instead of temporary files when creating items for WSL2 compatibility (WSL paths are invalid when passed to Windows executables)
- Output status/progress messages to stderr instead of stdout, fixing direnv integration where stdout was evaluated as shell code
- Profile-level default configuration:
profiles.<name>.defaultssection for shared settings across secrets in a profile - Default providers for profiles: define common providers once and have all secrets use them unless overridden
- Default values and required settings can now be specified at profile level to reduce repetition
as_pathoption for secrets: write secret values to temporary files and return the file path instead of the value. Temporary files are automatically cleaned up when the resolved secrets are dropped in Rust SDK usage. For CLI commands (getandcheck), temporary files are persisted and NOT deleted after the command exits. In the Rust SDK, fields withas_path = trueare generated asPathBuforOption<PathBuf>instead ofString
- Secret
requiredfield is nowOption<bool>to allow profile-level defaults to apply when not explicitly set - Secret
defaultfield can now inherit from profile-level defaults if not specified per-secret - Secret
providersfield can now inherit from profile-level defaults if not specified per-secret - Profile defaults only apply to secrets that don't explicitly set these fields
Secrets::check()now returnsResult<ValidatedSecrets>instead ofResult<()>, allowing callers to access the validated secrets
- CLI: Count optional secrets as "found" in the summary
- Support for piping multi-line secrets via stdin
- Import command now resolves secrets from all profiles, not just the active profile (fixes issue #36)
- Fix incorrect stats in the summary for certain configurations
- Installers for arm/linux
- Integrate
secrecycrate for secure secret handling with automatic memory zeroing - Add
reflect()method to Provider trait for provider introspection - Export
Providertrait from secretspec crate for use in derived code
- Made keyring provider optional via
keyringfeature flag (enabled by default) - Unified provider parsing logic in init command to support all provider formats consistently
- Downgraded keyring dependency to 3.6.2
- Updated
with_providerin derive macro to acceptTryInto<Box<dyn Provider>>for consistent provider handling
- Fixed secret optionality logic: having a default value no longer makes a secret optional in generated types
- SDK: Added
set_provider()andset_profile()methods for configuration - SDK: Removed provider/profile parameters from
set(),get(),check(),validate(), andrun()methods - SDK: Embedded Resolved inside ValidatedSecrets
- Fix stdin handling for piped input in set/check commands
- Fix SECRETSPEC_PROFILE and SECRETSPEC_PROVIDER environment variable resolution
- Ensure CLI arguments take precedence over environment variables
- add CLI integration tests
- Update test script to handle non-TTY environments correctly
- SDK: Hide internal functions
secretspec --version
- Profile inheritance: fields are merged with current profile taking precedence
Initial release of SecretSpec - a declarative secrets manager for development workflows.