This is the full developer documentation for SecretSpec # Basic Usage > The SecretSpec commands you will use most often Once your project has a `secretspec.toml` file and you have selected a default provider, most day-to-day work uses a small set of commands. ## Check required secrets [Section titled “Check required secrets”](#check-required-secrets) Check that every required secret can be resolved. Missing values are shown without printing any secrets, and SecretSpec offers to set them interactively: ```bash $ secretspec check ``` Use `secretspec check --no-prompt` in CI or other non-interactive environments. It exits with an error when a required secret is missing. ## Store or replace a value [Section titled “Store or replace a value”](#store-or-replace-a-value) Set a secret without putting its value in your shell history: ```bash $ secretspec set API_KEY Enter value for API_KEY (profile: development): ******** ✓ Secret 'API_KEY' saved to keyring (profile: development) ``` Running `set` again replaces the stored value. The secret must already be declared in `secretspec.toml`. ## Read one value [Section titled “Read one value”](#read-one-value) Resolve and print a single secret: ```bash $ secretspec get DATABASE_URL postgresql://localhost/myapp ``` Caution `get` prints the secret as plain text. Avoid using it in shared terminals, logs, or scripts. Use `run` or an SDK when an application needs the value. ## Run your application [Section titled “Run your application”](#run-your-application) Start a command with the resolved secrets available as environment variables: ```bash $ secretspec run -- npm start ``` The `--` separates SecretSpec’s options from the command you want to run. SecretSpec stops before starting the command if a required secret is missing. ## Add a declaration [Section titled “Add a declaration”](#add-a-declaration-018) **New in version 0.18** Declare a new secret without editing `secretspec.toml` by hand, then store its value: ```bash $ secretspec add API_KEY --description "API access token" $ secretspec set API_KEY ``` `add` changes only the declaration. It never asks for or stores the secret value. ## Delete stored values [Section titled “Delete stored values”](#delete-stored-values-018) **New in version 0.18** Remove a stored value from its provider: ```bash $ secretspec delete API_KEY ``` This leaves the declaration in `secretspec.toml`, so the project still records that it expects `API_KEY`. See the [CLI reference](/reference/cli/#delete-018) for deleting multiple values or using `--all`. ## Use another profile or provider [Section titled “Use another profile or provider”](#use-another-profile-or-provider) Your configured defaults apply automatically. Override them for one command with `--profile` or `--provider`: ```bash $ secretspec check --profile production $ secretspec run --provider dotenv://.env.test -- npm test ``` These options do not change your saved preferences. ## Next steps [Section titled “Next steps”](#next-steps) * See every option in the [CLI command reference](/reference/cli/) * Learn how [profiles](/concepts/profiles/) separate environments * Explore available [providers](/concepts/providers/) # But I Use SOPS > SOPS encrypts files. SecretSpec gives applications a portable contract for the secrets they need. Whenever I show someone SecretSpec, I often hear the same response: > But I use SOPS. [SOPS](https://getsops.io/docs/) is good. It encrypts files so they can live in Git without exposing their plaintext values. But SecretSpec solves a different problem: how applications declare, find, and consume secrets. ## How does your application use the secret? [Section titled “How does your application use the secret?”](#how-does-your-application-use-the-secret) Once you have encrypted `secrets.yaml`, how does your Python service consume it? What about your Go worker or Node.js app? You still need to decrypt the file, inject its values, select the right file for each environment, validate required keys, and repeat that integration for every language. And if you release the project as open source, that choice does not stay yours. With SOPS baked into the setup, everyone who runs or contributes to the project must adopt SOPS and its key management, whatever secrets tooling they already use. SecretSpec starts at the other end. The project [declares what the application needs](/concepts/declarative/) without storing any values: secretspec.toml ```toml [project] name = "payments" revision = "1.0" [profiles.default] DATABASE_URL = { description = "Postgres connection string" } STRIPE_API_KEY = { description = "Stripe secret key" } ``` The same secret can come from a developer’s [system keyring](/providers/keyring/) or CI [environment variables](/providers/env/), while a more sensitive production environment resolves it from [Vault](/providers/vault/). Applications use the same declaration through nine SDKs for [Rust](/sdk/rust/), [Python](/sdk/python/), [Go](/sdk/go/), [Ruby](/sdk/ruby/), [Node.js/TypeScript](/sdk/nodejs/), [Haskell](/sdk/haskell/), [PHP](/sdk/php/), [C#](/sdk/csharp/), and [Swift (0.18+)](/sdk/swift/) without knowing the provider. Encrypted files also make the key workflow a project-wide requirement. Adding a teammate means adding their key to `.sops.yaml` and re-encrypting every file; removing one means rekeying and rotating the affected secrets, since their key already saw the plaintext. SecretSpec leaves identity and access to the provider: onboarding to Vault or a cloud secrets manager is granting a role, and offboarding is revoking it. SOPS may be enough today. As your team grows more sensitive to how secrets are handled, you may want Vault’s access policies and centralized audit trail. If applications know about SOPS, each one needs migrating. If they know only SecretSpec, you change the [provider configuration](/concepts/providers/); SDK calls and secret names stay the same. The same resolver provides [profiles](/concepts/profiles/), [required-secret checks](/reference/configuration/#secret-variable-options), [per-secret provider routing and fallback](/concepts/providers/fallback/), [provider-native references](/concepts/references/), [temporary files](/reference/configuration/#as_path-option), and [metadata-only audit logs](/concepts/audit/). You build the integration once, not once per provider and language. ## Different layers, different jobs [Section titled “Different layers, different jobs”](#different-layers-different-jobs) SOPS protects a file. SecretSpec gives applications a provider-independent interface. The selected provider remains responsible for storage, encryption, identity, access control, and availability. I wrote a fuller [SecretSpec comparison](/comparison/) showing exactly where SecretSpec ends, where providers begin, and which responsibilities belong to each layer. ## Where SecretSpec goes next [Section titled “Where SecretSpec goes next”](#where-secretspec-goes-next) Because applications talk to an interface instead of a file, the interface can grow without touching them. Three open proposals point where it is heading: * [Project security requirements](https://github.com/cachix/secretspec/issues/188) would let a project declare the guarantees a provider must meet, such as encryption at rest or an audit trail, and reject providers that fall short. * [Lease-aware refresh](https://github.com/cachix/secretspec/issues/11) would let running applications follow key rotation and short-lived credentials instead of restarting for a new value. * The [SOPS provider](/providers/sops/) (0.17+) brings SOPS itself behind the same SDK interface, making your encrypted files one more place secrets can come from. With that provider, perhaps “But I use SOPS” just needs two more words: > But I use SOPS with SecretSpec. If encrypted files fit your workflow, keep using SOPS. Just recognize the boundary: encryption at rest is not an application secrets interface. # Claude Code Stores OAuth Tokens in Plaintext > On Linux, Claude Code protects MCP OAuth credentials with file permissions—not encryption. It should let users choose a real secret store. Claude Code’s [MCP documentation says authentication tokens are “stored securely”](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers). On Linux, that currently means plaintext JSON protected by file permissions. I checked Claude Code 2.1.257 after authenticating to several remote MCP servers. The file `~/.claude/.credentials.json` had mode `0600`, as it should, but it also contained a top-level `mcpOAuth` object with the access tokens. Here is the shape of one Cloudflare entry, with every credential value redacted: \~/.claude/.credentials.json ```json { "mcpOAuth": { "cloudflare-observability|…": { "accessToken": "", "clientId": "", "discoveryState": "", "redirectUri": "", "serverName": "cloudflare-observability", "serverUrl": "" } } } ``` This matches Anthropic’s [credential-management documentation](https://code.claude.com/docs/en/team#credential-management). * **macOS:** uses the encrypted macOS Keychain, falling back to `~/.claude/.credentials.json` when the Keychain is unavailable. * **Linux:** uses `~/.claude/.credentials.json` with mode `0600`. * **Windows:** uses `%USERPROFILE%\.claude\.credentials.json`, inheriting the access controls of the user’s profile directory. That is a much narrower claim than most people hear when a product says a credential is “stored securely.” ## OAuth did not solve secret storage [Section titled “OAuth did not solve secret storage”](#oauth-did-not-solve-secret-storage) The browser flow makes the secret easy to miss. Run `claude mcp login`, approve access in the browser, and return to a connected MCP server. Nobody manually created a token, copied it from a dashboard, or pasted it into a configuration file. But Claude Code still received a credential. It must persist that credential if the connection is to survive a restart. OAuth is valuable here. Claude Code can discover the authorization server, request specific scopes, complete the authorization-code exchange, refresh an access token, and revoke the grant. Anthropic’s [MCP documentation](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers) also lets users pin the scopes Claude Code requests. What OAuth does not specify is a secure local vault. API tokens can also be scoped, limited to particular resources, assigned an expiry, rotated, and revoked independently. OAuth standardizes delegation and renewal, while avoiding the copy-and-paste ceremony. Those are substantial benefits, but they do not turn the resulting bearer token into something that is safe to leave in plaintext. | Property | OAuth credential | Scoped API token | | ------------------------------- | ---------------- | ----------------- | | Must be stored by the client | Yes | Yes | | Can have limited permissions | Yes | Yes | | Can expire | Yes | Yes | | Can be revoked independently | Usually | Yes | | Can be replayed if stolen | Yes | Yes | | Standard interactive delegation | Yes | Provider-specific | | Standard automatic renewal | Often | Usually external | OAuth solves how Claude Code obtains and renews a delegated credential. Secret storage solves what happens to that credential between uses. They are separate concerns. ## Claude Code needs a credential-store interface [Section titled “Claude Code needs a credential-store interface”](#claude-code-needs-a-credential-store-interface) Claude Code should not decide that every Linux user’s MCP tokens belong in the same plaintext file. The persistence layer should be replaceable: ```text Claude Code OAuth client │ ▼ credential-store interface │ ▼ SecretSpec TypeScript SDK │ ▼ user-selected SecretSpec provider ``` The right integration point is the [SecretSpec Node.js / TypeScript SDK](/sdk/nodejs/). It embeds the Rust resolver, so the TypeScript side does not need bespoke code for each backend. Claude Code could serialize one MCP OAuth credential per server and ask SecretSpec to load, save, or delete it using the provider the user or organization selected. SecretSpec 0.20 has [33 provider integrations](/concepts/providers/#available-providers). They cover local keyrings, password managers, encrypted files, cloud secret managers, and deployment destinations. Providers declare their capabilities, so a credential store can require readable and writable storage while still using the same interface everywhere. The current TypeScript SDK exposes SecretSpec’s provider-independent resolver. We would add the small `get`/`set`/`delete` credential-store surface Claude Code needs rather than reimplement 33 integrations in its codebase. For example, set the [system keyring](/providers/keyring/) as the default provider in the local user configuration: ```bash $ secretspec config global init --provider keyring --profile default ``` A team might instead require [OpenBao](/providers/openbao/) or a cloud secret manager. A headless workstation might use an [age-encrypted store](/providers/age/). The OAuth flow would stay exactly the same; only persistence would change. > **What Codex does:** Codex makes MCP OAuth storage configurable. Its [configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference#mcp_oauth_credentials_store) documents `auto`, `file`, and `keyring` backends. Setting `mcp_oauth_credentials_store = "keyring"` selects the system keyring. This is not a general secret-provider interface, but it avoids making a plaintext credential file the only option on Linux. > **Coming in SecretSpec 0.21:** We are working on [versioned resolver and provider IPC](https://github.com/cachix/secretspec/pull/362) for zero-dependency integrations. Applications will be able to use SecretSpec providers over a local protocol without embedding an SDK or provider code. ## Making open source software more secure [Section titled “Making open source software more secure”](#making-open-source-software-more-secure) We are working to make open source tools retrieve credentials from a user-selected secret store instead of copying them into another plaintext file. SecretSpec now provides [Git](/integrations/git/) and [Docker](/integrations/docker/) credential helpers, and we have proposed a generic, operation-scoped [secret resolver interface for Nix](https://github.com/NixOS/nix/pull/16339). Because these projects are open source, we can inspect their credential boundaries and contribute safer ones upstream. We cannot make the equivalent fix in Claude Code. Its [public repository](https://github.com/anthropics/claude-code) does not include the core CLI implementation, and its [license is all rights reserved](https://github.com/anthropics/claude-code/blob/main/LICENSE.md). We can support the [open request for secure, pluggable credential storage](https://github.com/anthropics/claude-code/issues/73582) and propose a SecretSpec TypeScript integration, but only Anthropic can change Claude Code’s MCP OAuth storage today. That is one of the practical security benefits of open source: when a secret crosses the wrong boundary, users do not have to wait for the vendor to decide that the boundary matters. # Secrets Don’t Belong in Config > An audit of 445 NixOS modules shows the cost of making configuration and secrets share one interface. Applications should not require passwords, API keys, or tokens in their configuration files. Configuration describes behavior. It belongs in git, code review, bug reports, and developer machines. A secret grants authority. It needs restricted access and independent rotation. Putting both in one file couples different lifecycles and audiences. If rotating a password requires regenerating application configuration, the interface has coupled them too tightly. ## NixOS contains 110 workarounds for this [Section titled “NixOS contains 110 workarounds for this”](#nixos-contains-110-workarounds-for-this) We [audited all 445 NixOS modules that handle a real secret](https://github.com/NixOS/nixpkgs/issues/24288#issuecomment-5024009774) in nixpkgs at commit `141f212`, classifying each by where its secret value ends up. | Where the secret value ends up | Modules | Share | | ------------------------------------------ | ------: | ----: | | Merged into a config file at runtime | 110 | 25% | | Inlined into a config in `/nix/store` | 42 | 9% | | Delivered as an environment variable | 161 | 36% | | Left in a dedicated file opened by the app | 58 | 13% | | Loaded through systemd credentials | 53 | 12% | | Passed as a command-line argument | 19 | 4% | | Classification uncertain | 2 | — | The interesting number is 110. A quarter of the modules retrieve a secret safely, then copy it into configuration because that is the only interface the application accepts. These modules use `envsubst`, `replace-secret`, `jq`, `yq`, `sed`, or custom code to assemble a restricted file at startup. The result can be secure, but every module now owns application-specific, security-sensitive glue just to combine two inputs that should have remained separate. This is not unique to NixOS. The same workaround appears as an entrypoint script, Helm template, init container, or CI interpolation step on other platforms. As a side note, 42 modules can inline secrets into the world-readable `/nix/store`. That direct security problem is tracked in [nixpkgs issue #24288](https://github.com/NixOS/nixpkgs/issues/24288). The 110 runtime mergers make the broader point: even when deployment authors avoid the leak, the missing separation still creates work. ## Give secrets their own interface [Section titled “Give secrets their own interface”](#give-secrets-their-own-interface) Applications should accept secret values through a dedicated runtime channel, such as: * a `password_file` or `token_file` setting; * a systemd credential; * a narrowly scoped environment variable; * or an external secret provider. These mechanisms are not equally safe: environment variables can be inherited, arguments can appear in process listings, and files still need correct permissions. What separation does guarantee is that the deployer no longer has to manufacture a second, secret-bearing version of the configuration. The principle is simple; implementing it across environments is not. Local development might use a system keyring, CI environment variables, and production 1Password or Vault. Without a shared abstraction, each environment needs its own naming, lookup, validation, and injection glue. ## How I got it wrong in Cachix [Section titled “How I got it wrong in Cachix”](#how-i-got-it-wrong-in-cachix) Cachix historically stored its auth token and per-cache signing keys in `~/.config/cachix/cachix.dhall`, alongside cache names and other configuration. It was convenient, but the file had to be treated as a secret even though much of it was ordinary configuration. A typical file mixed them directly: \~/.config/cachix/cachix.dhall ```dhall { authToken = "XXX-AUTH-TOKEN" , binaryCaches = [ { name = "mycache" , secretKey = "XXX-SIGNING-KEY" } ] } ``` The cache name is configuration; the auth token and signing key are secrets. You could not share the cache configuration without also sharing credentials. [devenv 2.2 separates the token through SecretSpec](https://devenv.sh/binary-caching/#setup-with-secretspec-recommended). The project declares `CACHIX_AUTH_TOKEN`, devenv resolves it from the configured provider, and the value is passed to Cachix without being added to devenv’s configuration. [Cachix PR #737](https://github.com/cachix/cachix/pull/737) brings the same boundary into the client through the SecretSpec Haskell SDK. It resolves `CACHIX_AUTH_TOKEN` and `CACHIX_SIGNING_KEY` from SecretSpec and can store them in the user’s chosen provider instead of `cachix.dhall`. Existing environment variables and config files remain higher-priority fallbacks for compatibility. The PR is still open. That is the problem SecretSpec is designed to solve: configuration declares the requirement, while each environment chooses where the value lives. ## Declare once, resolve anywhere [Section titled “Declare once, resolve anywhere”](#declare-once-resolve-anywhere) SecretSpec applies that separation by making `secretspec.toml` a declaration of what an application needs, without storing the values: secretspec.toml ```toml [project] name = "myapp" [profiles.production] DATABASE_URL = { description = "Postgres connection string" } STRIPE_API_KEY = { description = "Stripe secret key" } ``` [Providers](/concepts/providers/) decide where the values live. A developer can use the system keyring, CI can use environment variables, and production can use 1Password, Vault/OpenBao, or a cloud secret manager without changing the declaration. An existing application can receive the resolved values at startup: ```bash $ secretspec run -- ./myapp ``` Applications can also resolve them directly through the [SecretSpec SDKs](/sdk/overview/) for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C#, and Swift (0.18+), all sharing the same resolver so behavior stays consistent across languages. Providers own where secret values come from. SDKs give applications an idiomatic way to consume them. Configuration remains a shareable declaration of what is required. ## Making the boundary practical [Section titled “Making the boundary practical”](#making-the-boundary-practical) If you maintain an application, stop adding passwords and tokens to ordinary configuration schemas. Accept a file reference, credential, environment variable, or provider instead. For NixOS, [SecretSpec issue #65](https://github.com/cachix/secretspec/issues/65) tracks how an official integration could declare and resolve secrets without per-module substitution glue. Consistent secret handling across developer machines, CI, and production used to require infrastructure that only dedicated platform teams could build. A project of any size should be able to separate secrets from configuration without building its own secrets platform first. # SecretSpec 0.12: Audit logs and coding agents > Require a human-readable reason whenever a coding agent reaches for your secrets. A coding agent reaches for the same secrets you do, but on its own initiative and many times a session: a read looks identical whether it came from you running a deploy or an agent exploring the codebase. [SecretSpec 0.12](https://github.com/cachix/secretspec/releases/tag/v0.12.0 "SecretSpec 0.12 release") makes that access accountable. It ships three things: * **Audit log** — every secret read and write is appended to a local, per-user JSONL log. On by default. Values are never recorded. * **Reason-on-access** — secret access can require a human-readable reason, enforced for coding agents by default. * **`secretspec audit` command** — filter and summarize the log, or pipe raw JSON Lines to `jq`. Behavior change in 0.12 If you run SecretSpec inside a coding agent, secret access now **fails** until a reason is supplied. This is the new default (`require_reason = "agents"`). Opt out with `require_reason = false` in the `[project]` table. Existing providers and library callers keep working unchanged. See [Upgrading](#upgrading). ## The audit log [Section titled “The audit log”](#the-audit-log) Every secret read and write, from the CLI and the Rust SDK, is appended to a local log as [JSON Lines](https://jsonlines.org/), one event per line. Secret **values are never written**, only metadata: the secret name, the profile, the provider that served it (with any embedded credentials redacted), the outcome, the reason, and who was asking, including the detected coding agent. ```json { "v": 1, "ts": "2026-06-04T17:04:00.893Z", "action": "get", "project": "my-app", "profile": "production", "key": "DATABASE_URL", "provider": "keyring://", "outcome": "found", "reason": "deploy web frontend", "actor": { "user": "alice", "agent": "claude-code", "is_agent": true }, "version": "0.12.0" } ``` The log lives in your per-user state directory (`~/.local/state/secretspec/audit.log`) and is created readable only by you. Read it with any tool, or use the new `secretspec audit` command for filtering and a readable summary: ```bash # Last 20 entries, formatted $ secretspec audit -n 20 # Only `run` events for one project $ secretspec audit --project my-app --action run # Raw JSON Lines, piped to jq $ secretspec audit --json | jq 'select(.outcome == "missing")' ``` It is configured in your **user-global config** (`~/.config/secretspec/config.toml`), not the project’s `secretspec.toml`, so a repository you clone can’t quietly turn off or redirect your audit log. The log is a single file capped at 1 MiB, a size-bounded recent record rather than permanent compliance history; forward it to a central system if you need that. To turn it off entirely: \~/.config/secretspec/config.toml ```toml [audit] enabled = false ``` See [Audit Logging](/concepts/audit/) for the full record schema and options. ## Supplying a reason [Section titled “Supplying a reason”](#supplying-a-reason) When a coding agent like Claude Code reaches for a secret without a reason, the access is refused and the agent is told exactly what to do next: ```console $ secretspec run -- npm test Error: Accessing secrets requires a reason. Provide one with --reason "", the SECRETSPEC_REASON environment variable, or Secrets::with_reason() in the SDK. (Policy: require_reason in [project] of secretspec.toml — defaults to "agents"; set it to false to disable.) ``` Claude Code reads that message, states why it needs the secret, and retries: ```bash $ secretspec run --reason "run the test suite before opening a PR" -- npm test ``` Both the refusal and the successful retry land in the audit log, so the reason is tied to the access. There are three ways to supply a reason: | Source | Scope | Precedence | | ------------------------ | ------------------ | ------------- | | `--reason` flag | CLI | highest | | `Secrets::with_reason()` | SDK | overrides env | | `SECRETSPEC_REASON` | CLI + SDK + derive | lowest | ```bash # CLI: the most explicit option, overrides the others $ secretspec run --reason "deploying release 0.12" -- ./deploy.sh ``` ```rust // SDK: the programmatic equivalent of --reason let secrets = Secrets::load(/* ... */)?.with_reason("nightly backup job"); ``` ```bash # Env: lowest precedence, but honored everywhere $ export SECRETSPEC_REASON="nightly backup job" ``` `SECRETSPEC_REASON` is resolved by `Secrets::load` / `load_from`, which means `secretspec-derive`-generated code and other library callers satisfy the policy and supply an audit reason **without any code changes**. Whichever path you use, blank or whitespace-only reasons are ignored, so they can’t quietly satisfy the policy. Under the hood this is backed by a new `Provider::set_reason` trait method (a no-op by default), so existing providers keep working unchanged. ## Configuring when a reason is required [Section titled “Configuring when a reason is required”](#configuring-when-a-reason-is-required) The new `require_reason` policy in the `[project]` table controls when a reason is mandatory: ```toml [project] name = "my-app" require_reason = "agents" # require it from agents (default), or true / false ``` * `"agents"` (the default): require a reason only when a coding agent is detected. * `true`: require it from every caller. * `false`: never require it. Because the policy lives in `secretspec.toml` and is enforced by SecretSpec, it applies to everyone and every CI runner, and is inherited through `extends`. Coding agents are spotted by the [`detect-coding-agent`](https://crates.io/crates/detect-coding-agent) crate (Claude Code, Cursor, Codex, Gemini CLI, Copilot, and more); set `SECRETSPEC_AGENT` for a harness it doesn’t recognize. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` Remember the new default: agents must pass a reason: set `require_reason = false` to opt out. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell > Native Python, Node.js, Go, Ruby, and Haskell bindings over the same Rust resolver as the CLI. SecretSpec separates *what* secrets an application needs, declared in `secretspec.toml`, from *where* the values live, a provider like your system keyring, 1Password, or Vault. Until now, reading those resolved secrets at runtime meant the CLI or the Rust SDK. If your service was written in Python or Go, you shelled out to `secretspec run` or reimplemented resolution yourself. [SecretSpec 0.13](https://github.com/cachix/secretspec/releases/tag/v0.13.0 "SecretSpec 0.13 release") closes that gap. It ships native SDKs for five languages: Python, Node.js / TypeScript, Go, Ruby, and Haskell. Each resolves the exact secrets your manifest declares, through the same providers, profiles, fallback chains, and generators as the CLI, with no per-language configuration. > This article uses the original `secretspec-ffi` name. The embedded ABI is named `libsecretspec` in SecretSpec 0.20+. ## Native bindings over one resolver [Section titled “Native bindings over one resolver”](#native-bindings-over-one-resolver) Every SDK is a thin client over the same Rust core that powers the CLI. No provider logic, profile resolution, chain fallback, `as_path` materialization, or secret generation lives in the binding. A provider added to SecretSpec works in every language the day it lands, and every SDK behaves identically. The binding strategy is chosen per ecosystem: * **Python**: a pyo3 extension, statically linked, shipped as a self-contained `cp39-abi3` wheel. * **Node.js**: a napi-rs addon with prebuilt per-platform packages. * **Ruby**: a native C extension (mkmf) with the resolver statically linked into a platform gem. * **Go**: the `secretspec-ffi` C ABI loaded at runtime via [purego](https://github.com/ebitengine/purego) (no cgo). * **Haskell**: the same C ABI, linked at build time through the Haskell FFI. ## The same three steps, in your language [Section titled “The same three steps, in your language”](#the-same-three-steps-in-your-language) Each SDK mirrors the vocabulary of the Rust derive crate: a builder that takes a provider, a profile, and an access reason, then `load()` to resolve, then a map of secrets you can read or export into the environment. ```python # Python from secretspec import SecretSpec resolved = ( SecretSpec.builder() .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .load() ) print(resolved.secrets["DATABASE_URL"].get) # value, or file path for as_path resolved.set_as_env() # export into os.environ ``` ```js // Node.js / TypeScript const { SecretSpec } = require('secretspec'); const resolved = SecretSpec.builder() .withProvider('keyring://') .withProfile('production') .withReason('boot web app') .load(); console.log(resolved.secrets.DATABASE_URL.get()); // value, or as_path file path resolved.setAsEnv(); // export into process.env ``` ```go // Go resolved, err := secretspec.New(). WithProvider("keyring://"). WithProfile("production"). WithReason("boot web app"). Load() fmt.Println(resolved.Secrets["DATABASE_URL"].Get()) // value, or as_path file path resolved.SetAsEnv() // export into the environment ``` ```ruby # Ruby resolved = Secretspec::SecretSpec.builder .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .load puts resolved.secrets["DATABASE_URL"].get # value, or as_path file path resolved.set_as_env! # export into ENV ``` ```haskell -- Haskell resolved <- S.load ( S.builder & S.withProvider "keyring://" & S.withProfile "production" & S.withReason "boot web app" ) S.setAsEnv resolved -- export into the environment ``` Across all of them, `load()` resolves every declared secret, a missing required secret raises a typed `MissingRequiredError`, and `as_path` secrets come back as a readable file path with a cleanup that removes the backing temp file. The access reason feeds the same audit log and `require_reason` policy from [0.12](/blog/secretspec-0-12-audit-logs-and-coding-agents/), so a Go service is as accountable as the CLI. ## Write your own binding [Section titled “Write your own binding”](#write-your-own-binding) Under all five SDKs sits a new crate, `secretspec-ffi`: a small, versioned C ABI for resolving secrets. If we do not ship your language yet, you can bind to it directly. It also exposes the public Rust building blocks the SDKs share, `Secrets::resolve()` and `Secrets::report()`, so a Rust program reaches the same value-carrying and value-free entry points. ## Typed secrets, one schema for every language [Section titled “Typed secrets, one schema for every language”](#typed-secrets-one-schema-for-every-language) `secretspec.toml` already knows the shape of your secrets, so 0.13 can hand that shape to your type system. `secretspec schema` emits a JSON Schema for your manifest, the union of all profiles or one profile with `--profile`. Pipe it through [quicktype](https://quicktype.io) to generate idiomatic typed classes in any language, then populate them from each SDK’s `fields()` map: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang python -o secrets_gen.py ``` ```python typed = Secrets.from_dict(resolved.fields()) print(typed.database_url) # typed str ``` One schema drives every language’s type system, with no hand-written emitter per language. ## Install [Section titled “Install”](#install) ```bash $ pip install secretspec # Python $ npm install secretspec # Node.js / TypeScript $ gem install secretspec # Ruby $ go get github.com/cachix/secretspec/secretspec-go # Go ``` For Haskell, add `secretspec` from Hackage to your `build-depends`. The CLI and Rust SDK upgrade as usual: ```bash $ cargo install secretspec ``` See the [SDK overview](/sdk/overview/) for the per-language guides. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.14: Secret references > A secret can now point at one that already exists in a provider's store, by the store's own coordinates, instead of a name SecretSpec picks. SecretSpec keeps a `secretspec.toml` that declares what secrets an application needs, and resolves the values from a provider: your system keyring, 1Password, Vault, a `.env` file, and so on. Until now it stored every secret under a naming convention it controlled, `secretspec/{project}/{profile}/{key}`, and that convention was the only place it looked. That works when SecretSpec created the secret. It does not when the secret already exists under a name something else chose: a `db` item in a 1Password vault, a `myapp/config` path at a Vault mount, an environment variable your platform already sets. To manage such a secret you had to copy its value into SecretSpec’s convention, leaving two copies to rotate, or leave it out of SecretSpec entirely. [SecretSpec 0.14](https://github.com/cachix/secretspec/releases/tag/v0.14.0 "SecretSpec 0.14 release") introduces `ref`. A secret can name one that already exists, by the store’s own coordinates, and SecretSpec reads and writes that secret in place: ```toml [profiles.production] DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["prod_op"] } ``` `DATABASE_URL` now resolves from the `password` field of the 1Password item `db`. SecretSpec does not prepend a project or profile, and does not create a name of its own. ## Why not just paste the address [Section titled “Why not just paste the address”](#why-not-just-paste-the-address) 1Password will give you an address for that field: `op://Production/db/password`. The obvious design is to accept that string in the config and be done. We built that first and removed it. A string like `op://Production/db/password` names the store and the secret at the same time, which ties the secret to 1Password. The same reference cannot then resolve from Vault in CI and 1Password on a laptop, cannot be redirected at a `.env` fixture for a test run without editing the manifest, and does not compose with a provider fallback chain, because the chain and the address disagree about where the secret is. SecretSpec already decides which store to use, through providers, profiles, fallback chains, and the `--provider` override. A `ref` names only the secret and leaves the store to that existing machinery. ## Coordinates [Section titled “Coordinates”](#coordinates) A `ref` is a table, not a URL. Each key names a level of structure that some stores have: ```plaintext vault which container holds the item (1Password only) └── item the store's own name for the secret (always required) └── section a named group of fields (1Password only) └── field one component inside the item (structured stores) └── version which revision to read (GCSM only) ``` Only `item` is required, because every store names its secrets somehow. `item` is the complete name: it replaces the whole convention path, with no project or profile and no folder prefix prepended. `ref = { item = "GITHUB_PAT" }` on the env provider reads the environment variable `GITHUB_PAT` and nothing else. The other keys refine `item` for stores that have that structure. A `.env` key holds a single value, so `field` on a dotenv ref is not meaningful. A Vault KV entry is a map, so `field` is required. When a store has no equivalent for a coordinate it reports an error naming that coordinate, rather than reading a different secret: ```toml GITHUB_TOKEN = { description = "GitHub token", ref = { item = "GITHUB_PAT", field = "x" }, providers = ["env"] } ``` ```text Error: Provider operation failed: the env provider does not support the `field` coordinate. Drop `field` from the ref for `GITHUB_PAT`. ``` All eleven providers resolve refs, and each rejects the coordinates it cannot represent. A store whose secrets have no internal parts gets that rejection from shared code, without any per-provider work. ## References name, providers route [Section titled “References name, providers route”](#references-name-providers-route) A `ref` supplies the name only. Which provider resolves it follows the normal [provider resolution order](/concepts/providers/fallback/): a `--provider` override, then the secret’s `providers` chain, then profile and global defaults. That is the same order every other secret uses. Because the store is not part of the reference, the same `ref` works across providers. Each provider in a fallback chain is asked for the same coordinates, and one that cannot interpret them logs a warning and the chain continues: ```toml [profiles.production] DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["onepassword://Production", "keyring"] } ``` Chain entries can also be inline `scheme://` URIs, as above, with no `[providers]` alias declared first. The `--provider` override redirects a referenced secret the same way it redirects a conventional one, so pointing a whole suite at a `.env` fixture needs no change to the manifest: ```bash $ secretspec run --provider dotenv:.env.fixtures -- cargo test ``` ## Writing through a ref [Section titled “Writing through a ref”](#writing-through-a-ref) Reads and writes use the same coordinates. `secretspec set` and interactive `check` write to the referenced secret in place wherever the store supports writes: ```bash $ secretspec set DATABASE_URL # writes the `password` field of the 1Password item `db`, in place ``` 1Password edits the field with `op item edit` and does not create items. Keyring, pass, dotenv, Bitwarden, Proton Pass, and LastPass write their refs as well. Vault, AWS Secrets Manager, and Google Secret Manager are read-only for refs and report that directly, rather than claiming the provider cannot write at all. A `ref` also composes with `generate`. If the referenced secret does not exist yet, SecretSpec generates the value and writes it to the coordinates, so the first `check` populates the item everything else already reads. ## Faster resolution [Section titled “Faster resolution”](#faster-resolution) `check`, `run`, and the SDKs now group secrets by store and fetch the groups concurrently instead of one store after another. Within a group, referenced secrets use the store’s bulk API where it has one (AWS `BatchGetSecretValue`, and the single Bitwarden, Proton Pass, and 1Password listings) and otherwise resolve concurrently, fetching each unique coordinate once. CLI authentication for 1Password, LastPass, and Proton Pass is probed once per account or session instead of once per provider instance. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` Three changes to be aware of: * A `onepassword://` URI carrying an item path used to drop the path and target a vault literally named `vault`. Item paths, including pasted `op://vault/item/field` strings, now fail with an error that gives the `ref` table to write instead. Provider URIs are store addresses only. * `ref` is always a table. String and URI forms are rejected, with the same translation in the error. * Manifest validation now runs on every load. Rules that `secretspec.toml` documents (a required secret cannot have a `default`, `generate` needs a `type`, ref coordinates must be non-empty) are enforced on load rather than ignored. A manifest that violated one of them will now fail with a clear error. See [Secret References](/concepts/references/) for the full model and the [configuration reference](/reference/configuration/#secret-references) for how each provider maps the coordinates. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.15: Provider credentials, Azure Key Vault / Gopass, and PHP SDK > Authenticate providers from another secret store, use Azure Key Vault or Gopass, export secrets for CI, and resolve them from PHP. [SecretSpec 0.15](https://github.com/cachix/secretspec/releases/tag/v0.15.0 "SecretSpec 0.15 release") ships: * **[Provider credentials](/concepts/providers/#provider-credentials)** — authenticate one secret provider with credentials stored in another, without exporting them to the application environment. * **[Azure Key Vault](/providers/akv/)** — store and resolve secrets with service-principal, Azure CLI, managed-identity, or AKS workload-identity authentication. * **[Gopass](/providers/gopass/)** — use a GPG-encrypted, git-synchronized password store, including multi-user and multi-store setups. * **[PHP SDK](/sdk/php/)** — use the shared SecretSpec resolver from PHP-FPM, Laravel, Symfony, and CLI applications through a native extension or `ext-ffi`. * **[AWS creation guardrails](/providers/awssm/)** — set a customer-managed KMS key and required tags when SecretSpec creates an AWS Secrets Manager secret. * **[`secretspec export`](/reference/cli/#export)** — resolve secrets without launching a command, with shell, dotenv, JSON, and GitHub Actions output. * **[Provider and resolution fixes](/concepts/providers/)** — ordered lazy fallback chains, early `ref` validation, correctly merged profile overrides, stable output, and broader Node.js Linux compatibility. ## Credentials for the secret store [Section titled “Credentials for the secret store”](#credentials-for-the-secret-store) Suppose [Bitwarden Secrets Manager](/providers/bws/) holds an application’s secrets, but its machine access token is kept in the user’s [OS keyring](/providers/keyring/). Declare the relationship on the provider alias: secretspec.toml ```toml [providers] keyring = "keyring://" [providers.bws] uri = "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c" [providers.bws.credentials] access_token = "keyring" ``` Before SecretSpec connects to Bitwarden, it reads `access_token` from the keyring at the normal `{project}/{profile}/access_token` address. The active [profile](/concepts/profiles/) is part of that address, so production and development can authenticate as different machines without changing the alias. When a credential already has a provider-native address, use a `ref`. Here a [Vault AppRole](/providers/vault/#approle-authentication) is kept as two fields of one [1Password item](/providers/onepassword/#use-existing-secrets): secretspec.toml ```toml [providers.vault_prod] uri = "vault://secret/myapp?auth=approle" [providers.vault_prod.credentials] role_id.provider = "onepassword" role_id.ref.vault = "Infra" role_id.ref.item = "vault-approle" role_id.ref.field = "role_id" secret_id.provider = "onepassword" secret_id.ref.vault = "Infra" secret_id.ref.item = "vault-approle" secret_id.ref.field = "secret_id" ``` The credential source uses the same [`ref` coordinates](/concepts/references/) as application secrets. The difference is where the value goes: SecretSpec hands it directly to the destination provider in memory. It is not added to the environment of a process started by [`secretspec run`](/reference/cli/#run). Provider credential names are semantic and checked before a source is opened. Bitwarden accepts `access_token`; Vault accepts `token`, `role_id`, and `secret_id`; 1Password accepts `service_account_token`; Azure Key Vault accepts `tenant_id`, `client_id`, and `client_secret`. A configured credential is authoritative, while a provider’s usual environment fallback remains available when no credential source is declared. Credential chains deliberately stop after one hop. The store containing a provider credential cannot itself depend on another provider credential. This keeps the bootstrap path finite and makes dependency mistakes fail before any store is contacted. ## Log in once, without an environment variable [Section titled “Log in once, without an environment variable”](#log-in-once-without-an-environment-variable) The new [`config provider login`](/reference/cli/#config-provider-login) command prompts for every credential an alias declares and writes it to the configured source: ```console $ secretspec config provider login bws Enter access_token for provider 'bws' (source: keyring): **** ✓ stored access_token in keyring at my-app/default/access_token ``` A user-level alias and its credential source can also be declared entirely with [`config provider add`](/reference/cli/#config-global-provider-add): ```bash $ secretspec config provider add bws "bws://project-uuid" \ --credential access_token=keyring $ secretspec config provider login bws ``` Credentials are fetched once per invocation and profile, then reused for every secret routed through that alias. Each credential read, and each value stored by `login`, gets an [audit event](/concepts/audit/) marked with the semantic credential name and source store. As with every SecretSpec audit event, the credential value is never recorded. See [Provider Credentials](/concepts/providers/#provider-credentials) for the full configuration and resolution rules. ## Azure Key Vault [Section titled “Azure Key Vault”](#azure-key-vault) Azure Key Vault joins the provider list with the `akv://` scheme: ```bash # Use service-principal credentials, or the current Azure CLI session $ secretspec run --provider akv://myvault -- npm start # Use the platform's managed identity $ secretspec check --provider akv://myvault?auth=managed_identity # Use AKS workload identity federation $ secretspec run --provider akv://myvault?auth=workload_identity -- ./deploy ``` The default authentication mode first looks for the `tenant_id`, `client_id`, and `client_secret` provider credentials introduced above, then their `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` environment fallbacks. If none are present, it uses the signed-in Azure CLI or Azure Developer CLI session. A partial service principal is an error, rather than a reason to silently switch identities. That makes a service principal straightforward to keep in the system keyring: secretspec.toml ```toml [providers.azure] uri = "akv://myvault" [providers.azure.credentials] tenant_id = "keyring" client_id = "keyring" client_secret = "keyring" ``` ```bash $ secretspec config provider login azure $ secretspec run --provider azure -- ./deploy ``` Sovereign clouds can use either a complete vault hostname or an explicit DNS suffix such as `akv://myvault?suffix=vault.azure.cn`. Azure restricts secret names to letters, digits, and hyphens and compares them case-insensitively. SecretSpec encodes the project, profile, and key as lowercase, unpadded Base32 components. The encoding keeps names that differ by case or punctuation distinct instead of letting Azure collapse them onto the same secret. Existing Azure secrets can be addressed with a read-only `ref`. See the [Azure Key Vault provider guide](/providers/akv/) for authentication, naming, references, and required permissions. ## Gopass joins the local providers [Section titled “Gopass joins the local providers”](#gopass-joins-the-local-providers) The new [`gopass://` provider](/providers/gopass/) reads and writes through the `gopass` CLI. Gopass builds on the Unix [`pass` provider](/providers/pass/) with multi-user and multi-store support while keeping entries GPG-encrypted and synchronized through git. Once Gopass is installed and its password store is initialized, select it like any other provider: ```bash $ secretspec set DATABASE_URL --provider gopass $ secretspec run --provider gopass -- npm start ``` By default, entries live under `secretspec/{project}/{profile}/{key}`. A custom URI can change that layout, including omitting `{project}` to share secrets between repositories: \~/.config/secretspec/config.toml ```toml [defaults.providers] shared = "gopass://secretspec/shared/{profile}/{key}" ``` An existing Gopass entry can also be addressed directly with a [`ref`](/concepts/references/), including the mount-point prefix used by a multi-store setup. See the [Gopass provider guide](/providers/gopass/) for installation, shared-store configuration, references, and current limitations. ## PHP joins the SDKs [Section titled “PHP joins the SDKs”](#php-joins-the-sdks) The new `cachix/secretspec` Composer package brings the shared resolver to PHP: ```bash $ composer require cachix/secretspec ``` ```php withProfile('production') ->withReason('boot web app') ->load(); echo $resolved->secrets['DATABASE_URL']->get(); $resolved->setAsEnv(); ``` It offers two native backends behind the same PHP API. The recommended native extension embeds the resolver and works under PHP-FPM without `ffi.enable`, like `ext-redis`. An `ext-ffi` fallback loads the shared resolver at runtime for CLI tools and local development. Both use the same Rust core as the CLI and the other language SDKs, so profiles, providers, fallback chains, generators, [`as_path`](/reference/configuration/#as_path-option), [audit reasons](/reference/configuration/#requiring-a-reason-for-secret-access), and typed missing-secret errors behave the same way. `setAsEnv()` updates `getenv()`, `$_ENV`, and `$_SERVER`, which lets Laravel’s `env()` helper and Symfony’s `%env(...)%` processors consume resolved secrets during application boot. See the [PHP SDK guide](/sdk/php/) for installation and framework examples. ## AWS creation guardrails [Section titled “AWS creation guardrails”](#aws-creation-guardrails) AWS accounts often require a customer-managed KMS key or specific tags in the same `CreateSecret` request. The [AWS Secrets Manager provider](/providers/awssm/) now accepts both on its URI: secretspec.toml ```toml [providers] prod = "awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform&tag.env=prod" ``` `kms_key_id` and repeatable `tag.NAME=VALUE` parameters are applied only when SecretSpec creates a secret. Updating an existing secret does not alter the key or tags it was created with. This supports tag-on-create SCP and IAM guardrails without turning routine secret updates into infrastructure changes. ## Export secrets for shells, tools, and CI [Section titled “Export secrets for shells, tools, and CI”](#export-secrets-for-shells-tools-and-ci) The new `export` command resolves every secret for the active profile without starting another process. Its default output can be evaluated by a POSIX shell: ```bash $ eval "$(secretspec export --profile production)" ``` Use `--format dotenv` to write dotenv syntax or `--format json` to pass the resolved values to another tool: ```console $ secretspec export --profile production --format json { "DATABASE_URL": "postgresql://prod.example.com/mydb" } ``` GitHub and Forgejo Actions can use `--format gha`. SecretSpec masks every value in the runner log and appends it to `$GITHUB_ENV`, making the secrets available to later steps and third-party actions: ```yaml - name: Export secrets run: secretspec export --profile production --format gha - name: Deploy run: ./deploy ``` Like non-interactive [`check`](/reference/cli/#check), `export` never prompts and exits non-zero when a required secret is missing, so it can gate a CI job. Export attempts are also recorded in the [audit log](/concepts/audit/). See the [`export` CLI reference](/reference/cli/#export) for every format and option. ## Provider and resolution fixes [Section titled “Provider and resolution fixes”](#provider-and-resolution-fixes) 0.15 also tightens the behavior around profiles and fallback chains: * [Provider chains](/concepts/providers/fallback/) are now walked strictly in order and resolved lazily. An undefined alias or unreachable fallback is skipped with a warning only when a read reaches it, so a later working provider can still answer. * Chain entries accept aliases, bare provider names such as [`keyring`](/providers/keyring/), shorthand such as [`dotenv:.env`](/providers/dotenv/), and complete provider URIs. * A single destination provider rejects unsupported [`ref` coordinates](/concepts/references/) before contacting the store. Multi-provider chains still validate each destination as they reach it, because an earlier store may support coordinates a later one does not. * [Profile overrides](/concepts/profiles/) inherit the base secret’s `description` and generation `type`. Validation now uses the effective merged secret while still catching real conflicts, such as combining [`generate`](/concepts/generation/) with a profile default. * `run` passes non-UTF-8 environment variables through to the child untouched, and command output that previously depended on map order is now stable. * Prebuilt [Node.js addons](/sdk/nodejs/) now target glibc 2.28 and statically include libdbus, restoring support for Amazon Linux 2023, RHEL 8/9, and similar distributions. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` Existing providers retain their conventional environment authentication when an alias does not declare credentials. Provider credentials are opt-in, and credential dependency chains are limited to one hop. See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change and fix in this release. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.16: Composed secrets, Infisical, and C# SDK > Derive secrets from other declared values, use Infisical Cloud or self-hosted, and resolve secrets natively from .NET. [SecretSpec 0.16](https://github.com/cachix/secretspec/releases/tag/v0.16.0 "SecretSpec 0.16 release") ships: * **[Composed secrets](/concepts/composed-secrets/)** — derive a read-only value, such as a connection string, from other secrets declared in the manifest. * **[Infisical](/providers/infisical/)** — read and write secrets in Infisical Cloud or a self-hosted instance, with Universal Auth, access-token, and provider-credential authentication. * **[C# SDK](/sdk/csharp/)** — resolve the same manifests from .NET through the shared native resolver, distributed as the `Cachix.SecretSpec` NuGet package. ## Composed secrets [Section titled “Composed secrets”](#composed-secrets) Applications often need a connection string while secret stores work better with its independently rotated parts. SecretSpec can now keep those parts separate and assemble the application-facing value when it resolves the manifest: secretspec.toml ```toml [profiles.default] DB_USER = { description = "Database user" } DB_PASSWORD = { description = "Database password" } DB_HOST = { description = "Database host" } DATABASE_URL = { description = "PostgreSQL connection string", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" } ``` `DB_USER`, `DB_PASSWORD`, and `DB_HOST` still come from their configured providers. `DATABASE_URL` is assembled in memory and behaves like any other resolved secret in the CLI and SDKs. Compositions are read-only, may build on other compositions, and are checked for missing references and cycles before resolution. See [Composed Secrets](/concepts/composed-secrets/) for optional values, escaping, profile inheritance, and validation rules. ## Infisical [Section titled “Infisical”](#infisical) The new `infisical://` provider works with Infisical Cloud, its EU service, and self-hosted instances. Point SecretSpec at an Infisical project and authenticate with Universal Auth: ```bash $ export INFISICAL_CLIENT_ID=... $ export INFISICAL_CLIENT_SECRET=... $ secretspec run \ --provider "infisical://app.infisical.com/7e2f1a4c-...?env=prod" \ -- npm start ``` Access tokens are also supported. Credentials can come from environment variables or SecretSpec’s [provider credentials](/concepts/providers/#provider-credentials), allowing, for example, an Infisical machine identity to be kept in the system keyring: secretspec.toml ```toml [providers.infisical] uri = "infisical://app.infisical.com/7e2f1a4c-..." [providers.infisical.credentials] client_id = "keyring" client_secret = "keyring" ``` By default, the active SecretSpec profile also names the Infisical environment. A `production` profile therefore reads from the `production` environment, while `?env=` can select a different one. The provider supports normal SecretSpec reads and writes, as well as references to existing Infisical secrets and versions. See the [Infisical provider guide](/providers/infisical/) for self-hosting, authentication, paths, references, and permissions. ## C# SDK [Section titled “C# SDK”](#c-sdk) The `Cachix.SecretSpec` NuGet package brings the shared SecretSpec resolver to .NET 8: ```bash $ dotnet add package Cachix.SecretSpec ``` ```csharp using Cachix.SecretSpec; using var resolved = SecretSpec.Builder() .WithProvider("keyring://") .WithProfile("production") .WithReason("boot web app") .Load(); Console.WriteLine(resolved.Secrets["DATABASE_URL"].Get()); resolved.SetAsEnv(); ``` It uses the same resolver as the CLI and other language SDKs, so profiles, providers, fallback chains, references, generators, audit reasons, and composed secrets work consistently in .NET. Native resolver builds are included in the NuGet package, with no separate SecretSpec CLI installation required. See the [C# SDK guide](/sdk/csharp/) for supported platforms, ASP.NET Core integration, preflight reports, error handling, and typed access. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` All three additions are opt-in: existing manifests and provider configurations continue to work unchanged. Add `composed` when a value should be derived, select an `infisical://` provider to use Infisical, or install `Cachix.SecretSpec` in a .NET application. See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change in this release. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.17: Scopes, secrets caching, SOPS, age, and systemd credentials > Resolve only the secrets a service needs, cache remote secrets safely, and use age-encrypted files or native systemd credentials alongside new providers. [SecretSpec 0.17](https://github.com/cachix/secretspec/releases/tag/v0.17.0 "SecretSpec 0.17 release") ships: * **[Scopes](/concepts/scopes/)**: let each service or task resolve only its declared subset of a profile. * **[Secrets caching](/concepts/providers/caching/)**: cache a slow fallback route in a local secret store with bounded freshness and explicit invalidation. * **[Cross-secret validation](#cross-secret-validation)**: declare alternative or mutually exclusive credentials instead of marking every value independently required or optional. * **[GitHub and Forgejo Actions](#github-and-forgejo-actions)**: resolve a profile in one workflow step and expose its secrets to the steps that follow. * **[New providers](#new-providers)**: now with 20 providers, use [SOPS](/providers/sops/), [age](/providers/age/), [KeePass KDBX](/providers/kdbx/), [OpenBao](/providers/openbao/), [Scaleway Secret Manager](/providers/scaleway/), and [systemd credentials](/providers/systemd-credential/) through the same SecretSpec interface, with JWT/OIDC authentication for [Vault](/providers/vault/) and [OpenBao](/providers/openbao/). ## Scopes [Section titled “Scopes”](#scopes) A profile describes how secrets resolve for an environment. A [scope](/concepts/scopes/) now describes which of those secrets one consumer may receive: secretspec.toml ```toml [profiles.default] DATABASE_URL = { description = "Database" } API_KEY = { description = "API key" } QUEUE_TOKEN = { description = "Queue token" } [scopes.api] secrets = ["DATABASE_URL", "API_KEY"] [scopes.worker] secrets = ["DATABASE_URL", "QUEUE_TOKEN"] ``` ```bash $ secretspec run --scope api -- ./api $ secretspec run --scope worker -- ./worker ``` Composed secrets may still read hidden dependencies to build a visible value, but those inputs are not exposed to the child. The same scope selection is available to `check`, `export`, and the [SDK builders](/sdk/overview/). Scopes minimize secret delivery; they are not an authorization boundary when the child itself holds provider credentials. Carrying the selected scope through resolver requests and results required a breaking change to [`secretspec-ffi`](https://github.com/cachix/secretspec/tree/main/libsecretspec), which is named `libsecretspec` in SecretSpec 0.20+. All [SecretSpec SDKs](/sdk/overview/) have been updated for 0.17 to support scopes, so applications should upgrade their SDK package and bundled native resolver together. ## Secrets caching [Section titled “Secrets caching”](#secrets-caching) Many cloud providers take long enough to resolve a secret that their latency becomes part of every development command. A single **1Password** lookup can take **roughly one second**. SecretSpec providers implement `get_many` so a backend can resolve several values together. Relatively few secret stores and CLIs expose a true bulk-read operation, however, so many providers still have to perform separate lookups. Waiting on a remote service or its CLI every time makes `check`, `run`, and application startup feel slow, especially as a project grows. A provider alias can now combine its authoritative fallback route with a local cache: secretspec.toml ```toml [providers] vault = "vault://vault.example.com:8200/secret" local = "keyring://secretspec/cache/{project}/{profile}/{key}" fast_vault = { fallback = ["vault"], cache = { provider = "local", max_age = "8h" } } [profiles.default.defaults] providers = ["fast_vault"] ``` Fresh entries avoid contacting the remote provider. A miss or expired entry falls through to [Vault](/providers/vault/) and refreshes the cache; writes update the authoritative provider first and then refresh or invalidate its cached copy. Route changes, reference changes, and writes that bypass the cached alias also invalidate the entry. The cache is a real copy of the secret, so SecretSpec requires a distinct store that it can delete from and records ownership before changing an entry. `secretspec cache clear [NAME]` forces the next read back through the authoritative route. [Vault](/providers/vault/) and [OpenBao](/providers/openbao/) KV v2 caches are the only providers that handle `max_age` as server-side expiry properly. None of SecretSpec’s current local providers has strong native support for expiry. They can remove an expired entry the next time SecretSpec sees it, but cannot ensure the local copy disappears at its deadline if SecretSpec never runs again. Our planned [FactorSeal](https://github.com/domenkozar/factorseal) provider in [Future work](#future-work) is intended to close that gap with an explicit API for credential eviction among the other goals. ## Cross-secret validation [Section titled “Cross-secret validation”](#cross-secret-validation) Profiles can now express credential alternatives directly: secretspec.toml ```toml [profiles.default] PASSWORD = { description = "Password", required = { at_least_one = "auth" } } ACCESS_TOKEN = { description = "Token", required = { at_least_one = "auth" } } GITHUB_TOKEN = { description = "GitHub token", required = { exactly_one = "github_auth" } } GITHUB_APP_KEY = { description = "GitHub App private key", required = { exactly_one = "github_auth" } } ``` The `auth` group accepts a password, an access token, or both. The `github_auth` group requires exactly one credential and rejects configurations that provide both the token and the app key. ## New providers [Section titled “New providers”](#new-providers) **[SOPS](/providers/sops/)** brings the encrypted-file workflow from our recent [SOPS comparison](/blog/but-i-use-sops/) behind SecretSpec’s provider-independent CLI and SDKs. SecretSpec delegates encryption and decryption to the installed SOPS CLI, so existing SOPS key services and `.sops.yaml` creation rules remain in control. The provider reads and writes YAML, JSON, dotenv, and INI files, supports a single shared file or `{project}` / `{profile}` path templates, and can source sensitive SOPS inputs such as age keys or cloud credentials through provider credentials. secretspec.toml ```toml [providers] sops = "sops://secrets/{project}/{profile}.enc.yaml" [profiles.production.defaults] providers = ["sops"] ``` **[age](/providers/age/)** offers a smaller encrypted-file setup. It stores a dotenv-style secret set for one or more age recipients, including hybrid post-quantum recipients. **[KeePass KDBX](/providers/kdbx/)** reads KDBX 3 and 4 databases and writes KDBX 4, with master passwords sourced from another provider rather than embedded in the URI. **[OpenBao](/providers/openbao/)** gets its own `openbao://` identity and `BAO_*` configuration while sharing compatible KV, token, AppRole, and JWT mechanics with [Vault](/providers/vault/). Both Vault and OpenBao can now exchange a JWT for a short-lived token, including an OIDC token minted automatically in GitHub Actions and Forgejo Actions with `id-token: write`. **[Scaleway Secret Manager](/providers/scaleway/)** adds regional, project-aware cloud storage and read-only references to existing secrets and revisions. **[systemd credentials](/providers/systemd-credential/)** is a read-only provider that resolves values from the current service’s `$CREDENTIALS_DIRECTORY`, including credentials used to bootstrap another provider. ## GitHub and Forgejo Actions [Section titled “GitHub and Forgejo Actions”](#github-and-forgejo-actions) Alongside 0.17, the new [`cachix/secretspec-action`](https://github.com/cachix/secretspec-action) installs SecretSpec, resolves the selected profile, masks every value in the runner log, and adds the secrets to the environment of later job steps: ```yaml jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: cachix/secretspec-action@main with: profile: production scope: api - run: ./deploy.sh ``` A missing required secret fails the action, so the same step also checks that the deployment environment is complete. See the [GitHub Actions guide](/ci/github-actions/) for provider selection and tokenless [Vault](/providers/vault/) or [OpenBao](/providers/openbao/) authentication through the runner’s OIDC identity. ## Upgrading [Section titled “Upgrading”](#upgrading) [SecretSpec 0.17](https://github.com/cachix/secretspec/releases/tag/v0.17.0) also brings: * **[Bitwarden Secrets Manager](/providers/bws/)** — now uses the separately installed official `bws` CLI instead of linking its SDK. * **Non-interactive setup** — `secretspec config global init --provider ... --profile ...` configures defaults without prompts. * **Windows packages** — for the Python, Ruby, and PHP SDKs. * **Clearer status output** — plus more controlled concurrency and retry behavior for Vault and OpenBao. ```bash $ cargo install secretspec ``` See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change in this release. ## Future work [Section titled “Future work”](#future-work) The next work brings more control to local secret access: * **[GUI confirmation dialogs](https://github.com/cachix/secretspec/pull/122)** — approve or deny a secret request in a native prompt instead of requiring a terminal interaction. * **A [Passbolt provider](https://github.com/cachix/secretspec/pull/127) (0.19+)** — bring Passbolt’s open-source, collaboration-focused credential manager behind the same SecretSpec interface for cloud and self-hosted teams. * **A [Bitwarden Password Manager provider](https://github.com/cachix/secretspec/pull/166)** — resolve regular Bitwarden vault items, separately from the Bitwarden Secrets Manager provider already available in SecretSpec. * **A JVM SDK** — work is underway to bring the shared SecretSpec resolver to Java, Kotlin, and other JVM languages. * **A [FactorSeal](https://github.com/domenkozar/factorseal) provider** — we have started work on a new Linux provider built around mandatory TPM-backed storage and secure defaults. FactorSeal also provides an explicit API for credential expiry, which is crucial for the caching work in this release: local copies can carry a defined eviction deadline instead of living without a retention policy. Still in development. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.18: Secret lifecycle, Bitwarden, Keeper, AWS Parameter Store, and Swift > Declare, discover, migrate, and delete secrets from the CLI; use four new providers; and resolve the same manifests from Swift. [SecretSpec 0.18](https://github.com/cachix/secretspec/releases/tag/v0.18.0 "SecretSpec 0.18 release") ships: * **[Secret lifecycle commands](#secret-lifecycle-commands)**: add declarations, delete stored values, and move secrets between providers without leaving the source copy behind. * **[Provider-backed discovery](#provider-backed-discovery)**: initialize a manifest from an age file, an AWS Parameter Store hierarchy, or a Bitwarden collection without writing any values to it. * **[Four new providers](#four-new-providers)**: use [Bitwarden Password Manager](/providers/bw/), [Keeper Secrets Manager](/providers/keeper/), [AWS Systems Manager Parameter Store](/providers/awsps/), and [Dashlane](/providers/dashlane/) through the same CLI and SDK interface. * **[Swift SDK](#swift-sdk)**: resolve manifests natively on macOS through a checksummed XCFramework that includes the shared Rust resolver. * **[Vault and OpenBao authentication](#vault-and-openbao-authentication)**: use custom AppRole and JWT mounts, AppRoles without SecretID binding, and server-configured default JWT roles. ## We have a new logo! [Section titled “We have a new logo!”](#we-have-a-new-logo) ![The new SecretSpec document-and-keyhole logo](/_astro/logo.DOR2BfU0_29pGgh.webp) ## Secret lifecycle commands [Section titled “Secret lifecycle commands”](#secret-lifecycle-commands) SecretSpec has always kept the declaration in `secretspec.toml` separate from the stored value. 0.18 brings both sides of that lifecycle into the CLI. [`secretspec add`](/reference/cli/#add-018) adds a declaration to the selected profile while preserving the manifest’s comments, formatting, and unrelated tables: ```console $ secretspec add STRIPE_API_KEY --description "Stripe API access token" ✓ Added secret 'STRIPE_API_KEY' to profile 'default' in secretspec.toml Set its value with: secretspec set STRIPE_API_KEY --profile default $ secretspec set STRIPE_API_KEY Enter value for STRIPE_API_KEY: ******** ✓ Secret 'STRIPE_API_KEY' saved to keyring (profile: default) ``` `add` never asks for or stores the value. The declaration can be reviewed and committed before each developer or deployment supplies its own value. [`secretspec delete`](/reference/cli/#delete-018) does the inverse on the storage side: it removes a value without changing the declaration. The next `check` therefore reports the secret as missing instead of quietly removing the application’s requirement: ```console $ secretspec delete STRIPE_API_KEY Deleted 'STRIPE_API_KEY' Deleted 1 secret value; 0 already absent ``` Deletion is idempotent, invalidates an associated cache entry, and follows the same primary-write-provider routing as `set`. `delete --all` requires an interactive confirmation, or an explicit `--yes` in non-interactive use. Provider migrations can now remove each source value after proving the move succeeded: ```bash $ secretspec import dotenv:~/.config/payments/.env --delete-source ``` [`import --delete-source`](/reference/cli/#import) reads the destination back and compares it with the source before deleting anything. An identical value already at the destination is safe to remove from the source; a conflicting value leaves the source intact. SecretSpec also rejects a source without deletion support before writing the destination and recognizes equivalent provider spellings as the same store, so a migration cannot delete the value it just wrote through another alias. Together these commands keep the distinction explicit: `add` changes what the application declares, `set` and `delete` change one environment’s stored value, and `import --delete-source` moves that value between stores. ## Provider-backed discovery [Section titled “Provider-backed discovery”](#provider-backed-discovery) The first SecretSpec command in an existing project is often `secretspec init --from .env`. In 0.18, [`init --from`](/reference/cli/#init) accepts every provider that can discover declarations, including age, AWS Parameter Store, and Bitwarden Password Manager. Hierarchical stores also receive an explicit project and profile so SecretSpec looks only inside the namespace the new manifest will use: ```console $ secretspec init \ --from 'awsps://production@us-east-1?template=/{profile}/{project}/{key}' \ --project payments \ --profile production ✓ Created secretspec.toml with 12 secrets ``` For a password-manager vault, scope discovery to the collection and item type that belong to the application: ```console $ secretspec init --from 'bw://Acme%20Inc@dev-secrets?type=login' ✓ Created secretspec.toml with 8 secrets ``` Discovery writes names and generated descriptions, never secret values. After reviewing the manifest, keep the discovered provider as the profile’s source or use `secretspec import` to copy the now-declared values somewhere else. ## Four new providers [Section titled “Four new providers”](#four-new-providers) 0.18 brings SecretSpec to 24 providers, with four additions spanning personal password managers, machine-oriented vaults, and cloud parameter storage. **[Bitwarden Password Manager](/providers/bw/)** is separate from the existing Bitwarden Secrets Manager provider. The new `bw://` provider uses the official `bw` CLI to read and write regular vault items: logins, secure notes, cards, identities, and SSH keys. It can address organizations and collections by name or ID, restrict a provider to one item type or field, discover declarations, and point [`ref`](/concepts/references/) secrets at existing items. A `?server=` guard verifies that the CLI is logged into the expected self-hosted instance instead of silently reading the wrong vault. **[Keeper Secrets Manager](/providers/keeper/)** uses Keeper’s official Rust SDK, so it does not need a separate CLI. A `keeper://FOLDER_UID` provider reads, writes, batches, and deletes convention records shared with a KSM application; refs can select an existing record and field. Its client configuration can come from `KSM_CONFIG`, a protected configuration file, or SecretSpec [provider credentials](/concepts/providers/#provider-credentials). **[AWS Systems Manager Parameter Store](/providers/awsps/)** stores every value as a KMS-encrypted `SecureString`. The `awsps://` provider uses the standard AWS credential and region chains and supports shared-config profiles, hierarchy prefixes, complete `{project}` / `{profile}` / `{key}` templates, customer-managed KMS keys, and parameter tiers. Refs can select an existing parameter by name, version, label, or ARN; unversioned name refs are writable, while pinned revisions remain read-only. Its bounded hierarchy discovery uses `GetParametersByPath` without decrypting values. **[Dashlane](/providers/dashlane/)** reads secrets, secure notes, and logins through the `dcli` CLI. It is intentionally read-only because `dcli` cannot create or edit vault items. A ref can address an existing item by title or identifier and select one of its fields. CI can provide `DASHLANE_SERVICE_DEVICE_KEYS` directly or source the same `service_device_keys` input from another SecretSpec provider. A project can route different secrets through any combination of them: secretspec.toml ```toml [providers] team_vault = "bw://Acme%20Inc@dev-secrets" keeper_ci = "keeper://SHARED_FOLDER_UID" parameters = "awsps://production@us-east-1?prefix=/platform" dashlane_notes = "dashlane://note" ``` Provider choice still stays outside application code. The CLI and every SDK resolve the same declaration regardless of which of these aliases supplies a value. ## Swift SDK [Section titled “Swift SDK”](#swift-sdk) The new [Swift SDK](/sdk/swift/) brings the shared SecretSpec resolver to macOS 12 or later on Intel and Apple silicon. Add the repository as a Swift package: ```swift dependencies: [ .package( url: "https://github.com/cachix/secretspec", from: "0.18.0" ), ] ``` Then use the same builder vocabulary as the other SDKs: ```swift import SecretSpec let resolved = try SecretSpec.builder() .withProfile("production") .withScope("api") .withReason("boot web app") .load() defer { try? resolved.close() } print(resolved.secrets["DATABASE_URL"]?.get() ?? "") try resolved.setAsEnvironment() ``` The SDK exposes fluent and one-shot resolution, typed failures, value-free preflight reports, scopes, provenance, environment export, and JSON input for generated Swift models. Calling `close()` deterministically removes temporary files created for `as_path` secrets. The SwiftPM release contains a checksummed XCFramework with the Rust resolver, so an application needs neither a Rust toolchain nor a separately installed SecretSpec library. ## Vault and OpenBao authentication [Section titled “Vault and OpenBao authentication”](#vault-and-openbao-authentication) [Vault](/providers/vault/) and [OpenBao](/providers/openbao/) deployments do not always use the default `approle` and `jwt` mount names. Their provider URIs can now choose a mount relative to `/v1/auth`: ```text vault://vault.example.com:8200/secret?auth=approle&auth_mount=platform-approle openbao://bao.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=deploy ``` AppRole authentication can omit `secret_id` when the server role is configured with `bind_secret_id=false`. JWT authentication can likewise omit its role when the selected mount has a server-configured `default_role`. Explicit URI, environment, or provider-credential inputs continue to take precedence. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` 0.18 also makes two local workflows less dependent on machine-specific setup: * custom [dotenv](/providers/dotenv/) paths accept a leading `~`, resolved to the current user’s home directory; * Linux [keyring](/providers/keyring/) builds use keyring 4’s Rust-native Secret Service transport, so SecretSpec binaries no longer require system `libdbus`. Existing manifests and providers continue to work unchanged. The new commands, providers, and SDK are opt-in. See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change and fix in this release. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.19: Moving and importing secrets between providers > Move secrets safely between storage layouts, keep config and ephemeral values in the same declaration model, and make remote providers faster. Secret storage changes as a project grows. Values move from local files to password managers, from one naming convention to another, and sometimes between providers with completely different data models. The application still expects the same `API_KEY` or `DATABASE_URL` at the end. [SecretSpec 0.19](https://github.com/cachix/secretspec/releases/tag/v0.19.0 "SecretSpec 0.19 release") treats those changes as a normal workflow instead of a one-off migration script. This release includes: * **[Provider-specific storage layouts](#provider-specific-storage-layouts)**: give each provider its own address, transform stored values, import existing files, and preview exact write references. * **[Config belongs in secrets](#config-belongs-in-secrets)**: resolve profile-specific config alongside stored secrets, generate ephemeral values, and securely prompt during `secretspec run`. * **[Passbolt provider](#passbolt-provider)**: read and write secrets in a self-hosted Passbolt server, with credentials supplied by another provider when needed. * **[Faster remote-provider workflows](#faster-remote-provider-workflows)**: attach a cache directly to an authoritative provider and batch 1Password field reads. * **[Smaller improvements](#smaller-improvements)**: create standalone profiles and install complete pkg-config metadata for native SDK consumers. ## Provider-specific storage layouts [Section titled “Provider-specific storage layouts”](#provider-specific-storage-layouts) `API_KEY` is the name used by your application. In 1Password, the same value might be the `token` field of an item named `old-api-item`. A [`ref`](/concepts/references/) gives SecretSpec this store address. `item` names the entry. Coordinates such as `field`, `section`, and `vault` locate a value inside structured stores. The `providers` list still decides which stores to try. Before 0.19, every provider in a secret’s route received the same `ref`, even though stores such as 1Password and dotenv organize secrets differently. Now each provider alias can template its usual layout, while `refs.` handles exceptions: secretspec.toml ```toml [providers] legacy = "onepassword://Legacy" production = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } local = { uri = "dotenv://.env", ref = { item = "{key}" } } [profiles.production] API_KEY = { description = "API key", providers = ["production", "local"], refs = { legacy = { item = "old-api-item", field = "token" } } } ``` `production` reads the `API_KEY` field from a `-production` 1Password item. The `local` fallback reads the dotenv key `API_KEY`. If `legacy` is selected explicitly, `refs.legacy` reads the `token` field from `old-api-item`. For each provider, `refs.` takes precedence over the alias’s `ref` template, which takes precedence over the provider convention. Templates accept `{project}`, `{profile}`, and `{key}` in every address field. Existing route-wide `ref` declarations remain supported. Because scoped references also apply to imports, that exception can describe a migration source without joining the normal fallback route: ```bash secretspec import legacy --profile production --delete-source ``` This reads from `refs.legacy` and writes through the `production` template. It also works between distinct entries in one physical store. SecretSpec rejects the import if both addresses resolve to the same entry. ### Transform stored values [Section titled “Transform stored values”](#transform-stored-values) Two new secret fields transform a stored value before it reaches the application. `extract` selects a value from JSON with an [RFC 6901 JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901): secretspec.toml ```toml [providers] runtime = "file:///run/secrets" [profiles.production] DATABASE_PASSWORD = { description = "Database password", providers = ["runtime"], ref = { item = "application.json" }, extract = { format = "json", pointer = "/database/password" } } ``` JSON strings become their unquoted contents. Numbers, booleans, objects, and arrays keep their JSON representation. Extracted declarations are read-only. `set`, `delete`, prompting, generation, and import cannot overwrite the source document. `encoding` defines the textual representation in provider storage: secretspec.toml ```toml [profiles.production] TEXT_CONFIG = { description = "Encoded configuration", encoding = "base64" } CLIENT_KEYSTORE = { description = "Binary client keystore", providers = ["runtime"], ref = { item = "client.p12.b64" }, encoding = "base64", as_path = true } ``` Supported encodings are standard Base64, URL-safe Base64, and hexadecimal. Writes encode the logical value. Reads decode the stored value. Decoded UTF-8 can be returned directly. Set `as_path = true` to materialize arbitrary bytes in a file. Transforms run in this order: ```text provider or cache → encoding decode → JSON extraction → as_path ``` This allows, for example, one declaration to decode a Base64-encoded JSON document and select one field from it. ### Import without reshaping the source [Section titled “Import without reshaping the source”](#import-without-reshaping-the-source) **[File](/providers/file/)** stores one plaintext UTF-8 file per secret beneath a required root. Convention paths use `{project}/{profile}/{key}`. `ref.item` selects an existing relative path, including a file mounted at runtime. Writes use atomic replacement and create private Unix files and directories. The provider rejects traversal and nested symlinks. It does not encrypt its contents. The file provider is also a migration adapter for directories that already contain one file per secret. A provider `ref` template maps the source layout, while the destination alias independently maps the same declarations into its native store: secretspec.toml ```toml [providers] legacy_files = { uri = "file:./old-secrets", ref = { item = "{profile}/{key}" } } production = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } [profiles.production.defaults] providers = ["production"] [profiles.production] API_KEY = { description = "Production API key" } ``` ```bash secretspec import legacy_files --profile production --delete-source ``` For `API_KEY`, the source is `old-secrets/production/API_KEY`. The destination is the `API_KEY` field in the `-production` 1Password item. The source files do not need to follow the SecretSpec convention. With `--delete-source`, 0.19 preflights every mapped source and destination, verifies every copied value, and only then removes the plaintext source files. Preflight, write, or verification failures leave every source untouched. A destination with a different existing value keeps its corresponding source. ### See the reference before writing [Section titled “See the reference before writing”](#see-the-reference-before-writing) `secretspec set` and interactive `secretspec check` now print the resolved write reference before reading a value: ```console $ secretspec set API_KEY --profile production --provider sops://secrets.enc.yaml Writing secret 'API_KEY' to sops://secrets.enc.yaml?format=yaml (profile: production) target: /work/my-app/secrets.enc.yaml ["my-app"]["production"]["API_KEY"] Enter value for API_KEY (profile: production): ******** ``` SOPS reports the canonical encrypted file and exact `sops set` selector. Other providers report their native item or path. A missing profile or unexpected template is visible before SecretSpec receives the new value. ## Config belongs in secrets [Section titled “Config belongs in secrets”](#config-belongs-in-secrets) **[Null](/providers/null/)** always reports a missing value and stores nothing. This lets manifest defaults provide non-sensitive values without adding a storage backend. One resolution can now return profile-specific configuration and provider-backed secrets together. This follows the separation described in [Secrets Don’t Belong in Config](/blog/secrets-dont-belong-in-config/). secretspec.toml ```toml [profiles.default] APP_MODE = { description = "Application mode", default = "local", providers = ["null"] } [profiles.staging] APP_MODE = { default = "staging" } [profiles.production] APP_MODE = { default = "production" } ``` `APP_MODE` resolves to `local`, `staging`, or `production` based on the selected profile. Each override inherits the description and `null` route from `[profiles.default]`. Only the value is repeated. The result is one declaration model for values the application needs, whether they come from a secret store or directly from the manifest. Config can travel through the same profile, scope, SDK, and `run` workflow without pretending it needs encrypted persistence. ### Ephemeral values [Section titled “Ephemeral values”](#ephemeral-values) The null provider can also generate a fresh value for each resolution. Use it for session tokens, test credentials, and other values that should exist only for one process invocation. Persistent credentials should continue to use a writable provider. ### Prompt for missing secrets during run [Section titled “Prompt for missing secrets during run”](#prompt-for-missing-secrets-during-run) Set `prompt = true` on a declaration to let `secretspec run` securely request its value when the configured providers do not have one: secretspec.toml ```toml [profiles.default] DEPLOY_PASSWORD = { description = "One-time deployment password", prompt = true, providers = ["null"] } ``` ```console $ secretspec run -- ./deploy ? Enter value for DEPLOY_PASSWORD (profile: default): ``` A writable provider saves the answer, turning the prompt into first-use provisioning. The `null` provider keeps it ephemeral and injects it only into that invocation. The hidden prompt reads from the controlling terminal, so the child’s stdin remains available for pipes and redirects. If no controlling terminal exists, `run` fails before starting the child. Declarations without `prompt = true` retain the existing fail-on-missing behavior. ## Passbolt provider [Section titled “Passbolt provider”](#passbolt-provider) Passbolt is the third new provider in 0.19. SecretSpec now has 27 providers. **[Passbolt](/providers/passbolt/)** reads and writes resources in a self-hosted Passbolt server through `go-passbolt-cli`. Convention values use the resource `secretspec/{project}/{profile}/{key}` and its `password` field. References can select existing resources by UUID or exact name and address the `password`, `username`, `uri`, or `description` field. secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.passbolt_team] uri = "passbolt://?server=https://pass.example.com&folder=a9230ec4-5507-4870-b8b5-b3f500587e4c" credentials = { private_key = "bootstrap", passphrase = "bootstrap" } ``` The OpenPGP private key and passphrase can come from another SecretSpec provider. Environment fallbacks and the Passbolt CLI configuration are also supported. Folder-scoped providers support declaration discovery with `init --from`. ## Faster remote-provider workflows [Section titled “Faster remote-provider workflows”](#faster-remote-provider-workflows) Remote secret reads pay for authentication, process startup, and network round-trips before the application can start. SecretSpec 0.19 reduces that work both across invocations and within one resolution. ### Cache one authoritative provider [Section titled “Cache one authoritative provider”](#cache-one-authoritative-provider) A single authoritative provider can now define `uri`, `credentials`, and `cache` on the same alias: secretspec.toml ```toml [providers] local = "keyring://secretspec/cache/{project}/{profile}/{key}" azure = { uri = "akv://team-vault", credentials = { client_secret = "keyring" }, cache = { provider = "local", max_age = "8h" } } [profiles.development.defaults] providers = ["azure"] ``` The cached `fallback` form introduced in 0.17 remains available when several authoritative providers can answer. Cache entries now include their absolute expiration time and originating `max_age`. SecretSpec removes an expired entry whenever it encounters one, and changing `max_age` invalidates entries written under the previous policy. Fallback resolution also reuses provider instances and handles independent primary misses concurrently. Azure Key Vault reuses its client and serializes initial challenge-based authentication, avoiding repeated Azure CLI processes within one resolution. ### Batch 1Password field reads [Section titled “Batch 1Password field reads”](#batch-1password-field-reads) 1Password field references now resolve together through one `op inject` call, instead of starting `op read` separately for every field. This reduces CLI startup and repeated unlock overhead when one profile loads several fields. If the batch contains a missing reference, SecretSpec falls back to bounded concurrent reads so it can preserve per-secret missing-value behavior without serializing the whole profile. In the cold-cache benchmark from [the implementation PR](https://github.com/cachix/secretspec/pull/317), a representative profile with 25 field references resolved in 11.890 seconds, down from 96.294 seconds. The batch used 3 `op` processes instead of 27, making that run 8.10 times faster. ## Smaller improvements [Section titled “Smaller improvements”](#smaller-improvements) ### Standalone profiles [Section titled “Standalone profiles”](#standalone-profiles) Profiles inherit `[profiles.default]` unless their defaults set `inherit = false`: secretspec.toml ```toml [profiles.default] DEV_DATABASE_URL = { description = "Developer database" } LOCAL_DEBUG_TOKEN = { description = "Local debugging token", required = false } [profiles.production.defaults] inherit = false providers = ["vault://vault.example.com:8200/secret"] [profiles.production] DATABASE_URL = { description = "Production database" } API_KEY = { description = "Production API key" } ``` `production` contains only its own declarations and fields. Other profiles in the same manifest can continue to inherit the default profile. ### pkg-config metadata for secretspec-ffi [Section titled “pkg-config metadata for secretspec-ffi”](#pkg-config-metadata-for-secretspec-ffi) > This section describes the 0.19 names. In SecretSpec 0.20+, the component is named `libsecretspec`, installed with `cargo cinstall -p libsecretspec`, and provides `libsecretspec.pc`. `cargo cinstall -p secretspec-ffi` now installs the library, C header, and a `secretspec_ffi.pc` file containing the complete link metadata. Go builds can use the `pkgconfig` tag, Ruby native extensions accept `--enable-pkg-config`, and Haskell builds use the `use-pkg-config` Cabal flag. The same metadata supports installed static or shared libraries. Haskell now declares its required macOS system frameworks. The Rust SDK’s `ProviderAlias` type also exposes `leaf`, `credentials`, and `credentials_mut` helpers for configuration tooling. ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash cargo install secretspec ``` Existing route-wide `ref` declarations, inheriting profiles, and cached fallback aliases remain compatible. All new configuration fields and providers are opt-in. 0.19 also: * fixes [concurrent keyring initialization](https://github.com/cachix/secretspec/issues/268). * preserves [non-UTF-8 environment values in `run` on Unix](https://github.com/cachix/secretspec/issues/140). * renders [SOPS path templates in one pass](https://github.com/cachix/secretspec/pull/271) and [validates deserialized path templates](https://github.com/cachix/secretspec/commit/bd448ad821d251f1d38a4235a1db868372bb2bd3). * preserves [complete multi-segment LastPass templates in route comparisons](https://github.com/cachix/secretspec/issues/272). * [refreshes fallback providers when a Rust `Secrets` instance is reused](https://github.com/cachix/secretspec/issues/283). * adds [`Secrets::resolve_named`](https://github.com/cachix/secretspec/pull/315) for resolving one secret without unrelated missing requirements. * rejects [credentials embedded in provider URIs](https://github.com/cachix/secretspec/pull/315). Use alias credentials or provider environment variables instead. See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change and fix in this release. ## Future work [Section titled “Future work”](#future-work) These items are not part of 0.19. They are open work for future releases: * **Native Windows ARM64 CLI archive (target: 0.19.1)**: add `secretspec-aarch64-pc-windows-msvc.zip` and its checksum to GitHub Releases so the CLI can run natively on Windows ARM64. The static installer will continue to select the x64 build on Windows ARM devices until it supports the native archive, and standalone updates depend on [axoupdater supporting Windows ARM64](https://github.com/axodotdev/axoupdater/pull/357). * **WinGet packaging**: publish the initial package tracked in [microsoft/winget-pkgs#413776](https://github.com/microsoft/winget-pkgs/pull/413776), then automate stable updates through [SecretSpec #297](https://github.com/cachix/secretspec/pull/297). * **[Notification and approval integrations](https://github.com/cachix/secretspec/issues/300)**: send new secret access requests to services such as email, Slack, or WhatsApp for approval. * **[JVM SDK](https://github.com/cachix/secretspec/issues/310)**: expose the shared SecretSpec resolver to Java, Kotlin, and other JVM languages. * **[Dart SDK](https://github.com/cachix/secretspec/issues/240)**: bring the shared resolver to Dart and Flutter applications. Every team has a secrets story. Come tell us yours on [Discord](https://discord.gg/naMgvexb6q). # SecretSpec 0.20: Git, Docker, inline specs, and five new providers > Let Git and Docker retrieve credentials from any provider, declare secrets in application code, use the JVM SDK, and deploy on Alpine. [SecretSpec 0.20](https://github.com/cachix/secretspec/releases/tag/v0.20.0 "SecretSpec 0.20 release") brings SecretSpec-managed credentials to Git and Docker, lets applications declare secrets directly in code, adds five providers, and expands support for JVM and Alpine applications. This release includes: * **[Git and Docker credential helpers](#git-and-docker-credential-helpers)**: let ordinary Git and Docker commands retrieve credentials from any SecretSpec provider without copying them into another credential store. * **[Declarations in application code](#declarations-in-application-code)**: build secret specifications in Rust and prompt for missing values from typed loaders. * **[libsecretspec and SDKs](#libsecretspec-and-sdks)**: pass inline specifications from eight SDKs, attach caller context, and adopt the new name for `secretspec-ffi`. * **[Five new providers](#five-new-providers)**: use [Azure App Configuration](/providers/aac/), [Kubernetes](/providers/kubernetes/), [EJSON](/providers/ejson/), [Fly.io](/providers/fly/), and [Cloudflare Secrets Store](/providers/cloudflare/). * **[JVM and Alpine support](#jvm-and-alpine-support)**: resolve secrets from Java and Kotlin, install static musl CLI builds, and run the Node.js SDK in Alpine images. * **[Other changes](#other-changes)**: forward container signals, report missing generated secrets correctly, emit shell completions, and make dotenv values round-trip. ## Git and Docker credential helpers [Section titled “Git and Docker credential helpers”](#git-and-docker-credential-helpers) Git and Docker can now read credentials directly from any SecretSpec provider. Configure each helper once, then keep using the usual Git and Docker commands. ### Git credentials [Section titled “Git credentials”](#git-credentials) The [Git integration](/integrations/git/) registers a helper for a host, then stores its token through your normal SecretSpec provider: ```bash $ secretspec git configure \ --url https://github.com \ --username YOUR_USERNAME $ secretspec git login https://github.com ``` After that, normal commands invoke `git-credential-secretspec` automatically: ```bash $ git clone https://github.com/OWNER/PRIVATE_REPOSITORY.git $ git push ``` The helper works before a repository has been cloned and can scope credentials by host or URL path. It also supports SMTP credentials for `git send-email`, so the password does not need to live in `sendemail.smtpPass`. ### Docker credentials [Section titled “Docker credentials”](#docker-credentials) The [Docker integration](/integrations/docker/) uses one helper per registry. Configure the registry and store its token separately: ```bash $ secretspec docker configure \ --registry ghcr.io \ --username YOUR_USERNAME $ secretspec docker login ghcr.io ``` `docker pull`, `docker push`, `docker build`, and Docker Compose can now retrieve the value through `docker-credential-secretspec`: ```bash $ docker pull ghcr.io/OWNER/IMAGE:TAG ``` Docker’s `config.json` contains only the helper configuration, not the credential value. Separate `DOCKER_CONFIG` directories can use different credentials for the same registry. The helpers only read credentials. Use `secretspec git login` / `logout` and `secretspec docker login` / `logout` to change stored values. Use `configure` and `unconfigure` to add or remove the helpers themselves. ## Declarations in application code [Section titled “Declarations in application code”](#declarations-in-application-code) `secretspec.toml` remains the portable contract shared by the CLI and every SDK. Applications can now define that contract directly when keeping a separate manifest is inconvenient. ### Rust-first specifications [Section titled “Rust-first specifications”](#rust-first-specifications) Rust applications can use the new public `Spec`, `Profile`, and `Secret` types to build a specification in code: ```rust use secretspec::{Profile, Secret, Secrets, Spec}; let spec = Spec::builder("checkout") .provider("env", "env://") .secret( "DATABASE_URL", Secret::required("PostgreSQL connection URL").providers(["env"]), ) .profile( "production", Profile::new().secret( "SENTRY_DSN", Secret::required("Production Sentry endpoint"), ), ) .build()?; let mut secrets = Secrets::from_spec(spec)?; secrets.set_profile("production"); let resolved = secrets.resolve()?; ``` Rust-built specs use the same validation as `secretspec.toml`. `Spec::schema_json` generates the same JSON Schema as `secretspec schema`, and `SpecBuilder` can update a file-backed spec without losing comments or formatting. See [Describing secrets in Rust](/sdk/rust/#describing-secrets-in-rust-020) for the complete builder and format-preserving editing API. Typed Rust loaders generated by `declare_secrets!` can now call [`prompt_missing()`](/sdk/rust/#interactive-prompting-020) to ask for and store missing required values. Prompting remains opt-in. ## libsecretspec and SDKs [Section titled “libsecretspec and SDKs”](#libsecretspec-and-sdks) SecretSpec 0.20 gives the SDKs the same new capabilities across languages. It also renames `secretspec-ffi` to [`libsecretspec`](/sdk/overview/). Packaged SDKs handle the rename automatically; update your build only if you link or load `libsecretspec` directly. ### Inline specifications [Section titled “Inline specifications”](#inline-specifications) The [SDKs](/sdk/overview/) for Go, Python, Node.js, Ruby, Haskell, PHP, C#, and Swift can now accept an inline specification. This is useful when the application already owns its configuration or cannot rely on a manifest on disk. For example, in Go: ```go spec := map[string]any{ "project": map[string]any{"name": "checkout"}, "profiles": map[string]any{"default": map[string]any{ "secrets": map[string]any{ "API_TOKEN": map[string]any{"description": "API token"}, }, }}, } resolved, err := secretspec.New(). WithInlineSpec(spec, "/logical/project"). Load() ``` The base directory tells SecretSpec where to resolve relative provider paths and inherited manifests. Invalid fields and unsupported spec versions fail with an error. ### Caller context [Section titled “Caller context”](#caller-context) Git and Docker set caller context automatically, recording which tool and operation requested a secret. CLI and SDK callers can provide the same context: ```bash $ secretspec get GITHUB_TOKEN \ --caller git \ --caller-version 2.51.0 \ --caller-operation credential_get \ --caller-resource github.com \ --reason "push release tag" ``` [Audit records](/concepts/audit/) include the caller name, version, operation, and resource. This is separate from [`require_reason`](/reference/configuration/#requiring-a-reason-for-secret-access), so policies can record both which tool accessed a secret and why. ### JVM and Alpine support [Section titled “JVM and Alpine support”](#jvm-and-alpine-support) The new [JVM SDK](/sdk/jvm/) lets Java, Kotlin, and other JVM applications load SecretSpec secrets directly. Add it with Gradle: ```kotlin dependencies { implementation("org.cachix:secretspec-jvm:0.20.0") } ``` Then load secrets with the same builder pattern as the other SDKs: ```java import org.cachix.secretspec.SecretSpec; try (var resolved = SecretSpec.builder() .withProvider("keyring://") .withProfile("production") .withReason("boot web app") .load()) { System.out.println(resolved.secret("DATABASE_URL").get()); resolved.setAsSystemProperties(); } ``` The package supports JDK 11 or newer on Linux, macOS, and Windows, including x64 and Arm64 systems. It includes everything needed to resolve secrets, so the application does not need a separate SecretSpec CLI or Rust toolchain. The standalone installer and `secretspec-update` now work on Alpine Linux. The Node.js SDK also works directly in images such as `node:alpine`, without a glibc compatibility layer. ## Five new providers [Section titled “Five new providers”](#five-new-providers) SecretSpec 0.20 adds five providers, bringing the total to 33. ### Azure App Configuration [Section titled “Azure App Configuration”](#azure-app-configuration) Use **[Azure App Configuration](/providers/aac/)** to read and manage ordinary key-values or follow references to Azure Key Vault secrets. ```bash $ az login $ secretspec set DATABASE_URL --provider aac://payments-production $ secretspec run --provider aac://payments-production -- ./payments ``` It works with service principals, Azure CLI sessions, managed identity, workload identity, and connection strings. Provider URLs can select labels, prefixes, and tags. ### Kubernetes [Section titled “Kubernetes”](#kubernetes) **[Kubernetes](/providers/kubernetes/)** stores values in a ConfigMap or Secret using the current kubeconfig context: ```bash $ secretspec set DATABASE_URL \ --provider k8s+secret://app-credentials@production $ secretspec run \ --provider k8s+secret://app-credentials@production \ -- ./application ``` You can use convention keys or point declarations at existing `.data` entries. The provider supports reads, writes, deletion, and declaration discovery. ### EJSON [Section titled “EJSON”](#ejson) **[Shopify’s EJSON](https://github.com/Shopify/ejson)** lets teams keep encrypted secrets alongside application source. Secret values are encrypted with a public key and decrypted with the matching private key, while the JSON structure stays visible. SecretSpec’s [EJSON provider](/providers/ejson/) reads string values from those files. A `ref.item` selects any RFC 6901 JSON Pointer, while convention addresses use `/{project}/{profile}/{key}`. Supply the EJSON private key as a SecretSpec [provider credential](/reference/provider-credentials/). It can come from Google Cloud Secret Manager, the system keyring, or any other readable provider. The EJSON provider is read-only. ### Fly.io [Section titled “Fly.io”](#flyio) Use **[Fly.io](/providers/fly/)** to publish and delete application secrets with `flyctl`. Secret values stay out of process arguments, and app-scoped deploy tokens can come from provider credentials. Fly.io does not return plaintext, so keep the original value in a readable provider and use `fly` as the deployment destination. ### Cloudflare Secrets Store [Section titled “Cloudflare Secrets Store”](#cloudflare-secrets-store) Use **[Cloudflare Secrets Store](/providers/cloudflare/)** to publish and delete account-level secrets through Cloudflare’s API. Authenticate with a scoped API token from another provider or an existing Wrangler session. Cloudflare does not return plaintext, so keep the original value in a readable provider and use `cloudflare` as the deployment destination. ## Other changes [Section titled “Other changes”](#other-changes) Shell completions now come directly from the CLI definition: ```bash $ source <(secretspec completions bash) ``` Bash, Elvish, Fish, Nushell, PowerShell, and Zsh now complete commands, profiles, scopes, secret names, providers, files, and executables. Completion does not read secret values. See [`secretspec completions`](/reference/cli/#completions-020) for persistent installation instructions ([#330](https://github.com/cachix/secretspec/pull/330)). 0.20 also includes these CLI and automation changes: * On Unix, `secretspec run` forwards `SIGTERM`, `SIGINT`, and `SIGHUP` to its child, so containers can shut down gracefully ([#391](https://github.com/cachix/secretspec/pull/391)). * `check --json`, `check --explain`, and SDK reports now mark an unprovisioned required [`generate` declaration](/concepts/generation/) as `missing_required` instead of resolved. Run `secretspec check` or `secretspec run` once to generate and store it ([#394](https://github.com/cachix/secretspec/pull/394)). * Human-readable `secretspec check` output moves to stdout, matching its JSON and explain modes. Diagnostics remain on stderr ([#372](https://github.com/cachix/secretspec/issues/372)). * Dotenv files, age-encrypted dotenv blobs, and `export --format dotenv` now use dotenv-ng. Dollar signs and bcrypt-style values now round-trip correctly ([#73](https://github.com/cachix/secretspec/issues/73)). * [`extract`](/reference/configuration/#structured-extraction-019) supports INI documents, selecting unsectioned keys with `/key` and named-section keys with `/section/key` ([#386](https://github.com/cachix/secretspec/pull/386)). * The [age provider](/providers/age/) can now delete secrets, enabling `secretspec delete`, `import --delete-source`, and age-backed provider caches ([#328](https://github.com/cachix/secretspec/pull/328)). * Closing a stdout pipe is quiet on Unix, so commands such as `secretspec export | head` behave like other Unix tools ([#377](https://github.com/cachix/secretspec/pull/377)). Provider-specific fixes keep 1Password batches fast when optional items are missing ([#401](https://github.com/cachix/secretspec/pull/401)), prevent Bitwarden convention names from colliding across projects and profiles ([#390](https://github.com/cachix/secretspec/pull/390)), make Infisical Universal Auth sessions more reliable ([#402](https://github.com/cachix/secretspec/pull/402)), and let Node.js applications exit cleanly after AWS resolution ([#365](https://github.com/cachix/secretspec/pull/365)). ## Upgrading [Section titled “Upgrading”](#upgrading) ```bash $ cargo install secretspec ``` For most users, 0.20 is a drop-in upgrade. The new integrations, providers, and inline declarations are opt-in. Check these cases before upgrading: * scripts that captured human-readable `secretspec check` output from stderr should capture stdout; * Google Cloud Secret Manager writes now use the collision-safe `secretspec2--{project}--{profile}--{key}` convention. Reads fall back to the matching 0.19 name until a write moves the value, leaving the old secret in place for rollback; * Bitwarden Password Manager convention items now use `secretspec/{project}/{profile}/{key}` titles. `init --from bw://` preserves references to legacy bare items so they can keep working or be migrated deliberately. Rust applications using `secretspec-derive` can remove direct `serde` or `secrecy` dependencies if they were needed only for generated types. Code using the old raw configuration or code-generation APIs should move to `Spec`, `SpecBuilder`, `Profile`, and `Secret`. See the [full changelog](https://github.com/cachix/secretspec/blob/main/CHANGELOG.md) for every change and fix in this release. ## What’s next [Section titled “What’s next”](#whats-next) ### Resolver and provider IPC [Section titled “Resolver and provider IPC”](#resolver-and-provider-ipc) The resolver and provider IPC protocols are currently RFCs in [PR #362](https://github.com/cachix/secretspec/pull/362). The [resolver protocol](https://feat-ipc-v1-secretspec.domen.workers.dev/reference/resolver-protocol/) would let applications request individual secrets from `secretspec serve`. The [provider protocol](https://feat-ipc-v1-secretspec.domen.workers.dev/reference/provider-protocol/) would let external executables act as SecretSpec providers. Both are targeted for 0.21 and may change during review. ### Nix secret-provider plumbing [Section titled “Nix secret-provider plumbing”](#nix-secret-provider-plumbing) Work on the Nix integration identified that SecretSpec first needs the IPC layer above, so the concrete integration is blocked until 0.21 is released. In the meantime, the underlying refactoring has been split into a [generic secret-provider interface for Nix](https://github.com/NixOS/nix/pull/16339). ### devenv machines [Section titled “devenv machines”](#devenv-machines) Separately, [devenv’s experimental `machines` interface](https://github.com/cachix/devenv/pull/3073) aims to handle NixOS installation, NixOS and nix-darwin deployment, and home-manager activation in one workflow. Its SecretSpec integration can supply deployment credentials without putting their values in Nix evaluation or store paths. This work follows devenv’s own release schedule and is not part of SecretSpec 0.21. Questions or feedback? Join us on [Discord](https://discord.gg/naMgvexb6q). ## Fencer for Open Source [Section titled “Fencer for Open Source”](#fencer-for-open-source) In partnership with SecretSpec, [Fencer has expanded its offering to open source](https://www.fencer.dev/blog/introducing-fencer-for-open-source). Public repositories now get free static analysis, dependency scanning, secret scanning, and GitHub configuration scanning, with no credit card and no expiry. We recommend Fencer for code security scanning, and Fencer recommends SecretSpec for secrets management. Fencer finds credentials and other risks already in a repository, while SecretSpec helps keep the next secret in the right provider instead of source code or a dotenv file. # We Are Forking dotenvy into dotenv-ng > A modern Rust dotenv implementation with literal dollars, round-trip rendering, and structured parse errors. We have released [`dotenv-ng`](https://github.com/cachix/dotenv-ng) 1.0, a modern Rust implementation for loading and rendering `.env` files. It began as a fork of [`dotenvy`](https://github.com/allan2/dotenvy) after its parser changed a secret while reading it. That may sound contradictory. SecretSpec is still on a mission to [eliminate environment variables as a secrets interface](/blog/secrets-dont-belong-in-config/), and we have written about [where `.env` went wrong](/blog/where-env-went-wrong/). It should not be the final home of a secret. But migrating away from `.env` starts with reading it correctly. ## Why fork dotenvy? [Section titled “Why fork dotenvy?”](#why-fork-dotenvy) The immediate failure was [SecretSpec issue #73](https://github.com/cachix/secretspec/issues/73). A dotenv file contained a value with bcrypt fragments: ```dotenv TEST="foo:$2a$10$TWoviNHS27HJMw1PKe4tBeIMlms6tWdYS9hKoHANKCQhluDlEt/gu" ``` The file was intact. Reading it through the dotenv provider returned a different value because `dotenvy` treated the dollar-prefixed fragments as variable substitutions. The failure appeared later as an authentication error, not a parse error. An upstream request to make substitution configurable had been [open since 2024](https://github.com/allan2/dotenvy/issues/113). A [pull request](https://github.com/allan2/dotenvy/pull/167) arrived in 2026 but targeted an unreleased API. A migration tool cannot require users to recognize and escape parser syntax inside their secrets. ## The maintenance gap [Section titled “The maintenance gap”](#the-maintenance-gap) The original Rust `dotenv` crate stopped releasing in 2020 and was eventually marked [unmaintained by RustSec](https://rustsec.org/advisories/RUSTSEC-2021-0141.html), which listed dotenvy as an alternative. Dotenvy’s description still calls it “a well-maintained fork.” Its latest published version, [0.15.7, was released on March 22, 2023](https://github.com/allan2/dotenvy/releases/tag/v0.15.7). A [Rust forum discussion](https://users.rust-lang.org/t/recommended-crate-for-storing-keys-for-web-site-database/133305/11) noted the two-year release gap in 2025. By the time the bcrypt bug blocked SecretSpec, it was more than three years. There is an uncomfortable irony in a maintained fork repeating its upstream’s release problem. Its maintainers do not owe us a release, but SecretSpec needed breaking fixes on a schedule we control. ## What does dotenv-ng improve upon? [Section titled “What does dotenv-ng improve upon?”](#what-does-dotenv-ng-improve-upon) We first considered a small patch. Auditing the parser uncovered more problems around JSON, Windows paths, Unicode names, precedence, and partial environment mutation. `dotenv-ng` therefore starts from dotenvy 0.15.7 but deliberately breaks compatibility where correctness requires it. Version 1.0 adds: * a source-aware parser with structured errors; * literal dollar signs by default, with substitution available only when a caller explicitly enables it; * a broader key grammar that supports dashes, leading digits, leading dots, and Unicode; * a renderer that adds only the quoting and escaping needed to parse a value back unchanged; * validation before process-environment mutation; and * an explicit `unsafe` boundary around that mutation. Property tests exercise arbitrary Unicode and syntax-heavy values, check that quoting is used only when necessary, and round-trip complete documents. The parser and renderer, the core of the rewrite, both have 100% line coverage. The complete compatibility and API changes are recorded in the [`dotenv-ng` 1.0 changelog](https://github.com/cachix/dotenv-ng/blob/v1.0.0/CHANGELOG.md). ## Try dotenv-ng 1.0 [Section titled “Try dotenv-ng 1.0”](#try-dotenv-ng-10) The package is available on [crates.io](https://crates.io/crates/dotenv-ng). Applications can keep the familiar `dotenv` crate name with a dependency alias: ```toml [dependencies] dotenv = { package = "dotenv-ng", version = "1" } ``` Starting in SecretSpec 0.20, dotenv-ng powers dotenv parsing and rendering throughout SecretSpec. # Where .env Went Wrong > A local convenience became a secrets interface, an environment model, and a deployment format. It was never designed to be any of them. `.env` is one of software’s most successful accidents. It starts as a shortcut for three `export` commands. Then it becomes the project’s configuration schema, secret store, environment model, onboarding guide, CI interface, and deployment format. Environment variables do one job well: deliver strings to a process. `.env` turned that delivery mechanism into a source of truth. A convenience became architecture. That is where `.env` went wrong. ## Environment variables only deliver values [Section titled “Environment variables only deliver values”](#environment-variables-only-deliver-values) Environment variables solve a small problem: getting values into a running process. The application can read `DATABASE_URL` without knowing whether a developer, CI system, or secrets manager supplied it. A `.env` file makes those values easy to save and reload. That is useful. But teams also use the file to describe what the application needs. `KEY=value` cannot say whether a value is required, secret, safe to commit, available only in production, or restricted to one service. Those requirements outlive any process and any developer laptop. They belong in a durable project declaration. `.env` stores values for delivery; it cannot define the application’s secret model. How does SecretSpec solve this? SecretSpec separates the committed [declaration](/concepts/declarative/) from value [storage](/concepts/providers/) and delivery. The CLI and [SDKs](/sdk/overview/) resolve the same declaration regardless of where values live or how applications receive them. ## A string is not a schema [Section titled “A string is not a schema”](#a-string-is-not-a-schema) Consider a typical example file: .env.example ```dotenv DATABASE_URL= REDIS_URL=redis://localhost:6379 STRIPE_API_KEY= DEBUG=false ``` The file raises more questions than it answers. Does an empty value mean required or optional? Is `REDIS_URL` a development default? Is `STRIPE_API_KEY` production-only? Is `DEBUG` a boolean? Dotenv cannot encode those answers. Node.js documents that [every value becomes a string](https://nodejs.org/api/environment_variables.html#variable-values). A [dotenv issue about booleans](https://github.com/motdotla/dotenv/issues/51), opened in 2015, still collects reactions from developers surprised that `"false"` is truthy. Teams put the missing information elsewhere: validation code, a README, `.env.example`, or a teammate’s memory. These sources drift. The file also makes `DEBUG` and `STRIPE_API_KEY` look equivalent. One is an ordinary setting that belongs in Git. The other grants authority and needs access control and rotation. Mixing them makes the whole file sensitive. Without an explicit declaration, missing values fail late: the application discovers them only when code tries to use them. How does SecretSpec solve this? The [SecretSpec declaration](/reference/configuration/) records names, descriptions, required values, and safe defaults. `secretspec check` and `secretspec run` validate those requirements before the application starts, while ordinary settings such as `DEBUG` remain in application configuration. ## Then the file starts to multiply [Section titled “Then the file starts to multiply”](#then-the-file-starts-to-multiply) A new requirement usually creates another file: ```text .env .env.local .env.development .env.development.local .env.test .env.production ``` The filenames become an environment model. Suffixes define scope, load order defines inheritance, and copying a file becomes deployment. This reverses the [Twelve-Factor App’s guidance](https://12factor.net/config). Its point was that environment variables should be independent controls because named environments become brittle as deployments multiply. `.env.production` recreates that grouping in a filename. Now every new value must be added to `.env.example`, documented in a README, validated in code, and copied into the right real files. Miss one and the environments drift. How does SecretSpec solve this? [Profiles](/concepts/profiles/) express real requirement differences as sparse overlays on `profiles.default`. Each deployment selects its values through providers instead of maintaining a complete, copied secret file. ## There is no `.env` spec [Section titled “There is no .env spec”](#there-is-no-env-spec) `.env` looks standardized, but every parser defines its own format. [Node.js documents the lack of a formal specification](https://nodejs.org/api/environment_variables.html#env-files), as does [python-dotenv](https://github.com/theskumar/python-dotenv#file-format). Each loader makes its own choices. python-dotenv expands `${NAME}` but not `$NAME`. Node dotenv delegates [variable expansion](https://github.com/motdotla/dotenv#variable-expansion) to another tool. Docker Compose supports its own [shell-style operators](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation). Vite even supports references in reverse order, then [warns](https://main.vite.dev/guide/env-and-mode#expanding-variables-in-reverse-order) that the same expression will not work in a shell or Docker Compose. Comments and quotes differ too. Node dotenv changed the meaning of `#` in unquoted values in version 15 as a [breaking change](https://github.com/motdotla/dotenv#comments). One devenv user found that [quotes became part of an exported key](https://github.com/cachix/devenv/issues/1333). How does SecretSpec solve this? SecretSpec defines one TOML declaration and one resolution model shared by its CLI and [SDKs](/sdk/overview/). Dotenv parsing is confined to the [compatibility provider](/providers/dotenv/), so changing loaders or storage backends does not change the application’s declaration. ## Which value wins? [Section titled “Which value wins?”](#which-value-wins) Parsers also disagree about precedence. [Node dotenv](https://github.com/motdotla/dotenv#path) normally lets the first file win. [Docker Compose](https://github.com/compose-spec/compose-spec/blob/main/spec.md#env_file) lets the last `env_file` win, then lets the `environment` section override that. [Vite](https://main.vite.dev/guide/env-and-mode#env-loading-priorities) gives an existing process variable priority over its files. Docker Compose gives two similar names different behavior. `env_file:` supplies variables to a container but does not use them to interpolate `compose.yaml`. `docker compose --env-file` does affect interpolation. In [an issue closed as working as designed](https://github.com/docker/compose/issues/9443), a maintainer described the option’s name as unfortunately chosen. How does SecretSpec solve this? SecretSpec applies one deterministic [provider resolution order](/concepts/providers/fallback/#provider-selection-order). Per-secret routes and fallbacks are explicit in the declaration, and the same resolver applies them across the CLI and SDKs. ## Who loaded `.env` first? [Section titled “Who loaded .env first?”](#who-loaded-env-first) Precedence also depends on timing. Node dotenv’s [ES module guidance](https://github.com/motdotla/dotenv/blob/94f6542d5c8b1ab211cab0dcd8f7aa907dd39124/README.md#L406-L435) needs special handling when imported modules read the environment during initialization. Vite warns that Bun’s automatic `.env` loading can interfere with [Vite’s own loading order](https://main.vite.dev/guide/env-and-mode#env-files). `VITE_*` values are replaced at build time and become part of the [client bundle](https://main.vite.dev/guide/env-and-mode#env-variables). The same line can become a runtime secret, a build-time constant, or a public browser value. The loader decides based on timing and context. At that point `.env` behaves like a small program, with control flow spread across filenames, flags, working directories, parent processes, and library versions. How does SecretSpec solve this? [`secretspec run`](/reference/cli/#run) resolves and validates secrets before launching the child, so its complete environment exists from process startup. Applications using an SDK load the declaration explicitly instead of depending on a module-import side effect. ## An ignored file is still a file [Section titled “An ignored file is still a file”](#an-ignored-file-is-still-a-file) The dotenv project says [not to commit `.env`](https://github.com/motdotla/dotenv#should-i-commit-my-env-file). `.gitignore` prevents one accident. It does not add encryption, access control, auditing, or revocation. The file can still end up in editor backups, chat messages, archives, support bundles, container build contexts, and old laptops. A devenv integration was [reported to copy `.env` contents into the Nix store](https://github.com/cachix/devenv/issues/1694), where paths are not confidential. When a developer leaves, there is no file access to revoke. Each credential they received is a separate copy. Even a secret stored in 1Password, Vault, a cloud secret manager, or a system keyring must be copied into plaintext before a dotenv-based application can use it. The local copy has fewer controls than the original. Environment-variable delivery has limits too. Docker mounts managed secrets as files because environment variables can [leak between containers](https://docs.docker.com/engine/swarm/secrets/#build-support-for-docker-secrets-into-your-images). A process also gets one global map, so a frontend build, worker, migration, and web service often receive the same secrets even when each needs only a few. Dotenv has no way to express that scope. How does SecretSpec solve this? The committed declaration contains no secret values. Providers supply storage, encryption, identity, and access control, while the [metadata-only audit log](/concepts/audit/) records local access. [Scopes (0.17+)](/concepts/scopes/) let each service or command resolve only its declared subset. ## Let `.env` become small again [Section titled “Let .env become small again”](#let-env-become-small-again) The useful part of `.env` is the short path from “this application needs a value” to “the application can run.” Keep it as an adapter for tools that expect `KEY=value`, or use it for ordinary local settings. Do not make it define the project’s requirements, store durable copies of secrets, encode environments in filenames, or decide which services receive which values. A durable design separates three jobs: * a committed declaration says which secrets the application needs; * protected storage controls who can read their values; * explicit delivery gives each process only the values it needs. Each piece can then change independently. A team can change storage without rewriting the application, validate requirements before startup, and limit each component to its own secrets. ## How SecretSpec applies this [Section titled “How SecretSpec applies this”](#how-secretspec-applies-this) SecretSpec puts the declaration in a file that is safe to commit: secretspec.toml ```toml [project] name = "payments" revision = "1.0" [profiles.default] DATABASE_URL = { description = "Postgres connection string" } REDIS_URL = { description = "Redis connection string", required = false } STRIPE_API_KEY = { description = "Stripe API key" } [profiles.development] REDIS_URL = { default = "redis://localhost:6379" } ``` This file records requirements, defaults, and descriptions without containing secret values. [Providers](/concepts/providers/) choose where values live, and [profiles](/concepts/profiles/) describe real differences in requirements. Existing programs can adopt SecretSpec without code changes: ```bash $ secretspec run -- ./server ``` This command injects resolved secrets into the child process environment. It is useful during migration, while the preferred integration is a [SecretSpec SDK](/sdk/overview/). With an SDK, the application resolves its declaration directly. This removes the environment-variable handoff used by `secretspec run`. Values stored in a keyring, password manager, or Vault never enter the global process environment. Applications that require a file can receive a [temporary file](/reference/configuration/#as_path-option) instead. [Scopes (0.17+)](/concepts/scopes/) let each component resolve only the secrets it declares. Migration can be gradual. SecretSpec initializes a declaration from an existing file: ```bash $ secretspec init --from dotenv:.env ``` This copies names without copying values. The current file can remain a provider during the transition: ```bash $ secretspec check --provider dotenv:.env $ secretspec run --provider dotenv:.env -- ./server ``` Values can then move to a system keyring, password manager, Vault, or another provider without changing the names the application reads. `.env` can remain for ordinary local settings. Existing applications can keep environment-variable delivery while they migrate. Applications using an SDK or file-based delivery can remove secrets from their process environments. **SecretSpec aims to eliminate environment variables for secrets altogether.** # GitHub Actions > Resolve secrets from secretspec.toml in GitHub Actions, Forgejo Actions, and other CI systems In a GitHub or Forgejo Actions job, `secretspec-action` installs the CLI and runs `secretspec export --format gha`, which masks every value in the runner log and appends `KEY=value` to `$GITHUB_ENV`. Every later step, including third-party actions, then sees the secrets as ordinary environment variables. ```yaml jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: cachix/secretspec-action@main with: profile: production - run: ./deploy.sh ``` A missing required secret fails the step before the job runs anything with an incomplete environment. ## Fetching from a secret manager [Section titled “Fetching from a secret manager”](#fetching-from-a-secret-manager) For secrets kept in a dedicated store, resolve them on the runner with the matching provider, shown here with [Vault or OpenBao](/providers/vault/). Other stores plug in the same way with their own credentials. Grant the job `id-token: write` and select `?auth=jwt`. Pinning a `role`, as in the example below, keeps the workflow independent of server configuration. Starting with SecretSpec 0.18, the role may instead come from the JWT auth mount’s configured `default_role`. Vault exchanges the runner’s OIDC token for a client token, so nothing is stored on the platform. ```yaml - uses: cachix/secretspec-action@main with: profile: production provider: vault://vault.example.com:8200/secret?auth=jwt&role=ci ``` Without an OIDC identity to draw on, select `?auth=approle` instead and pass `VAULT_ROLE_ID` and `VAULT_SECRET_ID` as CI secrets. ```yaml - uses: cachix/secretspec-action@main with: profile: production provider: vault://vault.example.com:8200/secret?auth=approle env: VAULT_ROLE_ID: ${{ secrets.VAULT_ROLE_ID }} VAULT_SECRET_ID: ${{ secrets.VAULT_SECRET_ID }} ``` ## Other CI systems [Section titled “Other CI systems”](#other-ci-systems) `secretspec-action` is a convenience wrapper around commands that work anywhere the CLI is installed. * `secretspec run -- ` runs a single command with the secrets confined to its environment. * `secretspec export` writes the resolved secrets to stdout for a tool that cannot be wrapped, such as a containerized pipeline. `eval "$(secretspec export)"` loads them into the current shell, while `--format dotenv` and `--format json` feed other consumers. Both resolve through the same provider chain and fail on a missing required secret. # Comparison > See how SecretSpec and its providers work together SecretSpec is the application-facing layer of a secrets system. It defines what an application needs, resolves those requirements across environments, and delivers the resulting values through its CLI and [provider-independent SDKs](/sdk/overview/). Providers connect that layer to concrete value sources. Depending on the backend, they can add secure storage, identity, access control, availability, and provider-native operations. This separation keeps [`secretspec.toml`](/concepts/declarative/) portable. A developer can use the system keyring, CI can supply environment variables, and production can use Vault or a cloud secret manager without changing the application’s secret contract. ```text secretspec.toml SecretSpec Provider what the app needs → resolve · check · deliver ← provider-backed values route · audit source · access ``` ## Division of responsibility [Section titled “Division of responsibility”](#division-of-responsibility) | Responsibility | SecretSpec | Providers augment SecretSpec with | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Application secret contract | Declares names, descriptions, [requirements and defaults](/reference/configuration/#secret-variable-options), [generated values](/concepts/generation/), and [composed values](/concepts/composed-secrets/) | Supply provider-backed values named by that contract | | Environments | Defines portable [profiles](/concepts/profiles/), [configuration inheritance](/concepts/inheritance/), and profile-specific requirements | Add provider-native projects, vaults, paths, or environments | | Preflight validation | [`check`](/reference/cli/#check) validates required secrets and configuration before the application starts | Report whether a requested value exists or can be accessed | | Provider selection | Routes each secret independently through [provider aliases and ordered fallback chains](/concepts/providers/fallback/) | Supply concrete sources and destinations | | Existing provider-native secrets | Uses [secret references](/concepts/references/) to give an existing value a stable, application-facing name | Interpret provider-specific coordinates such as vault, item, field, path, or version | | Application delivery | Resolves secrets through the [CLI](/reference/cli/), [exports environments](/reference/cli/#export), [starts child processes](/reference/cli/#run), and [manages temporary files](/reference/configuration/#as_path-option) | Supply values through provider APIs or clients | | Application SDKs | Offers one [provider-independent resolver](/sdk/overview/#one-resolver-thin-clients) with a shared [runtime API](/sdk/overview/#the-runtime-api) and [typed access](/sdk/overview/#typed-access) across supported programming languages | Vendor SDKs, when available, remain backend-specific; applications do not need to integrate them directly | | Audit | Records [local, metadata-only access events](/concepts/audit/) by default, including application context and optional reason | Add centralized, provider-side access records where supported and configured | | Encryption at rest | Delegates protection of provider-backed values to the selected provider | Protect values when the backend supports encryption; dotenv and environment providers add no at-rest encryption | | Identity and access policy | Uses the credentials available for the selected provider, including [credentials sourced from another provider](/reference/provider-credentials/) | Enforce users, roles, service identities, policies, and sharing | | Availability and retention | Delegates these guarantees for provider-backed values | May provide synchronization, replication, versions, backup, or retention, depending on the provider | | Dynamic secrets and credential rotation | [Roadmap](https://github.com/cachix/secretspec/issues/11); not currently available and has no assigned target release | Provide native lifecycle features where available; use them outside SecretSpec today | The distinction is intentional: SecretSpec provides portable application semantics, while each provider determines how its provider-backed values are stored, protected, and operated. Some providers, such as dotenv and environment variables, intentionally provide fewer safeguards. SecretSpec’s [default audit log](/concepts/audit/) complements provider logs by recording the project, profile, secret name, outcome, actor, and reason seen by the application workflow. It is a size-bounded, best-effort local log, not a replacement for central compliance records. ## Supported providers [Section titled “Supported providers”](#supported-providers) See [Available providers](/concepts/providers/#available-providers) for the provider comparison, including storage backend, read and write support, encryption at rest, and TPM-backed keys. Providers can be mixed within one project. For example, an application can read a shared credential from 1Password in the production profile, read the same secret from the system keyring in the development profile, and accept a deployment token from the environment in CI. A secret can also define an ordered fallback chain, which tries the next provider when an earlier provider does not return the value. SecretSpec keeps those storage decisions outside the application’s code. # Audit Logging > A local, append-only record of every secret access for after-the-fact review secretspec records every secret access to a local audit log so you can review, after the fact, **what** secret was accessed, **when**, by **whom**, with what **reason, if supplied**, which software integration called SecretSpec (0.20+), and what the **outcome** was. Auditing is **on by default**. Secret values are never written to the log. Only metadata is recorded, and any credentials embedded in a provider URI are redacted. ## Where the log lives [Section titled “Where the log lives”](#where-the-log-lives) By default the log is written to the per-user state directory, one entry per line in [JSON Lines](https://jsonlines.org/) format: | Platform | Default path | | -------- | ------------------------------------- | | Linux | `~/.local/state/secretspec/audit.log` | | macOS | `~/.local/state/secretspec/audit.log` | (secretspec follows the XDG state-directory convention on macOS too, matching where it keeps its config, so the path is the same as on Linux. Set `[audit] path` to override it.) The file is created with owner-only permissions (`0600` on Unix), inside an owner-only directory (`0700`). The first time secretspec writes to it, it prints a one-time note telling you where the log is and how to turn it off. ## What a record looks like [Section titled “What a record looks like”](#what-a-record-looks-like) ```json { "v": 1, "id": "386987e6-291f-4e8f-a08b-73db9d80897b", "ts": "2026-06-04T17:04:00.893Z", "session_id": "d59e0f0f-ed2f-456f-a2b6-be25a24b7ec7", "seq": 0, "action": "get", "project": "my-app", "profile": "production", "key": "DATABASE_URL", "provider": "keyring://", "outcome": "found", "reason": "deploy web frontend", "caller": { "name": "git", "version": "2.51.0", "operation": "credential_get", "resource": "github.com" }, "actor": { "user": "alice", "agent": "claude-code", "is_agent": true }, "version": "0.20.0" } ``` | Field | Meaning | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `v` | Schema version of the record | | `id` | Unique id for this event | | `ts` | RFC 3339 UTC timestamp | | `session_id` | Shared by every event from one `secretspec` invocation | | `seq` | Monotonic sequence within that invocation | | `action` | The operation: `get`, `set`, `check`, `run`, `import`, `export`, `cache_clear` / `cache_refresh` (0.17+), or `delete` (0.18+) | | `project` / `profile` | The project and profile in effect | | `scope` | The named scope for a scoped `check`, `run`, or `export`; omitted otherwise (SecretSpec 0.17+) | | `key` | The secret name for single-secret actions (`get`/`set`, and `delete` in 0.18+); never its value | | `keys` | The set of secret names for bulk actions (`check`/`run`/`import`/`export`) | | `command` | For `run`, the executed program (argv\[0] only — never its arguments, which may contain secrets) | | `provider` | The provider URI that served the access, with credentials redacted | | `outcome` | `found`, `missing`, `default`, `written`, `deleted` (0.17+ cache clear), `started` (a `run` launched its command), or `error` | | | A cached route writing its local entry is recorded as `cache_refresh`/`written`, never as `set`: no authoritative store was written. Dropping an entry — `cache clear`, or an entry a write superseded — is `cache_clear`/`deleted`. | | `error_kind` | A non-sensitive tag when `outcome` is `error` | | `reason` | The reason supplied via `--reason` / `SECRETSPEC_REASON` / the SDK, if any | | `caller` | Caller-asserted software integration context: `name`, and optional `version`, `operation`, and non-secret `resource` (SecretSpec 0.20+) | | `actor` | The OS user, the detected coding agent (if any), and whether this is an agent session | This pairs naturally with the [`require_reason`](/reference/configuration/#requiring-a-reason-for-secret-access) policy: when that policy applies, SecretSpec requires the caller to state *why* before proceeding and records the supplied reason alongside the access. Caller context answers *what software* requested access; `reason` answers *why the user* requested it. Caller context is informational, is not an authenticated identity, and never satisfies `require_reason`. Integrations must not place a credential or secret value in any caller field. ## Reading the log [Section titled “Reading the log”](#reading-the-log) The log is plain JSON Lines, so any tool works (`cat`, `tail -f`, `jq`). The [`secretspec audit`](/reference/cli/#audit) command reads it for you with filters and a readable summary: ```bash # Last 20 entries, formatted $ secretspec audit -n 20 # Only `run` events for one project $ secretspec audit --project my-app --action run # Raw JSON Lines, piped to jq $ secretspec audit --json | jq 'select(.outcome == "missing")' ``` ## Size cap [Section titled “Size cap”](#size-cap) The log is a single file capped at **1 MiB** by default. When it reaches the cap it is truncated and started fresh, so disk usage stays bounded without any log rotation to manage. This makes the log a size-bounded recent record rather than a complete, permanent history — it is not intended to satisfy long-term compliance retention on its own. Forward it to a central system if you need that. ## Reliability [Section titled “Reliability”](#reliability) Auditing never blocks secret access. If the log cannot be written (for example, a read-only filesystem), secretspec prints a `warning:` to stderr and continues — your `get`, `set`, and `run` still work. ## Configuration [Section titled “Configuration”](#configuration) Auditing is a per-machine concern, so it is configured in your **user-global config** (`~/.config/secretspec/config.toml`) under the top-level `[audit]` table — not in the project’s `secretspec.toml`. This means a repository you clone cannot turn off or redirect your audit log. See the [configuration reference](/reference/configuration/#audit-logging) for all options. To turn it off: \~/.config/secretspec/config.toml ```toml [audit] enabled = false ``` # Composed Secrets > Derive read-only values from other declared secrets with strict templates **New in version 0.16** Composed secrets derive one exported value from other secrets in the active profile. They are useful for connection strings, command arguments, and other formats whose components should remain independently stored: ```toml [profiles.default] DB_USER = { description = "Database user" } DB_PASSWORD = { description = "Database password" } DB_HOST = { description = "Database host" } DATABASE_URL = { description = "PostgreSQL connection string", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" } ``` `DB_USER`, `DB_PASSWORD`, and `DB_HOST` resolve through their ordinary providers. `DATABASE_URL` is then rendered in memory and exported alongside them. The composed result is never read from or written to a provider. ## Static dependency graph [Section titled “Static dependency graph”](#static-dependency-graph) Every `${UPPERCASE_NAME}` must name a secret declared in the effective profile. Reference names must match `[A-Z][A-Z0-9_]*`. SecretSpec validates the complete graph while loading `secretspec.toml`, before accessing a provider: * declaration order does not matter; * a composition may reference another composition; * unknown references are errors; * dependency cycles are errors. ```toml [profiles.default] USER = { description = "Database user" } PASSWORD = { description = "Database password" } HOST = { description = "Database host" } AUTHORITY = { description = "Database authority", composed = "${USER}:${PASSWORD}" } DATABASE_URL = { description = "Database URL", composed = "postgres://${AUTHORITY}@${HOST}/app" } ``` This differs deliberately from dotenv expansion, where behavior can depend on file order, process environment, and how a particular parser handles undefined or recursive variables. ## Template syntax [Section titled “Template syntax”](#template-syntax) Composition is a small, strict language: | Syntax | Meaning | | ------------------- | ------------------------------------------- | | `${UPPERCASE_NAME}` | Insert one declared secret’s exported value | | `$$` | Insert a literal `$` | For example, `$${EXTERNAL_NAME}` renders the literal text `${EXTERNAL_NAME}` without treating it as a SecretSpec reference. The following are intentionally unsupported: * lowercase or mixed-case references such as `${password}` or `${Password}`; * shell-style expressions such as `${NAME:-fallback}`; * ambient environment-variable lookup; * command substitution; * recursive expansion. Plain `{` and `}` are literal, so JSON objects, CSS blocks, and regular-expression quantifiers do not require brace escaping. Substitution is one pass. If `PASSWORD` contains the literal text `${HOST}`, inserting `${PASSWORD}` produces `${HOST}`; it is not scanned again. This keeps secret bytes opaque and prevents values from unexpectedly becoming executable template syntax. ## Missing, empty, and optional values [Section titled “Missing, empty, and optional values”](#missing-empty-and-optional-values) Missing and empty are different: * an empty dependency inserts an empty string; * a missing dependency makes a required composition missing; * when the composed secret sets `required = false`, a missing dependency omits the composed result instead. SecretSpec never silently replaces a missing reference with empty text. Interactive `secretspec check` prompts for the unresolved provider-backed dependencies, not for the derived result. ## Read-only behavior [Section titled “Read-only behavior”](#read-only-behavior) A composed secret cannot also declare `default`, `providers`, `ref`, `type`, or enabled `generate`. These fields would give the same name two competing value sources. * `get` resolves the target’s transitive dependencies and prints the result; * `set` rejects the composed name as read-only; * `import` skips composed names because there is no stored value to copy; * `check`, `run`, `export`, and SDK resolution include the composed value like any other resolved secret. ## Profiles and inheritance [Section titled “Profiles and inheritance”](#profiles-and-inheritance) References are checked against the effective profile after `default` profile inheritance. A profile may override the template while inheriting component declarations: ```toml [profiles.default] DB_USER = { description = "Database user" } DB_PASSWORD = { description = "Database password" } DB_HOST = { description = "Database host" } DATABASE_URL = { description = "Database URL", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" } [profiles.development] DATABASE_URL = { composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app_dev" } ``` Profile-level storage defaults do not apply to composed secrets, because their source is the dependency graph rather than a provider. Profile-level `required` defaults still apply. ## Paths and encoding [Section titled “Paths and encoding”](#paths-and-encoding) When a dependency uses `as_path = true`, its exported temporary-file path is inserted. Setting `as_path = true` on the composed secret instead writes the final rendered value to a temporary file. Composition performs raw string concatenation. It does not URL-encode or JSON-encode values: SecretSpec cannot infer whether a component is a username, password, host, path, query parameter, or structured value. Store each component in the representation required by the destination format. To export the resolved secret map as safely encoded JSON, use `secretspec export --format json`. See the [`composed` configuration reference](/reference/configuration/#composed-secrets) for the field-level constraints. # Declarative Configuration > Understanding secretspec.toml and its declarative approach SecretSpec uses `secretspec.toml` to declare what secrets your application needs, separating requirements from storage mechanisms for portability across environments. ## Basic Structure [Section titled “Basic Structure”](#basic-structure) ```toml [project] name = "my-app" revision = "1.0" extends = ["../shared/common"] # Optional: inherit from other configs [profiles.default] DATABASE_URL = { description = "PostgreSQL connection string", required = true } API_KEY = { description = "External API key", required = true } SESSION_SECRET = { description = "Session signing secret", required = true, type = "password", generate = true } ``` ## Secret Declarations [Section titled “Secret Declarations”](#secret-declarations) Each secret is declared with configuration options: ```toml SECRET_NAME = { description = "Human-readable explanation", # Required: shown in prompts required = true, # Optional: defaults to true default = "value" # Optional: fallback if not set } ``` **Options:** * `description`: Explains the secret’s purpose (required in the `default` profile; profile overrides inherit it when omitted) * `required`: Whether the secret must be provided (default: `true`) * `default`: Fallback value for optional secrets * `composed` (0.16+): Derive a read-only value from other declared secrets (see [Composed Secrets](/concepts/composed-secrets/) for the strict template and dependency semantics) * `type`: Secret type for auto-generation (`password`, `hex`, `base64`, `uuid`, `command`, `rsa_private_key`, `openpgp_private_key` (0.21+), and `ssh_private_key` (0.21+)) * `generate`: Enable auto-generation when the secret is missing (`true` or a table with options) * `prompt` (0.19+): Securely ask for a missing value during `secretspec run` and let the selected provider decide whether to save the answer ## Related Concepts [Section titled “Related Concepts”](#related-concepts) * [Configuration Inheritance](/concepts/inheritance/) lets projects share common secret definitions via the `extends` field * [Secret Generation](/concepts/generation/) auto-creates passwords, tokens, and keys when secrets are missing * [Run prompts (0.19+)](/reference/configuration/#prompt-on-missing-during-run-019) provision stored secrets on first use, or remain invocation-only with `null` * [Composed Secrets (0.16+)](/concepts/composed-secrets/) derive values from other declared secrets without dotenv or shell expansion ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Descriptive names**: Use `STRIPE_API_KEY` instead of generic `API_KEY` 2. **Clear descriptions**: Help developers understand each secret’s purpose 3. **Sensible defaults**: Provide development defaults, require production values 4. **Modular inheritance**: Create reusable base configurations for common patterns ## Complete Example [Section titled “Complete Example”](#complete-example) ```toml [project] name = "web-api" revision = "1.0" extends = ["../shared/base", "../shared/auth"] [profiles.default] # Inherits DATABASE_URL, INTERNAL_API_KEY from base # Inherits JWT_SECRET, SESSION_SECRET from auth # Service-specific additions: STRIPE_API_KEY = { description = "Stripe payment API", required = true } REDIS_URL = { description = "Redis cache connection", required = true } PORT = { description = "Server port", required = false, default = "3000" } ``` # Secret Generation > Automatically generating passwords, tokens, and keys for missing secrets **New in version 0.7** Secrets can be declared with `type` and `generate` to be auto-generated when missing. This is useful for passwords, tokens, and keys that do not need to be shared across developers. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```toml [profiles.default] DB_PASSWORD = { description = "Database password", type = "password", generate = true } API_TOKEN = { description = "API token", type = "hex", generate = { bytes = 32 } } SESSION_KEY = { description = "Session key", type = "base64", generate = { bytes = 64 } } REQUEST_ID = { description = "Request ID prefix", type = "uuid", generate = true } ``` ## Generation Types [Section titled “Generation Types”](#generation-types) | Type | Default Output | Options | | ----------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `password` | 32 alphanumeric chars | `length` (int), `charset` (`"alphanumeric"` or `"ascii"`) | | `hex` | 64 hex chars (32 bytes) | `bytes` (int) | | `base64` | 44 chars (32 bytes) | `bytes` (int) | | `uuid` | UUID v4 (36 chars) | none | | `command` | stdout of command | `command` (string, required) | | `rsa_private_key` | 2048-bit RSA private key (PKCS1 PEM) | `bits` (int) | | `openpgp_private_key` (0.21+) | ASCII-armored OpenPGP transferable secret key | `user_id` (required), `algorithm` (`"ed25519"` or `"rsa"`), `bits` (RSA only), `capabilities` (`["sign"]`, `["encrypt"]`, or both) | | `ssh_private_key` (0.21+) | Unencrypted OpenSSH Ed25519 private key | `algorithm` (`"ed25519"` or `"rsa"`), `bits` (RSA only), `comment` (string) | ### Command type [Section titled “Command type”](#command-type) The `command` type runs a shell command and uses its stdout as the generated value: ```toml MONGO_KEY = { description = "MongoDB keyfile", type = "command", generate = { command = "openssl rand -base64 765" } } ``` `command` requires `generate = { command = "..." }` rather than just `generate = true`. ### OpenPGP private keys [Section titled “OpenPGP private keys”](#openpgp-private-keys-021) **New in version 0.21** `openpgp_private_key` generates a GnuPG-compatible OpenPGP v4 key entirely in process; neither `gpg` nor another executable is required. Its modern default uses an Ed25519 certification-only primary key and puts routine operations on separate Ed25519 signing and Curve25519 encryption subkeys. The User ID is required. With no `capabilities`, SecretSpec creates both signing and encryption subkeys: ```toml [profiles.default] GENERAL_KEY = { description = "Service OpenPGP key", type = "openpgp_private_key", generate = { user_id = "Service Bot " } } # A signing-only key has no encryption subkey. RELEASE_KEY = { description = "Release signing key", type = "openpgp_private_key", generate = { user_id = "Release Bot ", capabilities = ["sign"] } } # RSA is available for consumers that require it; 3072 bits is the default. LEGACY_KEY = { description = "Legacy-compatible OpenPGP key", type = "openpgp_private_key", generate = { user_id = "Legacy Bot ", algorithm = "rsa", bits = 4096 } } ``` `capabilities` must be a non-empty list containing `"sign"`, `"encrypt"`, or both without duplicates. `algorithm` defaults to `"ed25519"`. Selecting `"rsa"` uses RSA for the primary key and every requested subkey; `bits` defaults to 3072 and accepts values from 2048 through 8192. `bits` is invalid with `"ed25519"`. The result is one `-----BEGIN PGP PRIVATE KEY BLOCK-----` value that can be imported by GnuPG and other OpenPGP tools. It has no OpenPGP passphrase and no expiration; protect it with an encrypted provider and rotate it according to the consuming system’s policy. Set `as_path = true` when a command needs a temporary key file rather than the armored value in an environment variable. ### SSH private keys [Section titled “SSH private keys”](#ssh-private-keys-021) **New in version 0.21** `ssh_private_key` generates an unencrypted OpenSSH private key entirely in process. `generate = true` uses Ed25519, the sensible default for new SSH keys: ```toml [profiles.default] DEPLOY_KEY = { description = "Deployment SSH key", type = "ssh_private_key", generate = true } # RSA is available for compatibility; 3072 bits is the default. LEGACY_DEPLOY_KEY = { description = "Legacy deployment key", type = "ssh_private_key", generate = { algorithm = "rsa", bits = 4096, comment = "deploy@example.com" } } ``` RSA sizes from 2048 through 8192 bits are accepted. `bits` is invalid with Ed25519. `comment` is optional and cannot contain control characters. Generated keys are not passphrase-encrypted, so store them in an encrypted provider. Set `as_path = true` when a command needs the key in a temporary file rather than in an environment variable. ## How it works [Section titled “How it works”](#how-it-works) * Generation only triggers when a secret is **missing**. Existing secrets are never overwritten. * Generated values are stored via the secret’s configured provider (or the default provider). * Subsequent runs find the stored value and skip generation (idempotent). * The `null` provider (0.19+) instead returns a fresh generated value for only the current resolution. * `generate` and `default` cannot both be set on the same secret. * Setting `type` without `generate` is informational only and does not trigger auto-generation. ## Ephemeral generation with null [Section titled “Ephemeral generation with null”](#ephemeral-generation-with-null-019) **New in version 0.19** Use `providers = ["null"]` when the value should be generated on demand and never written to provider storage: ```toml [profiles.default] SESSION_SECRET = { description = "Per-run session secret", type = "base64", generate = { bytes = 32 }, providers = ["null"] } ``` One materializing resolution receives one value. The next `run`, `get`, `check`, or SDK value-carrying resolution receives a new one. Value-free reports describe the value as generated without minting it. Use a writable provider instead when another process or later invocation must retrieve the same value. ## Example [Section titled “Example”](#example) ```toml [profiles.default] # Auto-generated on first run, reused after that DB_PASSWORD = { description = "Database password", type = "password", generate = true } # Custom length and character set ADMIN_PASSWORD = { description = "Admin password", type = "password", generate = { length = 64, charset = "ascii" } } # 64-byte key encoded as base64 ENCRYPTION_KEY = { description = "Encryption key", type = "base64", generate = { bytes = 64 } } # RSA private key (default 2048-bit) JWT_SIGNING_KEY = { description = "JWT signing key", type = "rsa_private_key", generate = true } # RSA private key with custom key size TLS_KEY = { description = "TLS private key", type = "rsa_private_key", generate = { bits = 4096 } } # OpenPGP signing key (requires SecretSpec 0.21+) RELEASE_KEY = { description = "Release signing key", type = "openpgp_private_key", generate = { user_id = "Release Bot ", capabilities = ["sign"] } } # OpenSSH Ed25519 private key (requires SecretSpec 0.21+) DEPLOY_KEY = { description = "Deployment SSH key", type = "ssh_private_key", generate = true } # Informational type only, no generation EXTERNAL_API_KEY = { description = "Provided by vendor", type = "password" } ``` See the [configuration reference](/reference/configuration/#secret-generation) for the full specification. # Configuration Inheritance > Sharing common secret definitions across projects with extends SecretSpec supports sharing common secrets across projects through the `extends` field in `[project]`. This avoids duplicating secret definitions in monorepos or multi-service setups. ## Basic Example [Section titled “Basic Example”](#basic-example) A shared base configuration: shared/common/secretspec.toml ```toml [project] name = "common" [profiles.default] DATABASE_URL = { description = "Main database", required = true } INTERNAL_API_KEY = { description = "Internal service API key", required = true } ``` A project that extends it: myapp/secretspec.toml ```toml [project] name = "myapp" extends = ["../shared/common"] [profiles.default] DATABASE_URL = { description = "MyApp database", required = true } # Override API_KEY = { description = "External API key", required = true } # Add new ``` ## Monorepo Structure [Section titled “Monorepo Structure”](#monorepo-structure) ```plaintext monorepo/ ├── shared/ │ ├── base/secretspec.toml # Common secrets │ └── database/secretspec.toml # DB-specific (extends base) └── services/ ├── api/secretspec.toml # API service (extends database) └── frontend/secretspec.toml # Frontend (extends base) ``` ## Multiple Inheritance [Section titled “Multiple Inheritance”](#multiple-inheritance) A project can extend multiple configurations. Later sources take precedence over earlier ones: ```toml [project] name = "api-service" extends = ["../../shared/base", "../../shared/database", "../../shared/auth"] ``` ## Rules [Section titled “Rules”](#rules) * Child definitions completely replace parent definitions for the same secret * Later sources in `extends` override earlier ones * Shared ancestors are applied once, so diamond-shaped inheritance is supported * Each profile is merged independently * Profile `[defaults]` inherit field by field across source files * The `inherit` profile-default field (0.19+) follows that same `extends` precedence. A child profile keeps an inherited `inherit = false` unless a later source explicitly sets it to `true`. * A child `[scopes.]` completely replaces the parent scope of the same name — its `secrets` list wins outright; the two lists are **not** unioned. Scopes defined only in a parent are inherited. (Whole-value replacement is the safe default for an allowlist: extending a config cannot silently widen a scope the parent narrowed.) Available from SecretSpec 0.17. * Paths are relative to the containing `secretspec.toml` file # Concepts Overview > How SecretSpec's core concepts work together SecretSpec is built around three core ideas that separate concerns and keep your secrets portable across environments. ## Declare what you need [Section titled “Declare what you need”](#declare-what-you-need) A [`secretspec.toml`](/concepts/declarative/) lists the abstract secrets your project depends on, with descriptions, defaults, and whether they are required. This file lives in version control so every developer and CI system sees the same requirements. ## Use profiles for environments [Section titled “Use profiles for environments”](#use-profiles-for-environments) [Profiles](/concepts/profiles/) let you vary secret requirements per environment. A `production` profile can enforce strict requirements while a `development` profile provides safe defaults. Non-default profiles inherit from `default` when it exists, so you only specify what changes; SecretSpec 0.19+ also supports standalone profiles that opt out of this inheritance. ## Store secrets anywhere with providers [Section titled “Store secrets anywhere with providers”](#store-secrets-anywhere-with-providers) [Providers](/concepts/providers/) are pluggable backends (keyring, dotenv, 1Password, Vault, etc.) that handle actual storage and retrieval. The same `secretspec.toml` works regardless of where secrets are stored, and you can swap providers without changing your project configuration. ## How they connect [Section titled “How they connect”](#how-they-connect) ```plaintext secretspec.toml Profile selected Provider resolves (what you need) --> (which requirements) --> (where to get values) ``` 1. You declare secrets in `secretspec.toml` 2. The active profile determines which secrets are required and what defaults apply 3. The provider retrieves (or stores) the actual values Each concern is independent: you can change your storage backend without touching profile definitions, or add a new environment without modifying provider configuration. ## Additional concepts [Section titled “Additional concepts”](#additional-concepts) * [Configuration Inheritance](/concepts/inheritance/) lets projects share common secret definitions via `extends` * [Scopes (0.17+)](/concepts/scopes/) let each service or task resolve only its declared subset of a profile * [Secret Generation](/concepts/generation/) auto-creates passwords, tokens, and keys when secrets are missing * [Composed Secrets (0.16+)](/concepts/composed-secrets/) derive read-only values from other declared secrets # Profiles > Managing environment-specific secret requirements with profiles ## What Are Profiles? [Section titled “What Are Profiles?”](#what-are-profiles) Profiles are named configurations that define how secrets behave in different environments. They specify which secrets are required vs optional, provide safe defaults for development, and enforce strict requirements for production. A key feature of profiles is inheritance: non-default profiles inherit secrets from the `default` profile when it exists. This means you only need to override the specific properties that change between related environments. SecretSpec 0.19+ also lets an unrelated profile opt out and remain standalone. If a manifest omits `default`, callers must select an existing profile with `--profile`, `SECRETSPEC_PROFILE`, or their user config; the final fallback name is still `default`. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) Define profiles in your `secretspec.toml`: ```toml [profiles.default] DATABASE_URL = { description = "PostgreSQL connection", required = true } API_KEY = { description = "External API key", required = true } [profiles.development] # Inherits DATABASE_URL and API_KEY from default, only overriding their requirements DATABASE_URL = { required = false, default = "postgresql://localhost:5432/myapp_dev" } API_KEY = { required = false, default = "dev-key-12345" } DEBUG = { description = "Enable debug mode", required = false, default = "true" } [profiles.production] # Inherits all secrets from default profile # Only need to add production-specific secrets SENTRY_DSN = { description = "Error tracking", required = true } ``` ## Selecting Profiles [Section titled “Selecting Profiles”](#selecting-profiles) SecretSpec resolves the active profile in this order: 1. **Command line**: `--profile production` (highest priority) 2. **Environment variable**: `SECRETSPEC_PROFILE=staging` 3. **User config**: Default profile in `~/.config/secretspec/config.toml` 4. **Fallback**: `default` profile ```bash # Use specific profile $ secretspec check --profile development ✓ DATABASE_URL - PostgreSQL connection (using default) ✓ API_KEY - External API key (using default) # Set via environment $ export SECRETSPEC_PROFILE=production $ secretspec run -- npm start ``` ## Profile Inheritance in Detail [Section titled “Profile Inheritance in Detail”](#profile-inheritance-in-detail) When using profiles, inheritance works as follows: 1. **Base definition in default**: Define all your secrets with their descriptions and base requirements in the `default` profile 2. **Override only what changes**: Other profiles only need to specify the properties that differ from default 3. **Field-level overrides**: Most explicitly set properties replace the corresponding property from `default`, while omitted properties continue to inherit 4. **Profile-specific secrets**: Secrets not in the default profile can be added to any profile ### Standalone profiles [Section titled “Standalone profiles”](#standalone-profiles-019) **New in version 0.19** Set `inherit = false` in a non-default profile’s `defaults` table when its secret set is unrelated to `[profiles.default]`: ```toml [profiles.default] DATABASE_URL = { description = "Development database", default = "sqlite://./dev.db" } API_KEY = { description = "Development API key" } [profiles.production] # Inherits both default declarations and overrides only what changes. DATABASE_URL = { required = true } [profiles.deployment.defaults] inherit = false # SecretSpec 0.19+ [profiles.deployment] # Does not inherit DATABASE_URL, API_KEY, or any of their fields. DEPLOY_TOKEN = { description = "Deployment credential", required = true } ``` The setting disables both automatic inclusion of default secrets and field-by-field inheritance for secrets explicitly redeclared in the standalone profile. Omitting it preserves the existing inheritance behavior. A standalone profile must declare at least one secret. ### Switching reference models [Section titled “Switching reference models”](#switching-reference-models-019) **New in version 0.19** Legacy `ref` and provider-scoped `refs` are alternative forms of one inherited address-model setting. Declaring either one in a profile replaces both forms from `[profiles.default]`; omitting both continues to inherit the default profile’s form. This lets a profile switch from one route-wide address to provider-specific addresses without retaining an invalid mixture of both: ```toml [providers] legacy = "onepassword://Legacy" production = "onepassword://Production" [profiles.default] API_KEY = { description = "API key", providers = ["legacy"], ref = { item = "shared-api", field = "token" } } [profiles.production] # Inherits the description, but replaces providers and the complete ref/refs choice. API_KEY = { providers = ["production"], refs = { production = { item = "production-api", field = "credential" } } } ``` The reverse switch also works: a profile’s `ref` replaces inherited `refs`. Within one effective secret, `ref` and `refs` remain mutually exclusive. See [Secret References](/concepts/references/#different-coordinates-per-provider-019) for how each model addresses providers. ## Profiles, Scopes, Providers, and Extends [Section titled “Profiles, Scopes, Providers, and Extends”](#profiles-scopes-providers-and-extends) These features solve different dimensions of a configuration: * A **profile** chooses an environment or context. It controls requiredness, defaults, provider routes, references, and the `{profile}` storage namespace. * A **scope** selects which secrets one service or task receives from the effective profile. It does not create another environment. * A secret’s **providers** choose where its value is read and written. Provider chains are also the least-privilege boundary: a process only needs access to the stores used by the secrets in its scope. * **`extends`** merges separate `secretspec.toml` files. Use it to share manifests across projects, not to express relationships among several profiles in one small manifest. For an application with `development` and `production` environments plus `app`, `public`, and `deploy` consumers, profiles normally model the two environments, scopes model the three consumers, and per-secret provider chains route each value to the appropriate store. ## Profile-Level Defaults [Section titled “Profile-Level Defaults”](#profile-level-defaults) To reduce repetition when multiple secrets in a profile share the same settings, use the `profiles..defaults` section: ```toml [providers] prod_vault = "onepassword://Production" keyring = "keyring://" [profiles.production.defaults] providers = ["prod_vault", "keyring"] required = true [profiles.production] DATABASE_URL = { description = "Production DB" } API_KEY = { description = "API Key" } SENTRY_DSN = { description = "Error tracking" } ``` Profile defaults apply to all secrets in that profile unless explicitly overridden. In SecretSpec 0.19+, the same table accepts `inherit = false` to make a non-default profile standalone; unlike `required`, `default`, and `providers`, this controls the relationship with `[profiles.default]` rather than supplying a value to each secret. The precedence order is: 1. **Secret-level configuration** (highest priority) — explicit settings in the secret definition 2. **Profile inheritance** — inherited from the default profile when the active profile omits a field 3. **Profile defaults** — from `profiles..defaults` 4. **Project provider defaults** — from `[defaults].providers` in 0.21+ 5. **Global defaults** (lowest priority) — from CLI, environment, or global config This is particularly useful for setting common [provider fallback routes](/concepts/providers/fallback/#ordered-fallback-routes), requirements, or defaults across all secrets in a profile. ## Practical Example [Section titled “Practical Example”](#practical-example) A web application with different requirements per environment: ```toml [project] name = "web-app" revision = "1.0" [profiles.default] DATABASE_URL = { description = "PostgreSQL connection", required = true } REDIS_URL = { description = "Redis for caching", required = true } JWT_SECRET = { description = "JWT signing key", required = true } [profiles.development] # Inherits all secrets from default, just adding defaults DATABASE_URL = { default = "postgresql://localhost:5432/webapp_dev" } REDIS_URL = { default = "redis://localhost:6379/0" } JWT_SECRET = { default = "dev-secret-change-in-prod" } HOT_RELOAD = { description = "Enable hot reload", required = false, default = "true" } [profiles.production] # Inherits DATABASE_URL, REDIS_URL, JWT_SECRET from default # Only adds production-specific secrets SENTRY_DSN = { description = "Error tracking", required = true } SSL_CERT = { description = "SSL certificate path", required = true } ``` # Providers > Choose and configure the storage backends SecretSpec uses for secrets A provider is a storage backend from which SecretSpec reads secrets and, when supported, writes them. Providers let one `secretspec.toml` describe the secrets an application needs without requiring every environment to use the same secret store. For example, a developer can use the system keyring, CI can supply environment variables, and production can use a shared password manager or cloud secret manager. The secret definitions stay the same; only their provider configuration changes. ## Provider specifications [Section titled “Provider specifications”](#provider-specifications) Anywhere SecretSpec accepts a provider, you can use one of three forms: * A provider name, such as `keyring` or `env`. * A provider URI, such as `dotenv://.env.local` or `onepassword://Production`. The URI configures a particular instance of the provider. * A provider alias, such as `prod_vault`, defined in project or user configuration. Aliases are useful when a URI is shared by several secrets or should have a meaningful, store-independent name. secretspec.toml ```toml [providers] prod_vault = "onepassword://Production" [profiles.production] DATABASE_URL = { description = "Production database", providers = ["prod_vault"] } ``` ## Available providers [Section titled “Available providers”](#available-providers) | Provider | Storage backend | Read | Write | Encrypted at rest | TPM-backed keys | | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [keyring](/providers/keyring/) | [macOS Keychain](https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web), [Windows Credential Manager](https://learn.microsoft.com/windows/win32/secauthn/credentials-management), or [Linux Secret Service](https://gnome.pages.gitlab.gnome.org/libsecret/) | ✓ | ✓ | ✓ | — | | [kdbx](/providers/kdbx/) (0.17+) | KeePass KDBX file (requires the `kdbx` build feature) | ✓ | KDBX 4 | ✓ | — | | [dotenv](/providers/dotenv/) | A `.env` file | ✓ | ✓ | ✗ | — | | [file](/providers/file/) (0.19+) | One plaintext UTF-8 file per secret | ✓ | ✓ | ✗ | — | | [env](/providers/env/) | Current process environment | ✓ | ✗ | ✗ | — | | [ejson](/providers/ejson/) (0.20+) | EJSON encrypted file (requires the `ejson` build feature and EJSON CLI) | ✓ | ✗ | ✓ | — | | [null](/providers/null/) (0.19+) | No storage; uses a manifest default, ephemeral generation, or an ephemeral run prompt | ✗ | ✗ | N/A | — | | [systemd-credential](/providers/systemd-credential/) (0.17+) | Credentials passed to the current systemd service | ✓ | ✗ | Depends on the unit’s credential source | [Via systemd-creds](https://www.freedesktop.org/software/systemd/man/latest/systemd-creds.html) | | [fly](/providers/fly/) (0.20+) | Fly.io application secrets through `flyctl` | ✗ | ✓ | ✓ | — | | [cloudflare](/providers/cloudflare/) (0.20+) | Cloudflare account-level Secrets Store through its REST API | ✗ | ✓ | ✓ | — | | [pass](/providers/pass/) | Unix `pass` password store | ✓ | ✓ | ✓ | [Via GnuPG](https://gnupg.org/blog/20210315-using-tpm-with-gnupg-2.3.html) | | [gopass](/providers/gopass/) (0.15+) | `gopass` password store (git-synced, GPG-encrypted) | ✓ | ✓ | ✓ | [Via GnuPG](https://gnupg.org/blog/20210315-using-tpm-with-gnupg-2.3.html) | | [protonpass](/providers/protonpass/) | Proton Pass | ✓ | ✓ | ✓ | — | | [passbolt](/providers/passbolt/) (0.19+) | Self-hosted Passbolt through `go-passbolt-cli` | ✓ | ✓ | ✓ | — | | [onepassword](/providers/onepassword/) | 1Password | ✓ | ✓ | ✓ | — | | [lastpass](/providers/lastpass/) | LastPass | ✓ | ✓ | ✓ | — | | [dashlane](/providers/dashlane/) (0.18+) | Dashlane, through the `dcli` CLI | ✓ | ✗ | ✓ | — | | [keeper](/providers/keeper/) (0.18+) | Keeper Secrets Manager (requires the `keeper` build feature) | ✓ | ✓ | ✓ | — | | [gcsm](/providers/gcsm/) | Google Cloud Secret Manager (requires the `gcsm` build feature) | ✓ | ✓ | ✓ | — | | [awssm](/providers/awssm/) | AWS Secrets Manager (requires the `awssm` build feature) | ✓ | ✓ | ✓ | — | | [awsps](/providers/awsps/) (0.18+) | AWS Systems Manager Parameter Store (requires the `awsps` build feature in 0.18+) | ✓ | ✓ | ✓ (`SecureString`) | — | | [scaleway](/providers/scaleway/) (0.17+) | Scaleway Secret Manager (requires the `scaleway` build feature) | ✓ | ✓ | ✓ | — | | [vault](/providers/vault/) | HashiCorp Vault (requires the `vault` build feature) | ✓ | ✓ | ✓ | — | | [openbao](/providers/openbao/) (0.17+) | OpenBao (requires the `openbao` build feature; 0.16 uses `openbao://` through `vault`) | ✓ | ✓ | ✓ | — | | [bw](/providers/bw/) (0.18+) | Bitwarden Password Manager via the `bw` CLI (requires the `bw` build feature) | ✓ | ✓ | ✓ | — | | [bws](/providers/bws/) | Bitwarden Secrets Manager (official `bws` CLI in SecretSpec 0.17+; requires the `bws` build feature) | ✓ | ✓ | ✓ | — | | [akv](/providers/akv/) | Azure Key Vault (requires the `akv` build feature) | ✓ | ✓ | ✓ | — | | [aac](/providers/aac/) (0.20+) | Azure App Configuration, including Key Vault-reference resolution (included by default; `aac` feature for custom builds) | ✓ | ✓ | ✓ | — | | [infisical](/providers/infisical/) (0.16+) | Infisical (requires the `infisical` build feature) | ✓ | ✓ | ✓ | — | | [age](/providers/age/) (0.17+) | An age-encrypted file (requires the `age` build feature) | ✓ | ✓ | ✓ | — | | [sops](/providers/sops/) (0.17+) | SOPS-encrypted files (requires the `sops` build feature and SOPS CLI) | ✓ | ✓ | ✓ | Depends on the configured SOPS key service | | [kubernetes](/providers/kubernetes/) (0.20+) | Kubernetes ConfigMaps and Secrets (requires the `kubernetes` build feature) | ✓ | ✓ | [Secrets can be encrypted at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#ensure-all-secrets-are-encrypted) | — | “TPM-backed keys” means the local key used by the provider can be protected by a [TPM 2.0](https://trustedcomputinggroup.org/resource/tpm-library-specification/) through the provider path SecretSpec uses. Pass and Gopass inherit this capability from GnuPG when its encryption key is moved to the TPM. systemd credentials inherit it from [systemd-creds](https://www.freedesktop.org/software/systemd/man/latest/systemd-creds.html), which seals encrypted credentials to the TPM2 by default when the host has one. [libsecret has an optional TPM2-enabled file backend](https://gnome.pages.gitlab.gnome.org/libsecret/libsecret-tpm2.html), but SecretSpec’s Linux keyring transport uses the Secret Service D-Bus API rather than that file backend. macOS Keychain uses Apple’s Secure Enclave rather than a TPM, and [Windows Vault credentials are not protected by Credential Guard](https://learn.microsoft.com/windows/security/identity-protection/credential-guard/how-it-works). An em dash means SecretSpec has no documented TPM integration for that provider; it does not describe other hardware security used internally by the provider service. Each provider page starts with a minimal working example, then covers setup, project configuration, storage conventions, existing provider-native secrets, and CI/CD where applicable. ## Configure the default provider [Section titled “Configure the default provider”](#configure-the-default-provider) Run the interactive configuration command to select the user-global provider SecretSpec uses when a secret has no provider-specific configuration. SecretSpec 0.17+ provides an explicit `global` namespace; the legacy spelling without it remains supported: ```bash $ secretspec config global init # 0.17+ ``` SecretSpec 0.17+ can persist the provider and profile non-interactively: ```bash $ secretspec config global init --provider env --profile default ``` The resulting user configuration contains a default provider: \~/.config/secretspec/config.toml ```toml [defaults] provider = "keyring" profile = "development" # Optional default profile ``` Use `--provider` for a one-off override, or `SECRETSPEC_PROVIDER` for commands in the current shell or CI job: ```bash # Route every secret in this command to a project .env file. $ secretspec run --provider dotenv -- npm start # Route every secret in subsequent commands to existing environment variables. $ export SECRETSPEC_PROVIDER=env $ secretspec check ``` `SECRETSPEC_PROVIDER` is a whole-resolution override: it replaces every per-secret fallback chain. Integrations such as devenv should only export it when the user explicitly configures a whole-resolution provider override. When consuming a JSON or SDK resolution, do not feed its top-level `provider` display label back into `SECRETSPEC_PROVIDER`; mixed per-secret routes cannot be represented by one provider string. Only export `SECRETSPEC_PROVIDER` when the user explicitly selected a whole-resolution override. Per-secret `source_provider` remains the authoritative provenance. A provider URI can configure the selected backend more precisely: ```bash # Select a specific 1Password vault. $ secretspec run --provider "onepassword://Development" -- npm start # Select a specific dotenv file. $ secretspec run --provider "dotenv:/home/user/work/.env" -- npm test ``` ## Configure a project default provider chain (0.21+) [Section titled “Configure a project default provider chain (0.21+)”](#configure-a-project-default-provider-chain-021) **New in version 0.21** Use project `[defaults].providers` when every profile should use the same provider chain unless a profile or secret overrides it. The chain can name a user-global alias, allowing each developer to supply personal provider-native coordinates while the committed manifest stays the same: secretspec.toml ```toml [defaults] providers = ["developer"] [profiles.staging] DATABASE_PASSWORD = { description = "Personal staging database password" } ``` \~/.config/secretspec/config.toml ```toml [defaults.providers.developer] uri = "awsps://developer@us-east-1" ref = { item = "/{project}/{profile}/developers/alice/{key}" } ``` The alias’s existing `{project}`, `{profile}`, and `{key}` placeholders expand in the project that selects it. Only `alice` is user-specific. Do not also declare `developer` in the project’s `[providers]` table, because a project alias with the same name takes precedence over the user-global alias. ## Configure provider aliases [Section titled “Configure provider aliases”](#configure-provider-aliases) Provider aliases can be declared at either project or user scope: * Define project aliases in the top-level `[providers]` table in `secretspec.toml`. Commit these aliases so team members and CI use the same mapping. * Define user aliases in `[defaults.providers]` in `~/.config/secretspec/config.toml`. Use these for personal mappings that should apply across projects. If both scopes define the same alias, the project alias takes precedence. secretspec.toml ```toml [providers] prod_vault = "onepassword://Production" shared_vault = "onepassword://Shared" local = "keyring://" [profiles.production] DATABASE_URL = { description = "Production database", providers = ["prod_vault", "local"] } SENTRY_DSN = { description = "Error reporting", providers = ["shared_vault", "local"] } ``` Provider lists may combine aliases, provider names, and inline provider URIs: secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "Production database", providers = ["onepassword://Production", "keyring"] } ``` ### Alias ref templates [Section titled “Alias ref templates”](#alias-ref-templates-019) **New in version 0.19** A leaf alias can map the logical `{project}`, `{profile}`, and `{key}` into its provider’s native coordinates. This lets every link in a fallback chain—and each side of an import—use a different address: secretspec.toml ```toml [providers] remote = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } local = { uri = "dotenv://.env", ref = { item = "{key}" } } [profiles.production] API_KEY = { description = "API key", providers = ["remote", "local"] } ``` Use per-secret `refs = { alias = { item = "..." } }` for exceptions. See [Secret References](/concepts/references/#different-coordinates-per-provider-019) for precedence, import-only source aliases, and cached-route restrictions. Use the CLI to manage user-level aliases: ```bash # SecretSpec 0.17+ $ secretspec config global provider add prod_vault "onepassword://Production" $ secretspec config global provider list $ secretspec config global provider remove prod_vault ``` These commands modify only `~/.config/secretspec/config.toml`. Edit the top-level `[providers]` table directly to change project aliases. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) **New in version 0.15** Some providers need credentials before they can retrieve secrets. Examples include an access token for Bitwarden Secrets Manager, a Vault token or AppRole credentials, a 1Password service account token, and Azure service-principal credentials. An alias can load these credentials from another provider. This avoids storing long-lived provider credentials in a shell profile or CI variable when a secure store is available. The [provider credential reference](/reference/provider-credentials/) lists every accepted semantic name, its environment fallbacks, and the SecretSpec version that introduced it. ### Use the convention address [Section titled “Use the convention address”](#use-the-convention-address) In an alias’s `credentials` table, map each semantic credential name to the provider that stores it: secretspec.toml ```toml [providers] keyring = "keyring://" # Read the access token from keyring before connecting to Bitwarden. bws = { uri = "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c", credentials = { access_token = "keyring" } } ``` A string value such as `"keyring"` is a provider specification. SecretSpec reads the credential from that provider at the conventional `{project}/{profile}/{credential}` address for the active project and profile. ### Use an explicit address [Section titled “Use an explicit address”](#use-an-explicit-address) Use a table with `provider` and `ref` when the credential already exists at a specific provider-native address: secretspec.toml ```toml [providers.vault_prod] uri = "vault://secret/myapp?auth=approle" credentials = { role_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "role_id" } }, secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "secret_id" } } } ``` The `ref` table uses the same provider-native coordinates as a secret [`ref`](/reference/configuration/#secret-references). ### Store provider credentials [Section titled “Store provider credentials”](#store-provider-credentials) Use `config provider login` to prompt for every credential declared by an alias and write it to the configured source: ```bash $ secretspec config provider login bws Enter access_token for provider 'bws' (source: keyring): **** ✓ stored access_token in keyring at smoke/default/access_token ``` You can also create a user-level alias with a convention-address credential source from the CLI: ```bash $ secretspec config global provider add bws "bws://project-uuid" --credential access_token=keyring # 0.17+ $ secretspec config provider login bws ``` Provider credentials follow these rules: * **Configured credentials are authoritative.** When an alias declares a credential, SecretSpec reads its configured source. Providers may still use their conventional environment variables when no explicit credential is supplied. * **Credentials remain internal.** SecretSpec passes a retrieved credential to the destination provider in memory. It does not export the credential or include it in the environment of a process started by `secretspec run`. * **Credential chains are one hop.** A source provider cannot require provider credentials of its own. SecretSpec validates this before accessing the provider, preventing dependency cycles. * **Convention addresses are profile-specific.** A string source uses the active project and profile. Use a `ref` source when multiple projects or profiles should share one provider credential. * **Names are provider-specific.** The catalog above is exhaustive. Unsupported names are rejected before any source is read. * **A URI may not carry a credential (0.19+).** A provider URI with a password (`scheme://user:PASSWORD@host`) is rejected, as is a service account token in the `onepassword+token://` userinfo. A URI is committed to `secretspec.toml`, echoed into shell history, and printed by CI, so a credential written there is already disclosed. Use a provider credential or the provider’s environment variable instead. ## Next steps [Section titled “Next steps”](#next-steps) * Learn how [Provider fallback](/concepts/providers/fallback/) selects and orders sources. * Cache slow remote routes and diagnose remaining latency with [Provider caching](/concepts/providers/caching/) (0.17+). * Review the URI and authentication details for an individual provider in the [Providers](/providers/keyring/) section. * Learn how [Profiles](/concepts/profiles/) apply provider defaults to an environment. * Learn how [Secret references](/concepts/references/) separate provider selection from provider-native addresses. # Provider caching > Cache slow provider routes in a local secret store **New in version 0.17** ## The problem [Section titled “The problem”](#the-problem) A remote secret read can include authentication, external-process startup, DNS, TCP and TLS setup, and one or more network requests. Those fixed costs can dominate a command that resolves only a few secrets. Separate SecretSpec CLI invocations may pay them again even when the values rarely change. Latency can also scale with the number of distinct secret addresses. SecretSpec groups compatible reads and providers can batch or parallelize them, but the remote service, proxy, or provider CLI still determines the cost of each read. ## Cache one provider [Section titled “Cache one provider”](#cache-one-provider-019) **Changed in version 0.19** Use a cached fallback alias on SecretSpec 0.17 and 0.18. Provider caching places a faster local secret store in front of an authoritative provider. A fresh cache entry returns without constructing or contacting the remote provider; a miss reads the remote value and stores it locally for later SecretSpec invocations. Add `cache` to the remote provider alias and select that same alias normally: secretspec.toml ```toml [providers] local = "keyring://secretspec/cache/{project}/{profile}/{key}" azure = { uri = "akv://team-vault", credentials = { client_secret = "keyring" }, cache = { provider = "local", max_age = "8h" } } [profiles.development.defaults] providers = ["azure"] ``` The alias remains the authoritative provider, so its [provider credentials](/concepts/providers/#provider-credentials) stay next to `uri` and `cache`. ## Cache a fallback route [Section titled “Cache a fallback route”](#cache-a-fallback-route-017) **New in version 0.17** When more than one provider can authoritatively answer, use a route alias. `fallback` lists its providers in read order and `cache.provider` selects the local leaf provider: secretspec.toml ```toml [providers] azure = "akv://team-vault?auth=cli" env = "env://" local = "keyring://secretspec/cache/{project}/{profile}/{key}" remote = { fallback = ["azure", "env"], cache = { provider = "local", max_age = "8h" } } [profiles.development.defaults] providers = ["remote"] ``` ## Read behavior [Section titled “Read behavior”](#read-behavior) SecretSpec reads a cached route in this order: 1. returns a fresh cache entry without constructing or contacting an authoritative provider; 2. on a miss, unusable entry, or cache error, reads the provider URI or tries each `fallback` entry in order; 3. caches the value returned by the authoritative provider that answers. Cache failures produce warnings but never block the authoritative route. SecretSpec never returns an expired value when all fallbacks fail. It deletes expired, malformed, and route-mismatched entries when found so stale copies do not remain indefinitely in stores without native expiry. Value-free resolutions such as `check --json`, `check --explain`, and SDK `no_values` requests may read or discard an existing entry, but never populate or refresh one. ## Writes [Section titled “Writes”](#writes) Writes and generated values go to the provider URI or the first fallback, then refresh the cache. If the refresh fails, the authoritative write still succeeds and SecretSpec deletes the old cache entry. If deletion also fails, the warning identifies the `cache clear` command to run. For a cached fallback route, select a leaf provider to bypass the cache for one command: ```bash $ secretspec check --provider azure ``` In the fallback example, a direct write such as `secretspec set API_KEY --provider azure` invalidates the corresponding cache entry. ## Freshness and invalidation [Section titled “Freshness and invalidation”](#freshness-and-invalidation) `max_age` requires a unit: `s`, `m`, `h`, `d`, or `w`; compound durations such as `1h30m` are accepted. Entries use SecretSpec’s logical `{project}/{profile}/{secret}` address, even when the authoritative secret has a provider-native `ref`. Each entry contains the value, absolute expiration time, originating `max_age`, format version, and a fingerprint of the fallback route and secret reference. Changing the route, reference, or `max_age` invalidates it. The cache must use a distinct store from every authoritative provider; otherwise, a refresh could overwrite the authoritative secret. SecretSpec rejects such routes during planning. The examples use a separate keyring namespace for `local`. The cache provider must also support deletion: keyring, pass, gopass, dotenv, age (0.20+), Azure App Configuration (0.20+), or a Vault/OpenBao KV v2 mount. Other providers are rejected during planning. An Azure App Configuration cache must select a different storage identity and address space from every authoritative entry; a separate App Configuration resource is not required. Clear one entry or every cached entry in the active profile: ```bash $ secretspec cache clear API_KEY # SecretSpec 0.17+ $ secretspec cache clear --profile production ``` ## Store-side expiry [Section titled “Store-side expiry”](#store-side-expiry) SecretSpec requests native expiry where supported, using `max_age`. [Vault](/providers/vault/#provider-caching-017) and [OpenBao](/providers/openbao/#provider-caching) set KV v2 `delete_version_after` metadata. This removes the copy on time even if SecretSpec never runs again. The entry’s absolute expiration time remains the source of truth for freshness on every store; a read at or after that time deletes the entry. Machines sharing a cache should keep their system clocks synchronized. If native expiry cannot be configured, for example because a Vault token lacks metadata access or the mount uses KV v1, SecretSpec refuses the cache write and uses the authoritative route. ## Ownership [Section titled “Ownership”](#ownership) Each cache entry records a marker, project, and profile. Because addresses can collide in flat stores such as dotenv, SecretSpec changes only entries whose ownership it can verify. Unmarked entries and unexpired entries owned by another project or profile are bypassed, not overwritten or deleted. [`cache clear`](/reference/cli/#cache-clear-017) reports them. Any expired SecretSpec entry can be deleted by the project that encounters it because its stored lifetime has ended. A marked but unreadable entry, such as a partial write, can be identified as SecretSpec’s and replaced. If clearing reports a foreign entry, two configurations are addressing the same place. Give each project a separate store or path, such as the `{project}/{profile}/{key}` path used by `local` above. ## Where cached aliases can be used [Section titled “Where cached aliases can be used”](#where-cached-aliases-can-be-used) An inline cached provider alias or cached fallback alias works anywhere a complete route is selected: * a secret or profile-default `providers` list; * a project `[defaults].providers` list (0.21+); * the user-global default provider; * `SECRETSPEC_PROVIDER`; * `--provider`. Because a cached alias defines a complete route, it must be the only entry in a `providers` list. Fallback entries and the cache provider may be aliases, provider names, or URIs, but must resolve to leaf providers; cached aliases cannot be nested. An inline cached alias can declare credentials next to its `uri`. For a cached fallback route, credentials belong on its leaf aliases rather than on the route alias. ## Security [Section titled “Security”](#security) The cache contains the secret value, not just metadata. Use an encrypted provider such as keyring, pass, gopass, or age (0.20+) when values must be encrypted at rest. Dotenv stores entries as plaintext. Native expiry limits how long a copy exists without another SecretSpec run. ## Reference [Section titled “Reference”](#reference) * See [`cache clear`](/reference/cli/#cache-clear-017) in the CLI reference. * Review the [inline cache fields](/reference/configuration/#secretspec-019-inline-provider-cache) (0.19+) or [cached fallback fields](/reference/configuration/#secretspec-017-cached-fallback-alias-values) in the configuration reference. * Review [Provider fallback](/concepts/providers/fallback/) for authoritative route and write semantics. ## Diagnose and improve provider performance [Section titled “Diagnose and improve provider performance”](#diagnose-and-improve-provider-performance) Use these checks when the first cache fill is too slow, the route cannot be cached, or you need to understand a platform-specific difference. ### Establish a baseline [Section titled “Establish a baseline”](#establish-a-baseline) Benchmark the same command, profile, and set of secrets each time. `check` resolves values without printing them: ```bash $ time secretspec get SECRET_NAME >/dev/null $ time secretspec check --no-prompt ``` The redirected `get` prevents the value from appearing in the terminal. Compare it with the complete profile: similar times suggest a fixed authentication or connection cost, while time that grows with the number of secrets points to per-secret round trips, provider grouping, or concurrency. If you have [hyperfine](https://github.com/sharkdp/hyperfine), compare repeated runs: ```bash $ hyperfine --warmup 1 'secretspec check --no-prompt' ``` Record the first run separately. A warmup can hide cold authentication or an external CLI’s startup cost, while repeated one-shot SecretSpec commands still pay that cost in normal use. ### Isolate authentication and network cost [Section titled “Isolate authentication and network cost”](#isolate-authentication-and-network-cost) When a provider uses an external CLI, time a harmless authentication command without printing its token. For Azure CLI, for example: ```bash $ time az account get-access-token \ --scope https://vault.azure.net/.default \ --output none ``` If this takes most of a cache miss, consider another supported authentication mode. External CLI sessions are convenient for local development, but direct service-principal, managed-identity, workload-identity, token, or SDK authentication can avoid the process boundary where appropriate. For example, the [Azure Key Vault provider](/providers/akv/#authentication) supports Azure CLI sessions as well as service principals, managed identity, and workload identity. Prefer the identity mode that matches the environment; do not replace short-lived or workload-bound credentials with long-lived credentials solely to reduce latency. Azure App Configuration (0.20+) supports the same Entra identity modes plus connection strings. When entries resolve Key Vault references, benchmark both the App Configuration request and the separate Key Vault request; a warm cache avoids both remote reads. To distinguish connection setup from the secret API itself, probe the remote endpoint without requesting a real secret. This Azure Key Vault example is expected to return an authorization error, but still reports DNS, TCP, TLS, and time to first byte: ```bash $ curl --silent --show-error --output /dev/null \ --write-out 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \ 'https://VAULT.vault.azure.net/secrets/__probe__?api-version=7.5' ``` Run the same probe from the host and from WSL or a container to expose a platform-specific DNS, VPN, proxy, or TLS delay. ### Keep related secrets on one route [Section titled “Keep related secrets on one route”](#keep-related-secrets-on-one-route) Secrets that use the same store and authentication configuration should select the same provider alias. SecretSpec groups those reads into one provider operation, deduplicates identical references, and lets providers use a bulk API or bounded parallel reads where supported. Do not merge aliases that intentionally use different identities, endpoints, namespaces, or other security settings. Those are distinct routes even if they use the same provider type. ### Tune per-address concurrency [Section titled “Tune per-address concurrency”](#tune-per-address-concurrency-017) **New in version 0.17** Providers using SecretSpec’s default per-address fetch path read up to eight unique addresses concurrently. Test a few caps when latency grows with the number of secrets: ```bash $ SECRETSPEC_PROVIDER_CONCURRENCY=1 secretspec check --no-prompt $ SECRETSPEC_PROVIDER_CONCURRENCY=4 secretspec check --no-prompt $ SECRETSPEC_PROVIDER_CONCURRENCY=16 secretspec check --no-prompt ``` More concurrency is not always faster. It can increase rate limiting, overload a reverse proxy, or create more simultaneous connections. The setting does not remove a provider’s cold authentication floor, and providers with a true bulk API may not use it for their batched reads. ### Check WSL and container overhead [Section titled “Check WSL and container overhead”](#check-wsl-and-container-overhead) If the same manifest is slower under WSL or in a container: * use the Linux build of each provider CLI instead of invoking a Windows executable through interoperability; * keep the project and provider CLI’s configuration and credential cache on the Linux filesystem rather than under `/mnt/c`; * compare the endpoint probe with and without `curl -4` to identify an address-family or VPN routing delay before changing system-wide networking; * check whether a proxy, VPN, antivirus product, or certificate helper is only active on one side of the host boundary; * remember that short-lived containers repeat process startup, authentication, DNS, and TLS setup on every invocation. ### Interpret the results [Section titled “Interpret the results”](#interpret-the-results) | Observation | Likely cost | First thing to try | | -------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | One secret and the full profile take about the same time | Authentication or process startup | Use direct authentication where appropriate, or cache the route | | Every SecretSpec invocation has the same fixed delay | External CLI or cold connection setup | Time the auth command and endpoint probe separately | | Time grows with the number of secrets | Per-secret network reads | Consolidate equivalent routes and benchmark concurrency | | A warm run is much faster than the first | Provider CLI, token, DNS, or connection cache | Preserve the relevant cache and benchmark cold runs separately | | Only WSL or a container is slower | Filesystem, DNS, VPN, proxy, or executable interoperability | Compare the host/container probes and move hot files off mounted host paths | # Provider fallback > Select providers and define ordered fallback routes for secrets Secrets may live in different stores across environments or during a migration. An ordered provider route lets SecretSpec read from the first store that has a value while keeping one store as the write target. ## Provider selection order [Section titled “Provider selection order”](#provider-selection-order) SecretSpec selects each secret’s route in this order: 1. The `--provider` command-line option. 2. The `SECRETSPEC_PROVIDER` environment variable. 3. The secret’s effective `providers` list after profile inheritance and `[profiles..defaults]` are applied. 4. Project `[defaults].providers` (0.21+). 5. The default provider in the user configuration. `--provider` and `SECRETSPEC_PROVIDER` replace the configured route for every secret. Without an override, effective `providers` list, or project default chain (0.21+), SecretSpec uses the user-level default. ## Ordered fallback routes [Section titled “Ordered fallback routes”](#ordered-fallback-routes) Provider lists accept aliases, provider names, and inline URIs. Reads try them from left to right: secretspec.toml ```toml [providers] prod_vault = "onepassword://Production" local = "keyring://" [defaults] # 0.21+ providers = ["local"] [profiles.production.defaults] providers = ["prod_vault", "local"] [profiles.production] # Uses the profile default: prod_vault, then local. DATABASE_URL = { description = "Production database" } # Overrides the profile default and reads only from the environment. DEPLOY_TOKEN = { description = "Deployment token", providers = ["env"] } [profiles.development] # Uses the project default provider chain in SecretSpec 0.21+. DATABASE_URL = { description = "Development database" } ``` Reads stop at the first value. Writes and generated values go only to the first provider in the effective list (`prod_vault` above). Later entries are resolved, constructed, and contacted only when needed. If a reached provider cannot be resolved, constructed, or read, SecretSpec warns and continues. If every reached provider fails, the operation returns a provider error rather than reporting the secret as absent. Note A secret’s [`ref`](/reference/configuration/#secret-references) changes only the address within a provider; route selection stays the same. ## Cached routes [Section titled “Cached routes”](#cached-routes) SecretSpec 0.17+ can place a local cache before an ordered route. See [Provider caching](/concepts/providers/caching/) for configuration and freshness rules. ## Next steps [Section titled “Next steps”](#next-steps) * Learn how to [configure provider aliases](/concepts/providers/#configure-provider-aliases). * Learn how [Profiles](/concepts/profiles/) apply provider defaults. # Secret References > Point a secret at one already managed in a provider's store, by the store's own coordinates **New in version 0.14** By default, SecretSpec owns the naming: it stores each secret under its own `{project}/{profile}/{key}` convention. A **secret reference** overrides that for one secret, naming a secret that already exists in the store and is managed outside SecretSpec. SecretSpec then reads (and writes) that existing secret in place, instead of a convention path it controls. You declare a reference with the `ref` field, a table of provider-independent coordinates: ```toml [profiles.production] # The 1Password item "db", its "password" field DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["prod_vault"] } # An existing environment variable GITHUB_TOKEN = { description = "GitHub token", ref = { item = "GITHUB_PAT" }, providers = ["env"] } ``` ## Coordinates address a secret from the outside in [Section titled “Coordinates address a secret from the outside in”](#coordinates-address-a-secret-from-the-outside-in) A `ref` is not a store-specific address like `op://vault/item/field`. It is a set of provider-independent coordinates, each naming a level of structure that some stores have: ```plaintext vault which container holds the item (1Password only) └── item the store's own name for the secret (always required) └── section a named group of fields (1Password only) └── field one component inside the item (structured stores) └── version which revision to read (supported stores only) ``` Only `item` is universal, because every store names its secrets somehow. `item` is the **complete** name, not a suffix: it replaces the entire convention path, so nothing is prepended. ```toml # Reads the .env key TOTALLY_DIFFERENT_NAME, not secretspec/myapp/default/DATABASE_URL DATABASE_URL = { description = "DB", ref = { item = "TOTALLY_DIFFERENT_NAME" }, providers = ["dotenv"] } ``` The other coordinates exist because some stores give a secret internal structure (`field`, `section`), nest it inside a container (`vault`), or keep revisions (`version`). A store that has no equivalent for a coordinate **rejects it with an error naming the coordinate**, rather than silently reading the wrong secret. The [configuration reference](/reference/configuration/#secret-references) documents exactly how each provider maps the coordinates. ## References name, providers route [Section titled “References name, providers route”](#references-name-providers-route) A `ref` supplies naming only. It does not pin the secret to a particular store. Which provider actually resolves the coordinates follows the ordinary [provider resolution order](/concepts/providers/fallback/): a `--provider` override, then the secret’s `providers` chain, then profile defaults, project `[defaults].providers` in 0.21+, and the user-global default. This is the difference from pasting a store URL into your config. Because the store is not baked into the reference, the same `ref` works across providers. Each provider in a fallback chain is asked for the same coordinates, and one that cannot interpret them warns and the chain continues: ```toml [profiles.production] DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["onepassword://Production", "keyring"] } ``` It also means `--provider` redirects reference secrets exactly like convention secrets, which makes test fixtures trivial: point every reference at a `.env` file without touching the manifest. ```bash $ secretspec run --provider dotenv:.env.fixtures -- cargo test ``` ## Different coordinates per provider [Section titled “Different coordinates per provider”](#different-coordinates-per-provider-019) **New in version 0.19** The original `ref` remains useful when every provider understands one address. When endpoints organize the same logical secret differently, attach a template to each leaf alias and use `refs` only for exceptions: ```toml [providers] remote = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } local = { uri = "dotenv://.env", ref = { item = "{key}" } } legacy = "onepassword://Legacy" [profiles.production] API_KEY = { description = "API key", providers = ["remote", "local"], refs = { legacy = { item = "old-api-item", field = "token" } } } ``` SecretSpec resolves each endpoint independently: `refs.` wins, then that alias’s template, then convention naming. This applies to primary and fallback reads, writes, and both sides of `import`; the `legacy` ref above can therefore describe an import source without adding it to the normal read route. Templates support `{project}`, `{profile}`, and `{key}` in every coordinate. Scoped refs deliberately key on aliases, not resolved URIs. Literal provider URIs and bare provider names use convention naming because they have no alias identity. Cached route aliases cannot own a template or scoped ref; configure their individual leaf aliases instead. `refs` and legacy route-wide `ref` are mutually exclusive. For profile inheritance, `ref` and `refs` (0.19+) are two forms of one address model. A profile that explicitly declares either form replaces the form inherited from `[profiles.default]`: `refs` can replace an inherited `ref`, and `ref` can replace inherited `refs`. A profile entry that declares neither keeps the inherited form. See [Profiles: Switching reference models](/concepts/profiles/#switching-reference-models-019) for an example. ## How it works [Section titled “How it works”](#how-it-works) * `item` is required; `field`, `vault`, `section`, and `version` are optional and only accepted by stores that have that structure. * Reads and writes are symmetric: `secretspec set` and interactive `check` write through the coordinates in place wherever the store supports writes. Read-only stores fail with a clear error. * `ref` and every provider-scoped `refs.` value are tables. String and URI forms (`ref = "op://vault/item/field"`) are rejected, with an error that spells out the equivalent table. * Secrets sharing identical coordinates and store are fetched once, and [audit log](/concepts/audit/) events carry the coordinates. See the [configuration reference](/reference/configuration/#secret-references) for the full specification: the coordinate table, how every provider interprets each coordinate, and the exact rules. Azure App Configuration (0.20+) native `ref.item` values name one App Configuration key and remain read-only. An App Configuration value can itself be a canonical Azure Key Vault reference; SecretSpec follows that stored URI, including its optional Key Vault version. This is separate from SecretSpec’s `ref.version` coordinate, which Azure Key Vault accepts directly starting in 0.20. # Scopes > Resolve and expose only the secrets a service or task needs **New in version 0.17** A profile defines how secrets behave in an environment. A scope selects which of those secrets one service, command, or task receives. This lets several consumers share one profile without giving every consumer the complete secret set. An API and a background worker can use the same `production` profile, for example, while receiving different credentials. ## Define scopes [Section titled “Define scopes”](#define-scopes) Add a top-level `[scopes]` table to `secretspec.toml`. Each named scope contains an allowlist of secret names: secretspec.toml ```toml [profiles.default] DATABASE_URL = { description = "Database" } API_KEY = { description = "API key" } QUEUE_TOKEN = { description = "Queue token" } [scopes.api] secrets = ["DATABASE_URL", "API_KEY"] [scopes.worker] secrets = ["DATABASE_URL", "QUEUE_TOKEN"] ``` The active profile still controls requirements, defaults, providers, references, generation, composition, and storage addresses. Selecting `api` only narrows the resolved set to `DATABASE_URL` and `API_KEY`; it does not create another profile or another copy of either secret. A scope must contain at least one unique, non-blank name. Every name must be declared by at least one profile, although the active profile does not need to declare every member. ## Select a scope [Section titled “Select a scope”](#select-a-scope) `check`, `run`, and `export` accept `--scope`: ```bash $ secretspec check --profile production --scope api $ secretspec run --profile production --scope api -- ./api-server $ secretspec export --profile production --scope worker --format dotenv ``` Set `SECRETSPEC_SCOPE` to select one without repeating the flag: ```bash $ export SECRETSPEC_SCOPE=worker $ secretspec run --profile production -- ./worker ``` The command-line flag takes precedence over the environment variable. With no scope selected, SecretSpec resolves the complete profile as before. The untyped language SDK builders also accept an explicit scope and return its name in resolved and report results. See the [SDK overview](/sdk/overview/#the-runtime-api) for the shared behavior and each SDK guide for its language-specific method. ## What gets resolved [Section titled “What gets resolved”](#what-gets-resolved) The visible set is the intersection of the selected scope and the merged active profile: * a required secret outside the scope does not block scoped resolution; * a secret outside the scope is not fetched unless a visible composed secret needs it; * a valid scope whose intersection with the profile is empty resolves nothing and contacts no provider; * a scope member absent from the active profile is simply absent from the resolved result, allowing one scope to be reused across profiles with different shapes. Scopes never change a secret’s `{project}/{profile}/{key}` storage address. A scoped and unscoped read of the same secret addresses the same provider value. ### Composed secrets [Section titled “Composed secrets”](#composed-secrets) A visible [composed secret](/concepts/composed-secrets/) may depend on values the scope excludes: secretspec.toml ```toml [profiles.default] DB_USER = { description = "Database user" } DB_PASSWORD = { description = "Database password" } DB_HOST = { description = "Database host" } DATABASE_URL = { description = "Application database URL", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" } [scopes.api] secrets = ["DATABASE_URL"] ``` SecretSpec resolves the three inputs to build `DATABASE_URL`, then exposes only `DATABASE_URL`. A secret that is neither visible nor a dependency of a visible composition is never fetched. ## Narrow a process environment [Section titled “Narrow a process environment”](#narrow-a-process-environment) `run --scope` removes every manifest-declared secret the scope does not admit from the child environment, even when the parent shell had already exported it. The removal covers names declared in every profile, preventing a value inherited from another profile from leaking into the launched process. `export --scope` only emits the selected values. It cannot remove variables that already exist in the current shell because its output formats cannot express an unset. Use `run --scope` when narrowing an existing environment is the goal. Scopes minimize secret delivery; they are not an authorization boundary. A child that has access to `secretspec.toml` and valid provider credentials may resolve another scope itself. Use provider permissions, service isolation, and operating-system controls when the process must be prevented from reading other values. ## Validate scoped requirements [Section titled “Validate scoped requirements”](#validate-scoped-requirements) [Cross-secret presence constraints](/reference/configuration/#cross-secret-presence-constraints-017) are evaluated over the group members visible to the scope. A group with no visible members is not enforced for that consumer. When some members are visible, `at_least_one` or `exactly_one` is enforced over those members so a scoped consumer cannot rely on a credential it never receives. Run an unscoped `secretspec check` when validating the complete profile rather than one consumer’s view. ## Profiles and inheritance [Section titled “Profiles and inheritance”](#profiles-and-inheritance) Scopes are orthogonal to profiles and reusable across them. They do not inherit from the `default` profile; instead, the selected scope is applied after normal profile inheritance produces the effective profile. In SecretSpec 0.19+, a profile with `defaults.inherit = false` does not include any default-profile secrets. A scope still intersects the effective profile, so it cannot bring an inherited secret into a standalone profile. When a project uses [`extends`](/concepts/inheritance/), a child scope replaces a parent scope with the same name. The lists are not unioned, so extending a configuration cannot silently widen an allowlist. Scopes defined only by a parent remain available to the child. ## Commands that ignore scopes [Section titled “Commands that ignore scopes”](#commands-that-ignore-scopes) `set` and `import` ignore `SECRETSPEC_SCOPE`: a resolution allowlist must not silently restrict which secrets a write or migration command manages. Rust’s generated typed loaders also ignore the ambient scope because their structs contain a field for every secret in the profile. Use the untyped resolver API when a Rust consumer needs scoped resolution. See the [`[scopes]` configuration reference](/reference/configuration/#scopes-section) for validation details, empty-selection behavior, audit semantics, and clearing an inherited scope selection. # Adding a New Provider > Step-by-step guide for implementing custom provider backends ## Provider Trait [Section titled “Provider Trait”](#provider-trait) All providers must implement the `Provider` trait. Every operation names its secret with an `Address`: either the store’s own coordinates (a secret’s `ref`) or SecretSpec’s `{project}/{profile}/{key}` naming convention, which your provider compiles into its native coordinates via `convention_address`: ```rust pub trait Provider: Send + Sync { fn name(&self) -> &'static str; fn uri(&self) -> String; /// Compile SecretSpec's naming convention into the store's native /// coordinates. The single owner of the provider's convention layout. fn convention_address(&self, project: &str, profile: &str, key: &str) -> Result; fn get(&self, addr: Address<'_>) -> Result>; fn set(&self, addr: Address<'_>, value: &SecretString) -> Result<()>; /// Optional, defaults to empty. The `ref` coordinates your store can /// honor beyond `item`; every other coordinate is rejected for you. fn supported_coords(&self) -> &'static [&'static str] { &[] } /// Optional, defaults to writable. Reject only the addresses the provider /// cannot safely write: for example every address on a read-only provider, /// or version-pinned and ARN refs on an otherwise writable provider. State /// the reason: it is what the user sees. fn check_writable(&self, addr: Address<'_>) -> Result<()> { Ok(()) } /// SecretSpec 0.19+: optional, defaults to Persist. Return Ephemeral only /// when generated values should be returned for one resolution without /// calling `set`; ordinary writes remain governed by `check_writable`. fn generated_value_persistence(&self) -> ProducedValuePersistence { ProducedValuePersistence::Persist } /// SecretSpec 0.19+: optional, defaults to Persist. This is independent of /// `prompt = true`, which selects operator input rather than storage policy. fn prompted_value_persistence(&self) -> ProducedValuePersistence { ProducedValuePersistence::Persist } /// SecretSpec 0.19+: optional pre-write description. The default renders /// native coordinates; file-backed providers should include the resolved /// file/container and selector. Never include credentials. fn describe_write_target(&self, addr: Address<'_>) -> Result { /* default */ } /// Optional batch read. The default resolves each request's address and /// fetches every unique address once, concurrently; override it when the /// store has a real bulk surface (one listing, a batch API). fn get_many(&self, requests: &[(&str, Address<'_>)]) -> Result> { /* default */ } /// SecretSpec 0.18+: optional discovery hook used to build secret /// declarations from a provider. Return definitions only; never put values /// in descriptions or other manifest fields. Flat stores can ignore the /// context; hierarchical stores use it to bound discovery. fn reflect(&self, context: DiscoveryContext<'_>) -> Result> { /* unsupported */ } } ``` Inside `get`/`set`, call `self.resolve_coords(addr)` to obtain the native coordinates for any address. It rejects any coordinate outside `supported_coords` (e.g. a `field` on a flat key/value store), so a `ref` written for another store fails loudly instead of resolving something else — you declare the set, you never write the check. Have `set` call `self.check_writable(addr)?` first, so the pre-check and the write agree on one refusal message. SecretSpec 0.19+ also exposes `generated_value_persistence` and `prompted_value_persistence`. Leave their default of `Persist` for storage providers. `Ephemeral` is an explicit automatic-value capability: after a healthy read miss, SecretSpec returns the generated or prompted logical value for the current materializing resolution without calling `set` or refreshing a cache. It does not make ordinary writes succeed, and each method must be a pure, I/O-free capability check. In particular, `prompt = true` selects how a missing value is acquired; `prompted_value_persistence` decides what the provider does with the answer. In SecretSpec 0.19+, override `describe_write_target` when the provider URI and native coordinates do not identify the physical destination clearly. The `secretspec set` and interactive `secretspec check` commands print this description before they read or prompt for a value; SDK and library writes do not print it. The method must be pure with respect to the backing store: resolving and formatting a path is fine, but it must not create the file or directory. Keep the description credential-free, just like `uri()`. ### Convention templates [Section titled “Convention templates”](#convention-templates) When a provider lets users replace its complete convention layout, call that option `template` and support the `{project}`, `{profile}`, and `{key}` placeholders. Reserve `prefix` for an option that only prepends literal text to an otherwise fixed convention. For example, a hierarchical provider might use: ```text mybackend://account?template=/{profile}/{project}/{key} ``` Render the template only in `convention_address`, then validate the resulting native name before any provider I/O. This keeps `get`, `set`, batch reads, generation, and imports in the same address space. Document when a template omits a placeholder intentionally; in particular, omitting `{key}` can make several declarations target the same stored value. ### Discovery and `init --from` [Section titled “Discovery and init --from”](#discovery-and-init---from) SecretSpec 0.18+ passes a `DiscoveryContext` to the `reflect(context)` discovery hook. During `secretspec init --from PROVIDER`, the CLI constructs the provider, calls that hook, and turns the returned map into declarations in a new manifest. Flat stores such as dotenv and age ignore the context; hierarchical stores use it to bound discovery. A reflected `Secret` describes the discovered key; do not copy its value into the description, default, or any other committed field. `secretspec import PROVIDER` is different: it does **not** call `reflect(context)` or enumerate the source. It iterates the secrets already declared in the active and default profiles and copies their values into the configured destination. Implement the reflection hook for manifest discovery, not to change import semantics. SecretSpec 0.18+ accepts any provider that implements the hook as an `init --from` source. Use `--project` and `--profile` when the provider’s convention needs context other than the current directory name and the `default` profile. For a hierarchical store, reflection must have a bounded namespace and a reversible mapping from native names to SecretSpec keys. A configured template such as `/{profile}/{project}/{key}` provides both: render it with `DiscoveryContext`, list the prefix before `{key}`, reject nested or otherwise ambiguous results, and return the remaining key names. Do not list an entire account or vault as a fallback. The reflection hook returns declarations, not values, so it is also not a runtime namespace-injection API. A Chamber-style “export everything under this path” feature would need a separate value-bearing contract or an intentional extension of the provider trait. ## Implementation Steps [Section titled “Implementation Steps”](#implementation-steps) 1. **Create provider module** in `src/provider/mybackend.rs` 2. **Define config struct** with `Serialize`, `Deserialize`, `Default`, and `TryFrom<&Url>` 3. **Implement provider struct** and use the `register_provider!` macro for automatic registration 4. **Implement Provider trait** for your provider struct 5. **Export from mod.rs**: Add `pub mod mybackend;` ## Documentation and Release Visibility [Section titled “Documentation and Release Visibility”](#documentation-and-release-visibility) The documentation site is built from `main`, so it can describe code that has not reached the latest SecretSpec release yet. A new provider must not appear to be available in the currently released binary before it is published. ### Provider page structure [Section titled “Provider page structure”](#provider-page-structure) Provider pages should be predictable to scan. Keep the shared sections in the following relative order, inserting provider-specific topics where readers need them: 1. A one-sentence description and, for an unreleased provider, the version compatibility notice. 2. **At a glance**: the provider name, URI, read/write behavior, best use case, authentication, optional build feature or availability, and default storage layout. 3. **Quick start**: the shortest useful `set`, `get`, and `run` workflow. Assume the reader completes the following setup section first; keep this example focused on the successful path. 4. **Setup**: prerequisites, authentication methods, and required permissions. 5. **Configuration**: **URI format**, copyable **URI examples**, and a **Project configuration** example showing a checked-in alias used by a secret. 6. **Storage model**: the exact provider-native name or path SecretSpec creates, including how projects and profiles stay isolated. 7. **Use existing secrets**: how `ref` maps to provider-native coordinates and whether referenced secrets are writable. 8. **CI/CD**, when machine authentication or deployment setup differs from local use. 9. **Advanced configuration** for optional provider-specific behavior. 10. **Troubleshooting and limitations** or **Security considerations**, when there are important operational constraints. Keep the at-a-glance table compact; explain edge cases in the relevant section instead of expanding the table. Start with this shape: ```md ## At a glance | | | | --- | --- | | Provider | `mybackend` | | URI | `mybackend://HOST[/path]` | | Access | Read and write | | Best for | The main workload or audience this provider serves | | Authentication | The identity or credential users need | | Build feature | `mybackend` | | Default storage | `secretspec/{project}/{profile}/{key}` | ``` Use sentence case for section headings. If a standard section does not apply, omit it rather than adding an empty placeholder. Keep command sequences in **Quick start** and list bare provider specifications in **URI examples** so the two sections do not repeat one another. When adding a provider for an upcoming release: 1. Add a version notice at the very top of the provider page, after its imports, with the shared component. A new feature always uses the self-closing form and renders **New in version 0.16** with no body: ```mdx import VersionCompatibility from '@cachix/site-kit/ui/VersionCompatibility.astro'; ``` Place a section-level notice directly after its heading. When a release changes existing behavior, set `kind="changed"` and explain the change in the required body: ```mdx ## Advanced authentication Advanced authentication now requires a token with the `admin` scope. ``` 2. Mark the provider as `(0.16+)` anywhere it appears in a provider list, table, selector example, sidebar, landing page, README, or generated documentation description. 3. If the provider changes authentication or configuration syntax, label the new form explicitly with its target version. 4. Add the provider under the existing `Unreleased` section in `CHANGELOG.md`. Update every provider location; names otherwise drift out of sync: 1. `docs/src/content/docs/providers/.mdx` 2. `docs/astro.config.ts` — sidebar and `starlightLlmsTxt` provider summary 3. `docs/src/content/docs/concepts/providers.mdx` — available providers table 4. `docs/src/content/docs/reference/providers.mdx` — provider details and security considerations 5. `docs/src/pages/index.astro` — `providerMetadata` and any provider selector examples 6. `docs/src/content/docs/quick-start.mdx` — provider selector example 7. `README.md` — provider lists and provider selector example If the provider accepts injected provider credentials, also update `docs/src/data/provider-credentials.json`. Record every semantic credential name in the Rust registration’s order, its ordered environment fallbacks, its minimum SecretSpec version, and the implementation files where those fallbacks are defined. Use an `.mdx` provider page with exactly one `## Provider credentials` section and render the shared component there: ```mdx import ProviderCredentials from '../../../components/ProviderCredentials.astro'; ## Provider credentials ``` The catalog also renders the complete [provider credentials reference](/reference/provider-credentials/), and every provider-specific component links back to it. Run `npm --prefix docs run check:provider-credentials`. It rejects missing or stale catalog entries, environment fallbacks without implementation backlinks, and credential-aware provider pages that do not render their catalog entry. Use durable wording such as “Added in SecretSpec 0.16.” The `(0.16+)` labels may remain where knowing the minimum version is useful. Apply the same rule to unreleased CLI commands and configuration fields: place a version notice beside the command or field, not only on a separate concept page. Readers often arrive directly from search results. ## Example Implementation [Section titled “Example Implementation”](#example-implementation) ```rust use super::Provider; use crate::{Result, SecretSpecError}; use url::Url; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MyBackendConfig { pub endpoint: Option, } impl Default for MyBackendConfig { fn default() -> Self { Self { endpoint: None } } } impl TryFrom<&Url> for MyBackendConfig { type Error = SecretSpecError; fn try_from(url: &Url) -> std::result::Result { if url.scheme() != "mybackend" { return Err(SecretSpecError::ProviderOperationFailed( format!("Invalid scheme '{}' for mybackend provider", url.scheme()) )); } // Parse URL into configuration Ok(Self { endpoint: url.host_str().map(|s| s.to_string()), }) } } pub struct MyBackendProvider { config: MyBackendConfig, } crate::register_provider! { struct: MyBackendProvider, config: MyBackendConfig, name: "mybackend", description: "My custom backend provider", schemes: ["mybackend"], examples: ["mybackend://api.example.com", "mybackend://localhost:8080"], } impl MyBackendProvider { pub fn new(config: MyBackendConfig) -> Self { Self { config } } } impl Provider for MyBackendProvider { fn name(&self) -> &'static str { Self::PROVIDER_NAME } fn uri(&self) -> String { "mybackend".to_string() } fn convention_address(&self, project: &str, profile: &str, key: &str) -> Result { Ok(NativeAddress { item: format!("secretspec/{}/{}/{}", project, profile, key), ..Default::default() }) } fn get(&self, addr: Address<'_>) -> Result> { let coords = self.resolve_coords(addr)?; // Reject coordinates the store cannot honor, then read coords.item Ok(None) } fn set(&self, addr: Address<'_>, value: &SecretString) -> Result<()> { let coords = self.resolve_coords(addr)?; // Write value at coords.item Ok(()) } } ``` # SDK Development > How the language SDKs are built, packaged, and released, and which platforms each one supports SecretSpec ships SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C#, Swift (0.18+), and JVM languages (0.20+). This page is for contributors: how the SDKs are put together, how each one is packaged and released, which platforms each artifact covers, and what to update when adding a platform or a new SDK. For the user-facing architecture and API, see the [SDK overview](/sdk/overview). ## One resolver, many packages [Section titled “One resolver, many packages”](#one-resolver-many-packages) All resolution logic lives in the `secretspec` Rust crate. The SDKs reach it two ways: * **Through the C ABI** (`libsecretspec`, which builds a `cdylib` for dynamic loading and a `staticlib` for embedding): Ruby (mkmf extension statically links the archive), Go (purego `dlopen` of the cdylib, or cgo against the archive with `-tags static`), Haskell (GHC FFI against the archive), C# (P/Invoke against per-runtime cdylibs in the NuGet package), Swift (0.18+; Clang C import from an XCFramework), JVM languages (0.20+; JNA against native assets in the JAR), and PHP’s `ext-ffi` fallback (runtime `dlopen` of the cdylib). The C ABI crate and artifacts are named `libsecretspec` in SecretSpec 0.20+; they were named `secretspec-ffi` / `secretspec_ffi` through 0.19. * **As an embedded extension**: Python ([pyo3](https://pyo3.rs/)), Node.js ([napi-rs](https://napi.rs/)), and PHP’s preferred backend ([ext-php-rs](https://github.com/davidcole1340/ext-php-rs)) compile the resolver directly into a language-native extension module. Every SDK exchanges the same JSON request/response with the core, and the cross-language conformance suite (`conformance/`, run by `.github/workflows/sdks.yml` on every PR) asserts they all reduce the same inputs to the same result. Package versions for the non-Rust SDKs are not hand-edited: release workflows run `scripts/sync-sdk-versions.sh`, which stamps the Cargo workspace version into every package manifest. ## Packaging workflows [Section titled “Packaging workflows”](#packaging-workflows) Each SDK has a dedicated distribution workflow that builds artifacts per platform and publishes on a version tag: | SDK | Package | Workflow | | ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------ | | Rust | `secretspec` on crates.io (source) | `publish.yml` | | Python | `secretspec` wheels on PyPI | `python-wheels.yml` | | Node.js | `secretspec` + per-platform packages on npm | `node-addon.yml` | | Go | Go module (source) + `libsecretspec` release assets | `go-embed.yml`, `go-static.yml`, `ffi-build.yml` | | Ruby | `secretspec` platform gems on RubyGems | `ruby-gems.yml` | | C# | `Cachix.SecretSpec` on NuGet | `dotnet-package.yml` | | Swift (0.18+) | SwiftPM source package + XCFramework release asset | `swift-package.yml` | | PHP | Composer package (source) + prebuilt extension binaries and `libsecretspec` release assets | `php-ext.yml`, `ffi-build.yml` | | Haskell | `secretspec` on Hackage (source) | `haskell-build.yml` | ## Platform support [Section titled “Platform support”](#platform-support) Platforms each released artifact covers. Windows support for the Python wheel, the Ruby gem, and the PHP extension binaries is added in SecretSpec 0.17. | SDK | Linux x64 | Linux arm64 | macOS Intel | macOS Apple silicon | Windows x64 | Windows arm64 | | ------------------- | ------------------------ | ------------------------ | ----------- | ------------------- | --------------------- | ------------- | | Rust (source crate) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Python | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | Node.js | ✓ (glibc and musl 0.20+) | ✓ (glibc and musl 0.20+) | — | ✓ | ✓ | — | | Go | ✓ | ✓ | — | ✓ | ✓ | — | | Ruby | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | C# | ✓ (glibc and musl) | ✓ (glibc and musl) | ✓ | ✓ | ✓ | ✓ | | Swift (0.18+) | — | — | ✓ | ✓ | — | — | | PHP | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | Haskell (source) | ✓ (CI-covered) | — | — | — | ✓ (CI-covered, 0.17+) | — | | JVM (0.20+) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Notes: * Most Linux binary artifacts build inside manylinux\_2\_28 containers so they run on any distro with glibc >= 2.28 (the Ruby Linux gem still links the build runner’s glibc; a baseline toolchain there is a tracked follow-up). The keyring provider uses a Rust-native D-Bus transport on Linux and does not require system libdbus. * Hackage distributes source only; the Haskell column records which platforms CI builds and tests, since users link `libsecretspec` themselves. * The Swift package targets macOS 12+ only. Its XCFramework contains native Intel and Apple-silicon slices; mobile Apple platforms are intentionally out of scope for a development-workflow resolver that launches provider CLIs and reads desktop files and credential stores. * The fully-static Go binary (`-tags static`, musl) is Linux x64 only. ## Why Swift uses the C ABI [Section titled “Why Swift uses the C ABI”](#why-swift-uses-the-c-abi) Swift interoperates with C directly through Clang modules, and SwiftPM distributes native Apple binaries as XCFramework binary targets. That fits the existing `libsecretspec` boundary exactly: ownership-audited C functions carry one already-versioned JSON contract. [UniFFI](https://mozilla.github.io/uniffi-rs/latest/) is a good default for a new object-rich Rust API that needs generated Swift and Kotlin bindings. It would be the wrong layer here: SecretSpec already has a deliberately narrow ABI shared by several SDKs, and introducing UniFFI would create a second exported ABI, generated Rust scaffolding, and another schema to version. The hand-written Swift layer is limited to `Codable` request/response models and idiomatic errors; resolution remains entirely in Rust. ## Versioned native calls [Section titled “Versioned native calls”](#versioned-native-calls-020) **New in version 0.20** `secretspec_resolve` remains the compatibility request for path and search resolution. SDKs that need a declaration held in application code call the new `secretspec_call` symbol with request version 1 instead. A separately versioned `source` is exactly one of `search`, `path`, or `inline`; an inline source also carries a logical `base_dir`, used for relative provider paths as `Secrets::from_spec_at` does. The inline specification is strict JSON, not the private Rust `Config` or a serialized compiled manifest. Its v2 shape contains `project`, `profiles`, and a `secrets` object per profile, with optional provider aliases, scopes, and the normal secret declaration fields. `project.extends` uses paths relative to the inline declaration’s `base_dir`, so the full configuration model—including inheritance—is supported. Unknown request and declaration fields, unsupported versions, and unsupported operations are rejected. SDKs bind `secretspec_call` only when using inline specs: an older library therefore reports the missing capability instead of silently ignoring an unknown field and loading a filesystem manifest. Inline schema v2 adds project-level `defaults.providers` in SecretSpec 0.21+. `scripts/build-swift-xcframework.sh` changes Cargo’s target-local dylib install name to `@rpath`, merges the native slices into a universal dylib, adds the C header and module map, and invokes `xcodebuild -create-xcframework`. `swift-package.yml` builds each architecture natively, tests the final universal artifact, computes SwiftPM’s SHA-256 checksum, and attaches the ZIP to the GitHub release. See `RELEASE.md` for the required pre-tag checksum step. ## Windows toolchains [Section titled “Windows toolchains”](#windows-toolchains) Windows artifacts split across two Rust targets, and the split is load-bearing: * **MSVC (`x86_64-pc-windows-msvc`)** for artifacts loaded by MSVC-built hosts: the CLI, the FFI cdylib, the Python wheel, the Node addon, the NuGet natives, and the PHP extension. PHP is the special case: PHP’s Windows ABI uses the vectorcall calling convention, which stable Rust does not expose, so `php-ext.yml` builds that one artifact on nightly Rust (the same setup ext-php-rs’s own CI uses). ext-php-rs downloads the PHP development pack matching the installed `php.exe` during the build. * **MinGW (`x86_64-pc-windows-gnu`, declared in `rust-toolchain.toml`)** for artifacts linked by MinGW toolchains, which cannot consume MSVC `.lib` archives: the staticlib bundled in the Ruby gem (RubyInstaller’s devkit) and the one the Haskell CI job links (GHC’s bundled toolchain). Building it needs a MinGW C compiler for the archive’s C dependencies (aws-lc-sys, SQLite, zstd) and NASM for aws-lc’s assembly. A `staticlib` does not carry its native link-time dependencies; consumers capture them from `cargo rustc ... -- --print native-static-libs`. On `windows-gnu` that list names import libraries that ship inside cargo registry crates (`libwindows.*.a` from `windows_x86_64_gnu`, `libwinapi_*.a` from `winapi-x86_64-pc-windows-gnu`) and exist in no MinGW distribution. `scripts/copy-mingw-import-libs.sh` stages exactly the referenced ones next to the archive — the Ruby gem bundles them in `vendor/`, the Haskell job points GHC’s linker at them. ## Linking through pkg-config [Section titled “Linking through pkg-config”](#linking-through-pkg-config-019) **New in version 0.19** `libsecretspec/scripts/cinstall.sh PREFIX static|shared` uses [cargo-c](https://github.com/lu-zero/cargo-c) to install one library type, the header, and a `libsecretspec.pc` carrying its full link line. This lets pkg-config consumers skip the `native-static-libs` capture above. Use separate prefixes for the two modes: both metadata files use `-lsecretspec`, and the linker prefers a shared library when both forms are present. ## Adding a platform to an SDK [Section titled “Adding a platform to an SDK”](#adding-a-platform-to-an-sdk) 1. Add the platform to the SDK’s distribution workflow matrix, and make the publish job consume the new artifact. 2. Build natively on a runner of that platform where possible; the workflows deliberately avoid cross-compiling because the crate links system libraries. 3. Keep the artifact self-contained: vendor or statically link anything an end user’s machine will not have (see the manylinux and MinGW import library notes above). 4. Smoke test in the same workflow: install or load the built artifact and call one function through it. 5. Update the platform table above, the [SDK overview](/sdk/overview) platform section, and label the platform with its target release (for example `(0.17+)`) until that release ships. 6. Add a user-facing CHANGELOG entry. ## Adding a new SDK [Section titled “Adding a new SDK”](#adding-a-new-sdk) 1. Create the binding crate/package as a workspace sibling (`secretspec-/`), thin: marshal the JSON envelope, expose the builder/resolve API mirroring the existing SDKs’ vocabulary. 2. Wire the package manifest into `scripts/sync-sdk-versions.sh` so its version tracks the workspace. 3. Add the SDK to the conformance suite and to `.github/workflows/sdks.yml`. 4. Create a distribution workflow following an existing one (`ruby-gems.yml` and `python-wheels.yml` are the smallest), including publish-on-tag with trusted publishing where the registry supports it. 5. Document it: `docs/src/content/docs/sdk/.md`, the sidebar in `docs/astro.config.ts`, the [SDK overview](/sdk/overview), and the platform tables on this page and the overview. 6. Follow the same release-visibility rules as providers: label everything with the target version until the release ships (see [Adding Providers](/development/adding-providers)). # Claude Code > Let Claude Code retrieve API and gateway credentials through SecretSpec providers **New in version 0.21** The Claude Code credential integration is available in SecretSpec 0.21+. It configures Claude Code’s native [`apiKeyHelper`](https://code.claude.com/docs/en/settings#available-settings) to retrieve an Anthropic API or LLM gateway credential from any SecretSpec provider. Authentication scope This integration configures API or gateway authentication. When `apiKeyHelper` is the active credential, it replaces a Claude Pro, Max, Team, or Enterprise subscription for the session and bills the account behind the configured credential. Claude Code’s `/login` OAuth credentials have a separate lifecycle and remain managed by Claude Code. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Claude Code 2.1.211 or newer * SecretSpec 0.21 or newer, with `secretspec` on `PATH` ## Quick start [Section titled “Quick start”](#quick-start) **New in version 0.21** From anywhere in a Git repository, configure its personal `.claude/settings.local.json` in the main checkout root: ```bash $ secretspec claude configure Configured Claude Code credential integration in /work/my-project/.claude/settings.local.json. Store the credential with: secretspec claude login Undo with: secretspec claude unconfigure Keep /work/my-project/.claude/settings.local.json out of version control; it contains a machine-local SecretSpec configuration identifier. ``` SecretSpec preserves unrelated Claude settings and refuses to replace an existing `apiKeyHelper` that it does not manage. Claude Code uses `settings.local.json` for machine-specific project settings. SecretSpec prints a reminder to keep that file out of version control. Do not move the generated helper into the team-shared `.claude/settings.json`, because its configuration identifier belongs to one user’s SecretSpec state. Outside a Git repository, SecretSpec follows Claude Code and uses the current directory. Store the API key without putting it in shell history: ```bash $ secretspec claude login ? Enter Claude Code API or gateway credential: ``` Claude Code now invokes the managed SecretSpec command whenever it needs the credential: ```bash $ claude ``` `configure` stores no credential. It adds only a short configuration identifier to Claude settings. The matching owner-only SecretSpec state records the provider selection, access reason, audit resource, and credential declaration, but never the resolved value. Each settings scope and audit resource receives a deterministic embedded identity. Credentials therefore remain separate across projects, user configurations, and gateways, including in providers that flatten project namespaces. Lookup still works when Claude Code starts from a subdirectory. To pin a provider instead of using the current SecretSpec default, pass it to `configure`. Later `login`, `logout`, and helper calls automatically reuse it: ```bash $ secretspec claude configure --provider onepassword $ secretspec claude login ``` An exported `SECRETSPEC_FILE`, `SECRETSPEC_PROFILE`, `SECRETSPEC_PROVIDER`, or `SECRETSPEC_REASON` is not saved as durable helper configuration. Pass the corresponding option explicitly when it should be pinned. ## Configure every project [Section titled “Configure every project”](#configure-every-project) **New in version 0.21** This changes your user-level Claude Code settings `--global` updates `$CLAUDE_CONFIG_DIR/settings.json` when `CLAUDE_CONFIG_DIR` is set, or `~/.claude/settings.json` otherwise. The helper provides the user-level default for Claude Code projects. SecretSpec asks for confirmation that defaults to **No**. Undo it with `secretspec claude unconfigure --global`. Use the same `CLAUDE_CONFIG_DIR` value for `configure`, `login`, `logout`, and `unconfigure`; each directory represents a separate Claude Code account and receives an isolated embedded credential. ```bash $ secretspec claude configure --global $ secretspec claude login --global ``` Pass `--yes` only for non-interactive setup. User, project, and local settings have separate embedded credentials. Claude Code project or local settings can override the user-level helper, while managed settings and command-line settings have higher precedence than every user-controlled settings file. ## Use an LLM gateway [Section titled “Use an LLM gateway”](#use-an-llm-gateway) **New in version 0.21** Set Claude Code’s non-secret gateway URL normally, then record its host as the SecretSpec audit resource: .claude/settings.json ```json { "env": { "ANTHROPIC_BASE_URL": "https://gateway.example.com" } } ``` ```bash $ secretspec claude configure --resource gateway.example.com $ secretspec claude login ``` The audit resource participates in the embedded credential identity, so two projects or settings scopes can use different gateway credentials in the same provider without collision. `login`, `logout`, and retrieval all record the configured gateway host in caller context. Changing `--resource` selects a new embedded credential and does not delete the old one. Run `logout` before reconfiguring when the old credential should be removed. `apiKeyHelper` sends the returned value using Claude Code’s model credential headers. Confirm that the gateway accepts that authentication shape before using it. The gateway URL itself is ordinary configuration and does not belong in SecretSpec. ## Use a project manifest [Section titled “Use a project manifest”](#use-a-project-manifest) **New in version 0.21** Pass `--file` when the credential already has a project or company declaration. In this mode, `--token-secret` is required and `--profile` is available: company-claude.toml ```toml [project] name = "company-claude" revision = "1.0" [profiles.default] ANTHROPIC_API_KEY = { description = "Anthropic API key for Claude Code" } ``` ```bash $ secretspec --file company-claude.toml set ANTHROPIC_API_KEY $ secretspec --file company-claude.toml claude configure \ --token-secret ANTHROPIC_API_KEY ``` The managed state records the manifest’s absolute logical path, resolved profile, secret name, and an explicitly supplied provider. It does not copy the credential. If the manifest moves, rerun `configure`. Manage custom-manifest values with ordinary `secretspec set` and `secretspec delete`; `claude login` and `logout` intentionally manage only the embedded store. ## Remove credentials and configuration [Section titled “Remove credentials and configuration”](#remove-credentials-and-configuration) **New in version 0.21** Remove the embedded credential without changing Claude Code settings: ```bash $ secretspec claude logout $ secretspec claude logout --global ``` The first command removes the current project’s embedded credential; the second removes the user-level credential. A provider pinned by `configure` is selected automatically. For a custom manifest, use `secretspec delete` instead. Remove SecretSpec’s `apiKeyHelper` from the current project: ```bash $ secretspec claude unconfigure ``` Add `--global` to remove the user-level setting. User-level removal also defaults to **No** and accepts `--yes` for non-interactive use: ```bash $ secretspec claude unconfigure --global ``` `logout` and `unconfigure` are independent. Unconfigure preserves the stored credential, its owner-only lifecycle metadata, unrelated Claude settings, and the settings file itself. You can still run `logout` after `unconfigure`. If the managed `apiKeyHelper` changes outside SecretSpec, unconfigure refuses to remove it. ## Refresh and precedence [Section titled “Refresh and precedence”](#refresh-and-precedence) Claude Code caches an `apiKeyHelper` result for five minutes by default and calls the helper again after an HTTP 401. Set `CLAUDE_CODE_API_KEY_HELPER_TTL_MS` when the credential has a shorter lifetime. In Claude Code’s [authentication precedence](https://code.claude.com/docs/en/authentication#authentication-precedence), Claude apps gateway sessions and enabled cloud providers take precedence over API credentials. Among ordinary model credentials, `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_API_KEY` take precedence over `apiKeyHelper`; the helper takes precedence over `CLAUDE_CODE_OAUTH_TOKEN`, Anthropic profiles, and `/login` credentials. Remove either static API variable before relying on SecretSpec: ```bash $ unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN $ claude ``` Run `/status` inside Claude Code to confirm that the intended settings file loaded and API authentication is active. Do not run the generated credential command merely to test the setup: it deliberately prints the credential. Organization policy can block `apiKeyHelper` when Claude Code is forced to verify membership through a particular login method. `apiKeyHelper`, `ANTHROPIC_API_KEY`, and `ANTHROPIC_AUTH_TOKEN` apply to the terminal CLI and surfaces that wrap it, including the VS Code extension, Agent SDK, and GitHub Actions. Claude Desktop and cloud sessions do not invoke the helper. ## Manual configuration [Section titled “Manual configuration”](#manual-configuration) **New in version 0.21** The management commands are optional when a declarative settings file is preferred. Declare the credential in a manifest and set `apiKeyHelper` to an ordinary `secretspec get` command: ```json { "apiKeyHelper": "secretspec --reason \"Claude Code model authentication\" get ANTHROPIC_API_KEY" } ``` This setting is not recorded as SecretSpec-managed, so `secretspec claude unconfigure` does not remove it. Keep an explicit `--file` in the command when the helper can run outside the manifest’s directory. Add `--caller claude-code --caller-operation credential_get --caller-resource api.anthropic.com` when the manual command should carry the same structured audit context as the managed integration. # Docker credentials > Let Docker retrieve registry credentials through SecretSpec providers The Docker credential integration is available in SecretSpec 0.20+. It lets `docker pull`, `docker push`, `docker build`, and Docker Compose retrieve registry credentials from any SecretSpec provider without copying the password or token into Docker’s `config.json`. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Docker * SecretSpec 0.20 or newer, including `docker-credential-secretspec` on `PATH` ## Quick start [Section titled “Quick start”](#quick-start) These commands are available in SecretSpec 0.20+. Configure the registry with its non-secret username: This changes your Docker configuration Docker has no repository-local configuration. `configure` updates `$DOCKER_CONFIG/config.json` when `DOCKER_CONFIG` is set, or the user-level `~/.docker/config.json` (`%USERPROFILE%\.docker\config.json` on Windows) otherwise. The change applies to every Docker command using that configuration. SecretSpec preserves unrelated settings and refuses to replace another helper. Undo it with `secretspec docker unconfigure --registry ghcr.io`. ```bash $ secretspec docker configure --registry ghcr.io --username YOUR_USERNAME ``` After confirmation, the command prints the matching login command: ```console Configured Docker credential for ghcr.io. Docker configuration: /home/you/.docker/config.json Store the credential with: secretspec docker login 'ghcr.io' Undo with: secretspec docker unconfigure --registry 'ghcr.io' ``` Store the password or access token in SecretSpec’s embedded, registry-isolated credential store: ```bash $ secretspec docker login ghcr.io ``` `login` prompts securely on a terminal and reads the password or token from standard input when piped. Use SecretSpec to log in and out After configuring the helper, use `secretspec docker login` and `secretspec docker logout` to manage the stored credential. Docker’s own `docker login` and `docker logout` operations try to write or erase through the helper, which is intentionally read-only. Docker now invokes `docker-credential-secretspec get` automatically: ```bash $ docker pull ghcr.io/OWNER/IMAGE:TAG $ docker push ghcr.io/OWNER/IMAGE:TAG ``` `configure` does not retrieve or store the credential. It adds the registry’s `credHelpers` entry and records only the registry, Docker configuration path, username, provider selection, and other value-free metadata. `login` prompts for the secret and stores it through the selected provider. Each registry and physical Docker configuration pair has a separate SecretSpec project and secret-key identity, so credentials remain isolated even in flat providers that do not namespace keys by project or profile. SecretSpec’s managed state is owner-readable and owner-writable only; Docker’s existing `config.json` permissions are preserved. Rerunning `configure` for the same registry and Docker configuration replaces its SecretSpec metadata and reports that replacement. It does not delete the stored credential. To use a provider other than your default, pass the same override to both commands. The follow-up command printed by `configure` includes it automatically: ```bash $ secretspec docker configure \ --registry ghcr.io \ --username YOUR_USERNAME \ --provider onepassword $ secretspec docker login ghcr.io --provider onepassword ``` Exported `SECRETSPEC_FILE`, `SECRETSPEC_PROFILE`, `SECRETSPEC_PROVIDER`, and `SECRETSPEC_REASON` values are not saved as durable Docker helper settings. Pass `--file`, `--profile`, `--provider`, or `--reason` explicitly when the helper should keep using that selection. ## Docker Hub [Section titled “Docker Hub”](#docker-hub) Docker uses the historical key `https://index.docker.io/v1/` for Docker Hub. SecretSpec 0.20+ normalizes the familiar Docker Hub hostnames and URL forms to that key: ```bash $ secretspec docker configure \ --registry docker.io \ --username YOUR_DOCKER_ID $ secretspec docker login docker.io ``` Registry addresses may contain a port, such as `registry.example.com:5000`, but not a repository path. Credentials are scoped to the registry rather than an image namespace. ## Use a project manifest [Section titled “Use a project manifest”](#use-a-project-manifest) Custom Docker credential configuration is available in SecretSpec 0.20+. For a credential already declared by a project, pass `--file` to select the advanced custom-manifest mode. In this mode, `--token-secret` and either `--username` or `--username-secret` are required: ```toml [project] name = "docker-credentials" revision = "1.0" [profiles.default] GHCR_TOKEN = { description = "GitHub Container Registry token" } ``` ```bash $ secretspec set GHCR_TOKEN --file secretspec.toml $ secretspec --file secretspec.toml docker configure \ --registry ghcr.io \ --token-secret GHCR_TOKEN \ --username YOUR_USERNAME ``` To resolve the username from SecretSpec too, declare it and replace `--username` with `--username-secret GHCR_USERNAME`. Custom-manifest mode also accepts `--profile` and `--provider`. The managed state records the manifest’s absolute path and, when supplied as `--profile`, that profile; it never records resolved secret values. Without an explicit `--profile`, the helper resolves the normal profile each time it runs. A symlinked manifest retains its logical path, so relative `extends` entries resolve beside the symlink. If the manifest moves, rerun `configure` for the affected registry. Manage custom-manifest values with `secretspec set` and `secretspec delete`; `secretspec docker login` and `logout` intentionally manage only the embedded store. ## Alternate Docker configuration directory [Section titled “Alternate Docker configuration directory”](#alternate-docker-configuration-directory) Per-configuration Docker credential isolation is available in SecretSpec 0.20+. SecretSpec and Docker both honor `DOCKER_CONFIG` when selecting `config.json`: ```bash $ DOCKER_CONFIG="$HOME/.config/docker-work" \ secretspec docker configure \ --registry registry.example.com \ --username YOUR_USERNAME ``` The same registry can use different SecretSpec credentials in different Docker configuration directories. Embedded credentials are isolated by both registry and the physical Docker configuration path. Equivalent paths through symlinked directories resolve to the same credential identity. Use the same `DOCKER_CONFIG` value when logging in, logging out, or unconfiguring entries from that file. Use DOCKER\_CONFIG instead of docker —config Docker’s `--config ` flag selects a configuration for the Docker process, but Docker does not pass that path to credential helpers. Export `DOCKER_CONFIG=` instead so `docker-credential-secretspec` can identify the matching managed credential. Using only `docker --config ` can find the helper registration while leaving the helper unable to select its credential. ## Remove credentials and configuration [Section titled “Remove credentials and configuration”](#remove-credentials-and-configuration) These removal commands are available in SecretSpec 0.20+. Remove an embedded secret without changing Docker’s helper configuration: ```bash $ secretspec docker logout ghcr.io ``` Pass the same `--provider` used for login when it was explicitly overridden. Remove one helper registration from the active Docker configuration: ```bash $ secretspec docker unconfigure --registry ghcr.io ``` Remove every Docker credential helper registration that SecretSpec owns in that file: ```bash $ secretspec docker unconfigure --all ``` Configuration changes prompt with a default of **No**. Pass `--yes` for non-interactive setup or removal. SecretSpec preserves the default credential store, other registry helpers, existing `auths`, and unrelated Docker options. If a managed entry changes outside SecretSpec, `unconfigure` refuses to modify another helper’s entry. If the SecretSpec helper entry is already absent, `unconfigure` safely removes the stale managed state so an interrupted removal can be rerun. `logout` and `unconfigure` are independent: logout deletes the embedded secret, while unconfigure removes Docker’s reference to the helper. This matches the separation between `login` and `configure`. ## Read-only helper behavior [Section titled “Read-only helper behavior”](#read-only-helper-behavior) In SecretSpec 0.20+, `docker-credential-secretspec` answers Docker’s `get` operation. It rejects `store`, `erase`, and `list`, so Docker’s own `docker login` and `docker logout` cannot overwrite or delete values in a shared provider. Use `secretspec docker login` and `secretspec docker logout` for the embedded store, or normal SecretSpec commands for a custom manifest. Docker may still print `Removing login credentials` and exit successfully after `docker logout` even though a read-only helper retained the credential. Use `secretspec docker logout` to remove the stored value, and `secretspec docker unconfigure` to stop Docker from invoking the helper. When no matching configuration or stored value exists, the helper returns Docker’s standard credential-not-found response. # Git credentials > Let Git retrieve HTTPS and SMTP credentials through SecretSpec providers The Git credential helper is available in SecretSpec 0.20+. It lets ordinary `git clone`, `git fetch`, `git pull`, and `git push` commands retrieve HTTPS credentials from any SecretSpec provider. It also supports SMTP authentication for `git send-email`. Use it when your Git token already lives in a provider such as 1Password, Bitwarden, or Vault and you do not want to copy it into a separate Git credential store. The integration does not manage SSH keys or inject secrets into repositories. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Git * SecretSpec 0.20 or newer, including `git-credential-secretspec` on `PATH` ## Configure Git [Section titled “Configure Git”](#configure-git) These commands are available in SecretSpec 0.20+. Register the helper, keeping the non-secret username in Git: ```bash $ secretspec git configure \ --url https://github.com \ --username YOUR_USERNAME ``` Then store the password or token through your configured default provider: ```bash $ secretspec git login https://github.com ? Enter value for PASSWORD (profile: default): ``` The built-in manifest declares a required `PASSWORD` and optional `USERNAME`. It is embedded in the binary: the helper never searches the current directory for `secretspec.toml`, so clone, fetch, and push resolve the same declarations inside or outside a repository. Git configuration records no manifest path. Each canonical credential target has a separate provider namespace. The identity includes the protocol and host, plus the configured path when `useHttpPath` is enabled, so credentials for different hosts or path scopes cannot share a value accidentally. To keep the username in the provider too, omit `--username` from `configure` and supply it when logging in: ```bash $ secretspec git configure --url https://github.com $ secretspec git login https://github.com --username YOUR_USERNAME ``` `login` prompts securely on a terminal and reads the password or token from standard input when piped. Use the same `--provider` override on `configure` and `login` when the credential should not use your default provider. The helper checks the URL independently before loading the provider. A token configured for `https://github.com` is not returned for another host or for an HTTP remote. Use HTTPS for credentials Although the helper accepts `http://` URLs for trusted local or test systems, HTTP does not encrypt the credential in transit. Use `https://` for remote services. To limit a credential to part of a host, include the path in the URL: ```bash $ secretspec git configure \ --url https://github.com/cachix \ --username YOUR_USERNAME $ secretspec git login https://github.com/cachix ``` SecretSpec also enables Git’s `useHttpPath` setting for that URL. This example answers for repositories below `https://github.com/cachix/`, but not for another GitHub organization. The path-scoped credential is stored separately from one configured for all of `https://github.com`. ## Send patches with SMTP [Section titled “Send patches with SMTP”](#send-patches-with-smtp) SMTP credential support is available in SecretSpec 0.20+. Git queries credential helpers when `sendemail.smtpUser` is set and `sendemail.smtpPass` is omitted: ```bash $ git config --global sendemail.smtpServer smtp.example.com $ git config --global sendemail.smtpServerPort 587 $ git config --global sendemail.smtpEncryption tls $ git config --global sendemail.smtpUser user@example.com $ secretspec git configure \ --url smtp://smtp.example.com:587 \ --username user@example.com \ --global $ secretspec git login smtp://smtp.example.com:587 ``` The SMTP URL must include the port that Git uses. The username on `configure` must match `sendemail.smtpUser`. `login` and `logout` read it back from Git configuration; pass `--username` explicitly if the helper has already been unconfigured or another account is being managed. Protocol, server, port, and username form the embedded storage identity, so two accounts on the same SMTP server never share a password. Git resolves a single helper per credential URL, so one account is configured for a given server and port at a time. Running `configure` again with a different `--username` switches to that account and says which entry it replaced. The other account’s password stays in its own storage identity and is used again as soon as you switch back, and `logout` is what removes a stored password. Server names are matched case-insensitively, so `smtp://SMTP.Example.COM:587` and `smtp://smtp.example.com:587` are the same target. The `smtp` URL is Git’s credential-context name, not a transport-security setting. Encryption remains controlled by `sendemail.smtpEncryption=tls|ssl`. SecretSpec never writes `sendemail.smtpPass` or any other `sendemail.*` setting, and the helper rejects HTTP(S), a different port, or another username when answering an SMTP request. ## Clone private repositories [Section titled “Clone private repositories”](#clone-private-repositories) Configure the embedded credential globally before the destination repository exists: This changes your global Git configuration Using `--global` enables this credential helper for matching URLs in every Git repository owned by your user. Review the URL before confirming. To roll back the example below, run `secretspec git unconfigure --url https://github.com --global`; see [Remove the configuration](#remove-the-configuration) for all removal options. ```bash $ secretspec git configure \ --url https://github.com \ --username YOUR_USERNAME \ --global $ secretspec git login https://github.com ``` Then clone normally: ```bash $ git clone https://github.com/OWNER/REPOSITORY.git ``` Git invokes the SecretSpec credential helper automatically. The token does not need to appear in the clone URL or your shell history. Global changes require a confirmation that defaults to **No**. Pass `--yes` only for non-interactive setup. `configure` records `--provider` and `--reason` in the generated helper only when you pass them on the command line. An exported `SECRETSPEC_PROVIDER` or `SECRETSPEC_REASON` still applies to the command you are running, but is never written into Git configuration, so a variable set for one shell session cannot pin the helper to a provider or attribute every later fetch to an unrelated reason. `login` and `logout` honour the exported variables as usual. ## Use a custom manifest [Section titled “Use a custom manifest”](#use-a-custom-manifest) Custom Git helper configuration is available in SecretSpec 0.20+. Pass `--file` when the credential should use declarations from a project or company manifest. In this mode, `--token-secret` is required and `--username-secret` and `--profile` are available: ```toml [project] name = "company-git" revision = "1.0" [profiles.default] GITHUB_TOKEN = { description = "GitHub token for HTTPS authentication" } ``` ```bash $ secretspec set GITHUB_TOKEN --file company-git.toml $ secretspec --file company-git.toml git configure \ --url https://github.com \ --token-secret GITHUB_TOKEN \ --username YOUR_USERNAME ``` The managed helper records the custom manifest’s absolute path and resolved profile. Only a `--file` passed on the command line selects a custom manifest: `secretspec git` ignores an exported `SECRETSPEC_FILE` so that a variable set for a project cannot silently redirect credential configuration. Use ordinary `secretspec set` and `delete` commands with the same file to manage custom credential values; `git login` and `logout` intentionally operate only on the embedded store and reject an explicit `--file`. ## Remove stored values [Section titled “Remove stored values”](#remove-stored-values) `secretspec git logout` is available in SecretSpec 0.20+. Remove the embedded username and password or token for one exact target: ```bash $ secretspec git logout https://github.com ``` This leaves the Git helper configured. Repeat `login` to replace the credential, or use `unconfigure` when Git should stop invoking SecretSpec for that target. If `login` used a provider override, pass the same override to `logout`. ## Remove the configuration [Section titled “Remove the configuration”](#remove-the-configuration) `secretspec git unconfigure` is available in SecretSpec 0.20+. Remove one credential helper from the current repository: ```bash $ secretspec git unconfigure --url https://github.com ``` Remove every Git credential helper that SecretSpec configured in the current repository: ```bash $ secretspec git unconfigure --all ``` Add `--global` to operate on global configuration. Global removal also defaults to **No** and accepts `--yes` for non-interactive use: ```bash $ secretspec git unconfigure --all --global ``` SecretSpec stores generated entries in its own included Git configuration file. Configure and unconfigure never replace existing credential helpers, usernames, or unrelated includes. Removing the final managed credential removes the SecretSpec include and its file. If that file contains anything SecretSpec does not recognize, the command refuses to modify it and asks you to inspect it manually. ## Manual configuration [Section titled “Manual configuration”](#manual-configuration) The Git credential helper is available in SecretSpec 0.20+. The default convenience command is equivalent to registering the embedded helper yourself. In embedded mode, `PASSWORD` and `USERNAME` are stable aliases: the helper maps them to the target-specific secret names used by `git login`. For example: ```bash $ git config --local credential.https://github.com.username YOUR_USERNAME $ git config --local credential.https://github.com.helper \ 'secretspec --url https://github.com --password-secret PASSWORD --username-secret USERNAME' ``` When configuring a path manually, set `useHttpPath` and use the same URL in the helper: ```bash $ git config --local credential.https://github.com/cachix.useHttpPath true $ git config --local credential.https://github.com/cachix.helper \ 'secretspec --url https://github.com/cachix --password-secret PASSWORD --username-secret USERNAME' ``` These entries are not recorded in SecretSpec’s managed file, so `secretspec git unconfigure` does not remove them. Remove manually configured entries with `git config` as well. For SMTP, include the expected username in the helper command and keep transport settings under `sendemail.*`: ```bash $ git config --global credential.smtp://smtp.example.com:587.helper \ "secretspec --url smtp://smtp.example.com:587 --username user@example.com \ --password-secret PASSWORD --username-secret USERNAME" ``` ## Read-only behavior [Section titled “Read-only behavior”](#read-only-behavior) In SecretSpec 0.20+, the helper only answers Git’s `get` operation. It safely ignores automatic `store` and `erase` requests, so a rejected credential cannot delete or overwrite a value in a shared provider. Manage embedded values explicitly with `secretspec git login` and `logout`, or custom-manifest values with `secretspec set` and `delete`. Git can continue to try another configured helper or prompt when SecretSpec has no stored value for the selected target. # Migration > Bring existing secret declarations and values into SecretSpec SecretSpec can discover declarations from supported providers or copy values from another provider. Secret values are never written to `secretspec.toml`. Dotenv files support declaration discovery in every current release. SecretSpec 0.18+ can also discover declarations from age files, AWS Systems Manager Parameter Store, and Bitwarden Password Manager vaults. ## Start a new project from existing secrets [Section titled “Start a new project from existing secrets”](#start-a-new-project-from-existing-secrets) ### From `.env` [Section titled “From .env”](#from-env) When an existing project already has a `.env` file, initialize its manifest from the names in that file: ```bash $ secretspec init --from dotenv://.env ``` This creates declarations only; values are never written to `secretspec.toml`. Review the generated declarations, then copy the values into your configured default provider: ```bash $ secretspec import dotenv://.env ``` ### From another provider [Section titled “From another provider”](#from-another-provider-018) **New in version 0.18** Use `init --from` with any provider that supports declaration discovery. For example, you can discover declarations from an AWS Parameter Store hierarchy: ```bash $ secretspec init \ --from 'awsps://us-east-1?template=/{profile}/{project}/{key}' \ --project payments \ --profile production ``` Discovery creates declarations only; it does not copy secret values into `secretspec.toml`. You can also discover declarations from age files and Bitwarden Password Manager vaults. See the [`init` reference](/reference/cli/#init) for examples and provider-specific options. ## Import into an existing project [Section titled “Import into an existing project”](#import-into-an-existing-project) If `secretspec.toml` already declares the secrets, import their values from the current environment: ```bash $ secretspec import env ``` The source can also be any other provider name or URI. For example, to copy declared values from a 1Password vault: ```bash $ secretspec import onepassword://Development ``` Imports copy values into your configured default provider, or into the system keyring when you have not configured one. They do not overwrite values that are already present there. ## Next steps [Section titled “Next steps”](#next-steps) * Learn how [providers](/concepts/providers/) select the source and destination for secret values * Use [provider references](/concepts/references/) when existing values have provider-native names or addresses # Azure App Configuration Provider > Azure App Configuration integration **New in version 0.20** The [Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/) provider reads and manages ordinary key-values and resolves canonical Azure Key Vault references. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | --------------------------------------------------------------------------------------------------------- | | Provider | `aac` (0.20+) | | URI | `aac://STORE[?OPTIONS]` | | Access | Read, write, delete, and discovery; native references are read-only | | Best for | Azure applications that centralize configuration and Key Vault references | | Authentication | Service principal, Azure CLI, managed identity, workload identity, or App Configuration connection string | | Availability | SecretSpec 0.20+; included in official and default builds (`aac` feature for custom minimal builds) | | Default storage | `secretspec:{project}:{profile}:{key}` with no label | ## Quick start [Section titled “Quick start”](#quick-start) The official SecretSpec CLI includes the AAC provider. These commands assume the store already exists and your signed-in Azure identity has **App Configuration Data Owner** on it: ```bash $ az login $ secretspec set DATABASE_URL --provider aac://payments-production $ secretspec get DATABASE_URL --provider aac://payments-production $ secretspec run --provider aac://payments-production -- your-command ``` Use **App Configuration Data Reader** instead for identities that only run `get`, `check`, or `run`. See [Assign Azure roles](#assign-azure-roles) for the complete setup. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * An [Azure App Configuration store](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-azure-app-configuration-create) * For Entra authentication, [**App Configuration Data Reader**](https://learn.microsoft.com/azure/azure-app-configuration/concept-enable-rbac) for reads or **App Configuration Data Owner** for writes and deletes * For connection-string authentication, a read-only access key for reads or a read-write access key for writes and deletes * **Key Vault Secrets User** on each referenced vault when entries are Key Vault references * SecretSpec 0.20+. Official binaries and default Cargo builds include AAC; custom `--no-default-features` builds must enable `--features aac`. ### Authentication [Section titled “Authentication”](#authentication) Prefer Microsoft Entra ID Use Entra authentication by default, especially managed identity or workload identity for deployed workloads. RBAC avoids distributing App Configuration access keys. Reserve `auth=connection_string` for environments where Entra authentication is unavailable, protect and rotate the connection string as a secret, and [keep App Configuration local authentication disabled](https://learn.microsoft.com/azure/azure-app-configuration/howto-disable-access-key-authentication) when it is not needed. Select authentication with `auth`: * `env` (default): a complete `tenant_id`, `client_id`, and `client_secret` provider-credential triple, with `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` as fallbacks. With none configured, SecretSpec uses the signed-in Azure CLI or Azure Developer CLI session. A partial triple is an error. * `cli`: Azure CLI or Azure Developer CLI only. * `managed_identity`: system-assigned managed identity. * `workload_identity`: Azure workload identity federation. * `connection_string`: the `connection_string` provider credential, falling back to `AZURE_APPCONFIG_CONNECTION_STRING`. This environment variable is a SecretSpec fallback name. The connection string’s `Endpoint` must exactly match the provider URI’s endpoint, so the URI still selects the store and a credential cannot redirect requests to another endpoint. Read-only and read-write access keys can expose direct values and routing metadata across their store permissions; selectors do not narrow those permissions. ### Assign Azure roles [Section titled “Assign Azure roles”](#assign-azure-roles) Azure control-plane roles such as **Reader**, **Contributor**, and **App Configuration Contributor** do not grant Entra-authenticated access to stored key-values. Assign an App Configuration *data-plane* role to the exact user, service principal, workload identity, or managed identity that SecretSpec uses. For a deployed identity, obtain the store resource ID and assign read-only access. Replace the principal object ID and use `User` instead of `ServicePrincipal` when assigning a human account: ```bash $ APP_CONFIG_ID=$(az appconfig show \ --name payments-production \ --resource-group production \ --query id \ --output tsv) $ az role assignment create \ --assignee-object-id "" \ --assignee-principal-type ServicePrincipal \ --role "App Configuration Data Reader" \ --scope "$APP_CONFIG_ID" ``` Change the role to **App Configuration Data Owner** only for an identity that runs `set`, `delete`, cache invalidation, or another write path. Azure role assignments can take several minutes to propagate, so a new assignment may briefly continue returning HTTP 403. When selected entries can be Key Vault references, grant the same runtime identity—or the separate identity selected by `key_vault_auth`—read access to each referenced vault: ```bash $ KEY_VAULT_ID=$(az keyvault show \ --name payments-vault \ --resource-group production \ --query id \ --output tsv) $ az role assignment create \ --assignee-object-id "" \ --assignee-principal-type ServicePrincipal \ --role "Key Vault Secrets User" \ --scope "$KEY_VAULT_ID" ``` SecretSpec never needs Key Vault write or delete permission. See the Azure CLI [`az role assignment create` reference](https://learn.microsoft.com/cli/azure/role/assignment) for other principal types and scopes. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ------------------- | ----------------------------------- | --------------- | | `tenant_id` | `AZURE_TENANT_ID` | 0.20+ | | `client_id` | `AZURE_CLIENT_ID` | 0.20+ | | `client_secret` | `AZURE_CLIENT_SECRET` | 0.20+ | | `connection_string` | `AZURE_APPCONFIG_CONNECTION_STRING` | 0.20+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text aac://STORE[?auth=env|cli|managed_identity|workload_identity|connection_string] [&suffix=DNS_SUFFIX][&audience=TOKEN_AUDIENCE] [&key_vault_auth=inherit|env|cli|managed_identity|workload_identity] [&key_vault_suffix=DNS_SUFFIX] [&label=LABEL][&prefix=PREFIX][&tag=NAME=VALUE]... ``` * `STORE`: a bare store name, which uses `.azconfig.io`, or a complete host. * `suffix`: App Configuration DNS suffix for a bare store name. Do not combine it with a dotted host. * `audience`: Entra token audience. Public Azure defaults to `https://appconfig.azure.com`; non-public hosts require an explicit HTTPS origin. For `env` service-principal or `workload_identity` authentication in a sovereign cloud, also set `AZURE_AUTHORITY_HOST` to that cloud’s Entra authority. For `cli`, select the matching Azure cloud before signing in. `audience` controls the requested token scope; it does not select the Entra authority. * `label`: selects one exact label. Omitting it selects the null label rather than every label. * `prefix`: prepended literally to convention keys. Include any separator the desired key requires. * `tag`: exact `NAME=VALUE` selector. Up to five unique tag names may be repeated in the URI; all must match. * `key_vault_auth`: identity used to resolve Key Vault references. `inherit` uses the App Configuration Entra identity, and omission behaves as `inherit`. Connection-string authentication cannot be inherited because it does not authenticate to Key Vault. * `key_vault_suffix`: allowed Key Vault DNS suffix, defaulting to `vault.azure.net`. Referenced vaults must be direct subdomains of this suffix. ```text aac://payments-production aac://shared?label=production&prefix=payments: aac://shared?tag=app=payments&tag=stage=production aac://shared?auth=connection_string&key_vault_auth=managed_identity aac://store.example.com?audience=https%3A%2F%2Fappconfig.example.com&key_vault_suffix=vault.example.com ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers.app_config] uri = "aac://shared?label=production&prefix=payments:" [providers.app_config.credentials] tenant_id = "keyring" client_id = "keyring" client_secret = "keyring" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["app_config"] } ``` Store the declared credentials, then use the alias: ```bash $ secretspec config provider login app_config $ secretspec run --provider app_config -- deploy ``` ## Storage and selection [Section titled “Storage and selection”](#storage-and-selection) Convention entries use `{prefix}secretspec:{project}:{profile}:{key}`. Project and profile components accept ASCII letters, digits, underscores, and hyphens. Secret keys start with an ASCII letter or underscore and continue with ASCII letters, digits, or underscores; `defaults` is reserved for profile configuration. App Configuration keys cannot contain `%` or be exactly `.` or `..`. Azure App Configuration identifies an entry by its [key and label](https://learn.microsoft.com/azure/azure-app-configuration/concept-key-value). Label, prefix, and tags select values; they do not grant access. Reads require the exact key, configured label, and all tag selectors. New entries receive the configured tags. Updates preserve existing tags, content type, and description. Writes and deletes refuse locked entries, non-matching tags, special content types, and concurrent changes detected through ETags. Azure RBAC applies to the store, not to a SecretSpec prefix, label, or tag route. Use a dedicated App Configuration store when workloads must not be able to list or read one another’s direct values or reference metadata. A narrower provider URI is selection configuration, not permission isolation. ## Use existing key-values [Section titled “Use existing key-values”](#use-existing-key-values) A secret’s [`ref`](/reference/configuration/#secret-references) names an existing App Configuration key through `item`. Other coordinates are rejected. Native references are read-only, even when they point to an ordinary direct value. ```toml [profiles.production] DATABASE_URL = { description = "Database URL", ref = { item = "payments:database-url" }, providers = ["aac://shared?label=production"] } ``` ## Azure Key Vault references [Section titled “Azure Key Vault references”](#azure-key-vault-references) Entries with the [canonical Key Vault-reference content type](https://learn.microsoft.com/azure/azure-app-configuration/use-key-vault-references-dotnet-core) `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` are resolved through their HTTPS Key Vault secret URI. A pinned version in that URI remains pinned; an unversioned URI reads the latest version. Key Vault references are never changed by `set` or `delete`. Feature flags, snapshot references, and other Azure App Configuration special content types are rejected rather than returned as opaque strings. Ordinary content types remain direct values. Connection strings authenticate only App Configuration. When `auth` is `connection_string`, select `key_vault_auth=env`, `cli`, `managed_identity`, or `workload_identity` before resolving Key Vault references. ## CI/CD [Section titled “CI/CD”](#cicd) Prefer workload identity or managed identity for deployed workloads. Grant the runtime **App Configuration Data Reader** and, only when it resolves Key Vault references, **Key Vault Secrets User** on the required vaults: ```bash $ secretspec run \ --provider 'aac://payments-production?auth=workload_identity' \ -- deploy ``` A provisioning or rotation job that calls `set` or `delete` needs **App Configuration Data Owner** instead. Keep that writer identity separate from read-only runtime identities. SecretSpec never needs Key Vault write or delete permission: Key Vault references are read-only. For a stored connection string, route the semantic `connection_string` provider credential through another SecretSpec provider instead of putting the connection string in the URI: secretspec.toml ```toml [providers.app_config_ci] uri = "aac://payments-production?auth=connection_string" [providers.app_config_ci.credentials] connection_string = "keyring" ``` In CI, omit that credential route and set `AZURE_APPCONFIG_CONNECTION_STRING`, the SecretSpec-defined environment fallback for `connection_string`. In either form, the provider URI selects the store and the connection string must name that same endpoint. If selected values may be Key Vault references, add an explicit Entra `key_vault_auth` mode to the URI and provide that identity separately. The App Configuration connection string cannot authenticate to Key Vault. ## Deployment topologies [Section titled “Deployment topologies”](#deployment-topologies) | Topology | App Configuration role | Key Vault role | Boundary | | -------------------------------------------- | --------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | | Direct values, read-only runtime | App Configuration Data Reader | None | Runtime can read direct values and metadata allowed by store RBAC | | Key Vault references, read-only runtime | App Configuration Data Reader | Key Vault Secrets User | App Configuration exposes reference URIs; Key Vault controls resolved values | | SecretSpec-managed direct values | App Configuration Data Owner | None | Writer can create, replace, and delete direct values in management scope | | Mixed direct values and Key Vault references | Reader or Owner, according to operation | Key Vault Secrets User for referenced vaults | App Configuration and Key Vault permissions remain independent | | Dedicated store per trust boundary | Reader or Owner, according to operation | Only when references are used | Azure resource separation enforces isolation that selectors cannot provide | Shared stores reduce resource count but expose direct values, labels, tags, retained revisions, and Key Vault reference URIs to principals with store-level data access. Dedicated stores provide a clearer permission boundary. Prefixes, labels, and tags can organize a shared store, but cannot turn it into separate authorization domains. Choose Key Vault boundaries from runtime identities, ownership, and rotation responsibilities. A separate vault per application or environment is a common starting point when those boundaries differ; sharing a vault can be reasonable when the same principals and policies intentionally govern every secret. One App Configuration route can resolve references across multiple authorized vaults without changing its shape. ## Discovery and caching [Section titled “Discovery and caching”](#discovery-and-caching) `secretspec init --from aac://STORE` discovers convention entries for the active project, profile, prefix, label, and tags. Direct values and Key Vault references are discoverable; unsupported special content types stop discovery. Azure App Configuration (0.20+) can be the authoritative side of a [cached provider route](/concepts/providers/caching/). Use an encrypted cache when direct values or resolved Key Vault values must remain encrypted at rest. SecretSpec caches the resolved value, not a Key Vault reference. A local plaintext cache therefore exposes the resolved secret directly. Using another Azure App Configuration route as the cache writes a logical plaintext/direct value, although Azure encrypts it at rest. App Configuration readers can retrieve that value, and Azure [retains key-value revisions](https://learn.microsoft.com/azure/azure-app-configuration/concept-point-time-snapshot) for a tier-dependent history period after update or deletion. `cache clear` removes the active cache entry; it is not a revision-history purge. Use a distinct store, label, or prefix for the cache; changing only authentication or tags does not create a distinct storage identity. Choose the cache according to the resolved value’s sensitivity. ## Security considerations [Section titled “Security considerations”](#security-considerations) App Configuration readers can see direct values, metadata, retained revisions, and Key Vault reference URIs within their data-plane permissions. Key Vault reference values remain protected by separate Key Vault permissions. Prefer Key Vault references for secrets that should not be exposed to App Configuration readers, and scope both services’ permissions to least privilege. Store management scope is not value provenance. A `ref.item` proves which key SecretSpec requested, not who created or approved its current value. Likewise, a Key Vault reference media type proves only that the entry contains a syntactically valid reference. Convention namespace, label, and tags define SecretSpec’s management scope; they do not prove SecretSpec created every matching direct entry. External tools must not place mutable direct values in that scope unless SecretSpec may adopt, update, and delete them. A principal with **App Configuration Data Owner** or a read-write connection string can replace a direct value or redirect a Key Vault reference to another vault beneath the allowed `key_vault_suffix`. SecretSpec then follows that URI with its configured Key Vault identity. Treat App Configuration writers and read-write access keys as trusted routing administrators, restrict their permissions and distribution, and limit the Key Vault identity to explicitly required vaults and secrets. A versionless reference tracks the latest Key Vault version; pin the 32-character version in the stored URI when review or rollout requires a fixed revision. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### HTTP 403 from App Configuration [Section titled “HTTP 403 from App Configuration”](#http-403-from-app-configuration) Confirm that the identity selected by `auth` has **App Configuration Data Reader** or **App Configuration Data Owner** on the store. Azure control-plane roles do not provide Entra data-plane access. If the assignment is new, wait for role propagation before retrying. With `auth=env`, use `auth=cli` temporarily to prove whether the signed-in developer identity behaves differently from the configured service principal. ### Partial service-principal configuration [Section titled “Partial service-principal configuration”](#partial-service-principal-configuration) `auth=env` accepts all three of `tenant_id`, `client_id`, and `client_secret`, or none of them. If only part of the triple is present across provider credentials and `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET`, SecretSpec fails rather than silently using another identity. Complete the triple, remove all three inputs to allow the Azure CLI/azd fallback, or select `auth=cli` explicitly. ### Connection string rejected [Section titled “Connection string rejected”](#connection-string-rejected) `auth=connection_string` requires the `connection_string` provider credential or `AZURE_APPCONFIG_CONNECTION_STRING`. Its `Endpoint` must exactly match the store selected by the provider URI. Use the connection string from that store, and do not place it directly in the URI. ### Key Vault reference cannot be resolved [Section titled “Key Vault reference cannot be resolved”](#key-vault-reference-cannot-be-resolved) App Configuration and Key Vault authenticate independently. Confirm that the Key Vault identity has **Key Vault Secrets User**, that the vault host is a direct child of `key_vault_suffix`, and that the referenced secret and optional version exist. With App Configuration connection-string authentication, set an explicit Entra mode such as `key_vault_auth=workload_identity` because a connection string cannot authenticate to Key Vault. ### A key is reported as missing [Section titled “A key is reported as missing”](#a-key-is-reported-as-missing) Omitting `label` selects only the null label; it does not search every label. All configured tag selectors must also match. `prefix` applies to SecretSpec convention keys but is not prepended to a native `ref.item`, which names the complete existing App Configuration key. ## Limitations [Section titled “Limitations”](#limitations) * One provider instance resolves references from at most 16 distinct Key Vault hosts. Split larger sets across provider aliases. * Feature flags, snapshot references, and unknown Azure App Configuration special content types are rejected. Only direct values and canonical Key Vault references are resolved. * `init --from` reflects only SecretSpec convention keys for the selected project, profile, prefix, label, and tags. It does not import arbitrary keys from the store; declare those with read-only `ref.item` entries. * Tag selectors require non-empty names and values. Empty values and Azure null-valued tags cannot be expressed as `tag=NAME=VALUE` selectors. * Native `ref.item` entries are read-only. Entries containing Key Vault references cannot be written or deleted through this provider, and SecretSpec never writes or deletes the referenced Key Vault secret. * Provider reads do not create an implicit durable cache, refresh loop, or watch subscription. Configure a SecretSpec cache explicitly and run commands again to observe changes. * No store-wide clear or watch operation is exposed. `secretspec cache clear` applies only to configured SecretSpec cache entries. * SecretSpec honors an App Configuration key’s lock by refusing writes and deletes, but does not create, remove, or manage locks. * The provider does not assign a native TTL. When Azure App Configuration is a cache provider, SecretSpec enforces its logical `max_age` and deletes stale entries when encountered; Azure revision history remains separate. * AAC does not currently apply Azure-specific retry/backoff to HTTP 429 or 5xx responses or configure a provider-specific request deadline. Apply an outer command timeout and retry policy where a deployment requires bounded execution. After an indeterminate write or delete network error, read the current entry before retrying the mutation. # age Provider > Store secrets in an age-encrypted file committed alongside code **New in version 0.17** The [age](https://age-encryption.org) provider keeps secrets in a single age-encrypted file that you can commit to your repository. The plaintext inside is a dotenv-style `KEY=value` blob that SecretSpec encrypts to one or more age recipients and decrypts with your age identity. A read decrypts the blob; a write decrypts it, updates one key, and re-encrypts the whole blob to the current recipients. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------- | | Provider | `age` | | URI | `age://[?options]` | | Access | Read, write, and delete (0.20+) | | Best for | Encrypted secrets committed alongside code | | Authentication | An age identity (private key) | | Build feature | `age` | | Default storage | A dotenv blob at the configured path, keyed by the secret name | ## Quick start [Section titled “Quick start”](#quick-start) Use an age v1.3 hybrid post-quantum key for new setups. SecretSpec’s Rust age library currently accesses this key type through the non-interactive `age-plugin-pq` compatibility plugin: ```bash $ mkdir -p "$HOME/.config/age" $ age-keygen -pq -o "$HOME/.config/age/keys.txt" Public key: age1pq1... $ age-plugin-pq -identity -o "$HOME/.config/age/plugin-identity.txt" "$HOME/.config/age/keys.txt" $ secretspec set DATABASE_URL --provider "age://secrets.age?identity=$HOME/.config/age/plugin-identity.txt" Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to age (profile: default) $ secretspec get DATABASE_URL --provider "age://secrets.age?identity=$HOME/.config/age/plugin-identity.txt" ``` With no recipients configured the blob is encrypted to your own identity, so the same key that reads it also writes it. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * An age identity. For new keys, age’s hybrid ML-KEM-768 + X25519 key generated by `age-keygen -pq` is recommended for post-quantum protection. * `age-plugin-pq` on `PATH` when using the recommended hybrid key with SecretSpec’s current Rust age library * Build with `--features age` ### Identity [Section titled “Identity”](#identity) The private key is resolved from the first of these sources: the `identity` provider credential, the `AGE_IDENTITY` environment variable holding the key material, or `?identity=` naming an identity file. The credential and environment forms carry the key material directly; the URI form names a file on disk. Routing the identity through the credential system lets the age key itself be a managed secret, for example one stored in the system keyring. ### Recipients [Section titled “Recipients”](#recipients) Recipients are age public keys and are never secret, so they are configured rather than supplied as credentials. With no `?recipients-file=`, the blob is encrypted to the public key derived from your own identity. To share the file, point `?recipients-file=` at a roster file listing every recipient. A roster is a plain text file in age’s recipients format: one recipient per line, `#` for comments, blank lines ignored. Recipients may be classic `age1...` keys, hybrid `age1pq1...` keys, native tagged `age1tag...`/`age1tagpq...` recipients, or `ssh-ed25519`/`ssh-rsa` keys. Hybrid `age1pq1...` encryption requires `age-plugin-pq`; tagged recipients are parsed natively before the generic plugin fallback. secrets.age.recipients ```text # alice age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p # a deploy host ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ``` Because an age file does not record its recipients, every write re-encrypts to whatever `?recipients-file=` names at that moment. Keep that file complete and committed so a write never drops a reader. When the roster changes, run a write against each secret to re-encrypt it to the new set. ### Plugins and post-quantum keys [Section titled “Plugins and post-quantum keys”](#plugins-and-post-quantum-keys) Native X25519 and SSH identities are supported directly. Plugin identities and recipients work when their `age-plugin-*` binary is on `PATH` **and the plugin operation is non-interactive**. SecretSpec currently supplies no age callback UI, so plugins that issue `confirm`, `request-public`, or `request-secret` requests can fail. A plugin that handles interaction entirely through its own OS UI may still work. Post-quantum keys need one conversion step. SecretSpec is built on the Rust age library, which does not yet read the native `AGE-SECRET-KEY-PQ-1` identity form that `age-keygen -pq` writes. Convert it to the plugin form once; `-o` creates the new secret identity file with mode `0600` and refuses to overwrite it: ```bash $ age-plugin-pq -identity -o "$HOME/.config/age/plugin-identity.txt" "$HOME/.config/age/keys.txt" ``` Use only post-quantum recipients (`age1pq1...` or `age1tagpq1...`) together in a roster. Age intentionally rejects a mixture of post-quantum and classic recipients, because the classic recipient would remove the file’s post-quantum protection. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ---------- | -------------------- | --------------- | | `identity` | `AGE_IDENTITY` | 0.17+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### Discover declarations [Section titled “Discover declarations”](#discover-declarations-018) **New in version 0.18** SecretSpec 0.18+ can initialize a manifest from the key names already in an age file. Reflection decrypts the file in memory to enumerate its keys but never writes their values to `secretspec.toml`: ```bash $ secretspec init --from "age://secrets.age?identity=$HOME/.config/age/plugin-identity.txt" ``` The provider must have enough identity configuration to open the file. Use `--project` and `--profile` to choose the metadata written to the new manifest. ### URI format [Section titled “URI format”](#uri-format) ```text age://[?key=value&...] ``` * `path`: the encrypted blob file, resolved against the project root when relative * `?identity=`: identity file, used when no credential or `AGE_IDENTITY` is set * `?recipients-file=`: roster of recipient public keys; without it, encrypt to your own identity * `?armor=false`: write a binary blob instead of the default ASCII armor ### URI examples [Section titled “URI examples”](#uri-examples) ```text age://secrets.age age://secrets.age?identity=/home/alice/.config/age/plugin-identity.txt age://secrets.age?recipients-file=secrets.age.recipients age://secrets.age?armor=false ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] team_age = "age://secrets.age?recipients-file=secrets.age.recipients" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["team_age"] } ``` Each developer configures their own identity through the `identity` credential, `AGE_IDENTITY`, or a personal `?identity=`, while the committed roster and blob path stay the same for everyone. ## Storage model [Section titled “Storage model”](#storage-model) Every secret is one `KEY=value` entry inside the blob, keyed by the secret name. Project and profile do not appear in the file; point separate profiles at separate blobs to keep them apart, for example `secrets.prod.age` and `secrets.dev.age`. Starting in SecretSpec 0.20+, this plaintext uses dotenv-ng syntax: `$` remains literal, and values are quoted only when needed to round-trip. Keys may include hyphens, leading digits, leading dots, and Unicode, but not whitespace, `=`, `#`, or control characters. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) names the key to read inside the blob, so a declared secret can map to a differently named entry. The age provider has no sub-address, so a `ref` sets only `item`. secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "POSTGRES_URL" }, providers = ["age://secrets.age"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) Commit the blob and its roster, and give the job an identity to decrypt with. The identity is a natural fit for the `identity` provider credential (sourced from another provider) or the `AGE_IDENTITY` environment variable: ```bash $ export AGE_IDENTITY="$CI_AGE_IDENTITY" $ secretspec run --provider "age://secrets.age" -- deploy ``` ## Security considerations [Section titled “Security considerations”](#security-considerations) A recipient can decrypt every secret in a blob, not individual entries within it. Put secrets that should reach different audiences in separate files, each with its own roster. Hybrid post-quantum recipients protect stored ciphertext against harvest-now/decrypt-later attacks, but static file encryption does not provide forward secrecy. Anyone who later obtains a long-term identity can decrypt historical ciphertext that still exists in Git history or backups. Rotate and erase identities, re-encrypt the blob, and manage repository history according to your retention policy when that risk matters. # Azure Key Vault Provider > Azure Key Vault integration **New in version 0.15** The [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) provider integrates with Azure for centralized secret management. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------------- | | Provider | `akv` | | URI | `akv://VAULT_NAME[?auth=METHOD][&suffix=DNS_SUFFIX]` | | Access | Read and write; secret references are read-only | | Best for | Workloads and teams on Azure | | Authentication | Service principal, Azure CLI, managed identity, or workload identity | | Availability | SecretSpec 0.15+; requires the `akv` build feature | | Default storage | `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider akv://myvault Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to akv (profile: default) # Get it back $ secretspec get DATABASE_URL --provider akv://myvault postgresql://localhost/mydb ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * An Azure Key Vault instance * Authenticated via a service principal, the Azure CLI (`az login`), a managed identity, or AKS workload identity * Build with `--features akv` ### Authentication [Section titled “Authentication”](#authentication) Select an authentication mode with the URI’s `auth` option: * `env` (default): service-principal provider credentials or environment variables, falling back to an Azure CLI session when none are set. * `cli`: Azure CLI or Azure Developer CLI only. * `managed_identity`: system-assigned managed identity. * `workload_identity`: AKS workload identity federation. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | --------------- | --------------------- | --------------- | | `tenant_id` | `AZURE_TENANT_ID` | 0.15+ | | `client_id` | `AZURE_CLIENT_ID` | 0.15+ | | `client_secret` | `AZURE_CLIENT_SECRET` | 0.15+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext akv://VAULT_NAME[?auth=env|cli|managed_identity|workload_identity][&suffix=DNS_SUFFIX] ``` * `VAULT_NAME`: Your Key Vault name (e.g. `myvault`), or a full DNS name for sovereign clouds (e.g. `myvault.vault.azure.cn`) * `auth`: Authentication method (default: `env`) * `env` — a service principal from the `tenant_id`, `client_id`, and `client_secret` provider credentials, with `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET` as fallbacks (all three must be available together); falls back to the signed-in Azure CLI / Azure Developer CLI session if none are available. A partial set is an error rather than a silent fallback to a different identity. * `cli` — the Azure CLI / Azure Developer CLI session only * `managed_identity` — the VM / App Service / AKS system-assigned managed identity * `workload_identity` — AKS workload identity federation (`AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_FEDERATED_TOKEN_FILE`, injected automatically by AKS) * `suffix`: an explicit Key Vault DNS suffix for a bare `VAULT_NAME`, e.g. `akv://myvault?suffix=vault.azure.cn` for a sovereign cloud, instead of relying on a dotted `VAULT_NAME` ### URI examples [Section titled “URI examples”](#uri-examples) ```text akv://myvault akv://myvault?auth=managed_identity akv://myvault?auth=workload_identity akv://myvault?suffix=vault.azure.cn ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] azure = "akv://myvault" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["azure"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Azure Key Vault secret names may only contain ASCII letters, digits and hyphens, and Azure compares object identifiers case-insensitively. SecretSpec stores convention names as `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}`, using lowercase, unpadded Base32 for each component. This encoding is deterministic and injective: names that differ by case, underscores versus hyphens, or leading/trailing hyphens remain distinct even though Key Vault’s identifiers do not preserve all of those distinctions. The encoded components contain no hyphens, so the `--` component separators cannot be confused with component data. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing secret instead: `item` is the secret name (`field` and `version` are both rejected through SecretSpec 0.18). `field` remains unsupported. SecretSpec 0.20+ accepts `version` as a 32-character ASCII alphanumeric Azure Key Vault version identifier; omission reads the latest version. References are **read-only** in this provider, and `item` must already be a valid Azure Key Vault secret name (letters, digits, and hyphens only) — unlike convention secrets, it is validated but never rewritten, since silently rewriting a `ref` could point at a different secret than the one you named. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "database-url", version = "0123456789abcdef0123456789abcdef" }, # version: 0.20+ providers = ["akv://myvault"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ### Service principal [Section titled “Service principal”](#service-principal) The `auth=env` mode accepts `tenant_id`, `client_id`, and `client_secret` as [provider credentials](/reference/provider-credentials/). For example, the credentials can be stored in the system keyring instead of a shell profile: secretspec.toml ```toml [providers.azure] uri = "akv://myvault" [providers.azure.credentials] tenant_id = "keyring" client_id = "keyring" client_secret = "keyring" ``` Store all three declared credentials, then use the alias: ```bash $ secretspec config provider login azure $ secretspec run --provider azure -- deploy ``` When a semantic credential is not explicitly configured, SecretSpec falls back to its matching conventional environment variable: ```bash # Set credentials $ export AZURE_TENANT_ID="..." $ export AZURE_CLIENT_ID="..." $ export AZURE_CLIENT_SECRET="..." # Run command $ secretspec run --provider akv://myvault -- deploy ``` Across provider credentials and environment fallbacks, all three values must be available together. A partial service principal is treated as a configuration error rather than a silent fallback to the Azure CLI session. ### AKS workload identity [Section titled “AKS workload identity”](#aks-workload-identity) ```bash # AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_FEDERATED_TOKEN_FILE are # injected automatically into workload-identity-enabled pods. $ secretspec run --provider akv://myvault?auth=workload_identity -- deploy ``` # AWS Systems Manager Parameter Store Provider > AWS Systems Manager Parameter Store integration **New in version 0.18** The [AWS Systems Manager Parameter Store](https://aws.amazon.com/systems-manager/features/#Parameter_Store) provider stores secrets as encrypted [`SecureString`](https://docs.aws.amazon.com/systems-manager/latest/userguide/secure-string-parameter-kms-encryption.html) parameters. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ------------------------------------------------------------------------- | | Provider | `awsps` (0.18+) | | URI | `awsps://[AWS_PROFILE@]REGION[?options]` | | Access | Read and write; version-, label-, and ARN-pinned references are read-only | | Best for | AWS workloads using Parameter Store for application configuration | | Authentication | Standard AWS SDK credential chain | | Build feature | `awsps` (0.18+) | | Default storage | `/secretspec/{project}/{profile}/{key}`; replaceable with `template` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set an encrypted parameter $ secretspec set DATABASE_URL --provider awsps://us-east-1 Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to awsps (profile: default) # Get it back $ secretspec get DATABASE_URL --provider awsps://us-east-1 postgresql://localhost/mydb # Run with parameters $ secretspec run --provider awsps://us-east-1 -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * An AWS account with Systems Manager Parameter Store enabled * IAM permission to read and write the target parameter hierarchy * Build with `--features awsps` using SecretSpec 0.18+ ### Authentication [Section titled “Authentication”](#authentication) The provider uses the standard AWS SDK credential chain, including: 1. `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optional `AWS_SESSION_TOKEN` 2. Shared AWS config and credentials files 3. IAM Identity Center (SSO) sessions 4. ECS task roles, EC2 instance profiles, and other workload identities Set a shared-config profile before the region in the URI: ```text awsps://production@us-east-1 ``` When the profile and region are omitted, the SDK resolves both from its ordinary environment and shared-config chains: ```text awsps ``` ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text awsps://[AWS_PROFILE@]REGION[?prefix=PREFIX][&template=TEMPLATE][&kms_key_id=KEY][&tier=TIER] ``` * `AWS_PROFILE`: Optional profile from the shared AWS config. * `REGION`: Optional AWS region. If omitted, the SDK region chain is used. * `prefix`: Optional hierarchy before `/secretspec`. A leading slash is optional and normalized. * `template`: Optional complete hierarchy using `{project}`, `{profile}`, and `{key}`. It must start with `/`, contain `{key}` exactly once as the final path segment, and include a parent path before it. `template` and `prefix` are mutually exclusive. * `kms_key_id`: Optional customer-managed KMS key ID, ARN, or alias used for `SecureString` writes. If omitted, Parameter Store uses the account’s default key. * `tier`: Optional `standard`, `advanced`, or `intelligent-tiering` value. Omitting it uses the account’s Parameter Store default tier. ### URI examples [Section titled “URI examples”](#uri-examples) ```text awsps://us-east-1 awsps://production@us-east-1 awsps://us-east-1?prefix=/myteam awsps://us-east-1?template=/{profile}/{project}/{key} awsps://us-east-1?kms_key_id=alias/my-key&tier=advanced awsps:// ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] parameters = "awsps://production@us-east-1?prefix=/myteam&kms_key_id=alias/parameter-store" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["parameters"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Convention secrets are stored at `[/prefix]/secretspec/{project}/{profile}/{key}`. For example, `DATABASE_URL` in project `myapp` and profile `production` maps to: ```text /secretspec/myapp/production/DATABASE_URL ``` With `?prefix=/myteam`, it maps to: ```text /myteam/secretspec/myapp/production/DATABASE_URL ``` Use `template` to replace the whole convention. This is useful when Terraform already provisions a Chamber-style hierarchy. For example, `?template=/{profile}/{project}/{key}` maps the same declaration to: ```text /production/myapp/DATABASE_URL ``` The `{project}` and `{profile}` placeholders are optional, but `{key}` must be the final path segment. That restriction gives discovery a bounded parent path and a reversible mapping from parameter names to SecretSpec declarations. ## Discover existing parameters [Section titled “Discover existing parameters”](#discover-existing-parameters) SecretSpec 0.18+ can create declarations for the direct children of the rendered Parameter Store hierarchy. It calls `GetParametersByPath` without decryption, does not write parameter values to the manifest, and does not scan outside that one path: ```bash $ secretspec init \ --from 'awsps://production@us-east-1?template=/{profile}/{project}/{key}' \ --project payments \ --profile production ✓ Created secretspec.toml with 12 secrets ``` `--project` defaults to the current directory name and `--profile` defaults to `default`. Initialization discovers declarations only. To continue reading the values from Parameter Store, add the URI as a checked-in provider alias and use it as the profile default: secretspec.toml ```toml [providers] parameters = "awsps://production@us-east-1?template=/{profile}/{project}/{key}" [profiles.production.defaults] providers = ["parameters"] ``` Alternatively, `secretspec import` copies the now-declared values from the source into your configured destination provider; it does not perform discovery itself. Every value SecretSpec creates is a `SecureString`. Writes set `Overwrite=true`, so updating a value creates a new Parameter Store version. Parameter Store retains its normal version history. Standard parameters accept values up to 4 KB; advanced parameters accept up to 8 KB and incur AWS charges. A standard parameter can be promoted to advanced, but Parameter Store does not downgrade an advanced parameter in place. Caution SecretSpec does not change an existing parameter from `String` or `StringList` to `SecureString`. Parameter Store rejects that type change; create a new encrypted parameter or migrate the existing parameter first. ## Use existing parameters [Section titled “Use existing parameters”](#use-existing-parameters) In SecretSpec 0.18+, a secret’s [`ref`](/reference/configuration/#secret-references) field can name an existing Parameter Store parameter. `item` is its full name or ARN. The optional `version` is appended as a Parameter Store selector and may be a numeric version or label. An unversioned reference by parameter name is writable; references using a version, label, or ARN are read-only. ```toml [profiles.production] # Latest value DATABASE_URL = { description = "DB", ref = { item = "/prod/database-url" }, providers = ["awsps://us-east-1"] } # Version 7 SIGNING_KEY = { description = "Signing key", ref = { item = "/prod/signing-key", version = "7" }, providers = ["awsps://us-east-1"] } # Version carrying the "current" label API_TOKEN = { description = "API token", ref = { item = "/prod/api-token", version = "current" }, providers = ["awsps://us-east-1"] } # Generate at an existing hierarchy on first use, then reuse it DB_PASSWORD = { description = "DB password", ref = { item = "/prod/db-password" }, type = "password", generate = true, providers = ["awsps://us-east-1"] } ``` `secretspec set`, interactive `check`, generation, and `import` write an unversioned name reference at that exact parameter name. As with convention writes, an existing value receives a new Parameter Store version and is never changed from `String` or `StringList` to `SecureString`. Cross-account shared parameters must be read by ARN. ARN references are read-only; SecretSpec writes only by parameter name in the provider’s configured account and region. ## IAM permissions [Section titled “IAM permissions”](#iam-permissions) Reads require `ssm:GetParameter` and `ssm:GetParameters`; convention and unversioned name-reference writes require `ssm:PutParameter`. A customer-managed KMS key also needs the corresponding KMS permissions for encryption and decryption. Discovery requires `ssm:GetParametersByPath`. It requests encrypted values without decrypting them, while normal secret reads do decrypt. Scope IAM resources to the configured hierarchy where possible. For the `/myteam/secretspec/` prefix in `us-east-1`, that resource pattern is: ```text arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/myteam/secretspec/* ``` ## CI/CD [Section titled “CI/CD”](#cicd) Prefer an AWS workload identity or short-lived OIDC credentials: ```bash $ export AWS_REGION=us-east-1 $ secretspec run --provider awsps -- deploy ``` # AWS Secrets Manager Provider > AWS Secrets Manager integration The [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) provider integrates with AWS for centralized secret management. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ----------------------------------------------- | | Provider | `awssm` | | URI | `awssm://[AWS_PROFILE@]REGION[?options]` | | Access | Read and write; secret references are read-only | | Best for | Workloads and teams on AWS | | Authentication | Standard AWS SDK credential chain | | Build feature | `awssm` | | Default storage | `[prefix/]secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider awssm://us-east-1 Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to awssm (profile: default) # Run with secrets $ secretspec run --provider awssm://us-east-1 -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * AWS account with Secrets Manager access * AWS credentials configured (CLI, environment variables, IAM roles, or SSO) * Build with `--features awssm` ### Authentication [Section titled “Authentication”](#authentication) AWS Secrets Manager uses the standard AWS SDK credential chain: 1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) 2. Shared credentials file (`~/.aws/credentials`) 3. AWS SSO (`aws sso login`) 4. IAM roles (EC2 instance profiles, ECS task roles, Lambda execution roles) ### Required IAM permissions [Section titled “Required IAM permissions”](#required-iam-permissions) For identities used only to read secrets, such as those running `secretspec get`, `secretspec check`, or `secretspec run`, use a read-only policy. Replace the example region and account ID with your own: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "SecretspecBatchFetch", "Effect": "Allow", "Action": "secretsmanager:BatchGetSecretValue", "Resource": "*", "Condition": { "StringEquals": { "aws:RequestedRegion": "us-east-1" } } }, { "Sid": "SecretspecRead", "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretspec/*" } ] } ``` Identities that run `secretspec set` also need this statement in the policy’s `Statement` array: ```json { "Sid": "SecretspecWrite", "Effect": "Allow", "Action": [ "secretsmanager:CreateSecret", "secretsmanager:PutSecretValue" ], "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretspec/*" } ``` If you use a prefix such as `?prefix=myteam`, adjust the secret ARN in the read and write statements: ```plaintext arn:aws:secretsmanager:us-east-1:123456789012:secret:myteam/secretspec/* ``` Note `BatchGetSecretValue` is used automatically during `check` and `run` to fetch secrets in batches of 20 instead of one call each. AWS Secrets Manager [does not support resource-level permissions](https://docs.aws.amazon.com/service-authorization/latest/reference/list_secretsmanager.html) for `BatchGetSecretValue`, so that action must use `"Resource": "*"`. Scoping it to a secret ARN does not grant the permission, and the batch request fails with `AccessDeniedException`. The `aws:RequestedRegion` condition limits the wildcard statement to the configured region. The wildcard does not authorize access to secret contents by itself. AWS also requires `secretsmanager:GetSecretValue` for every secret returned by a batch, and that permission remains scoped to the secret ARN. SecretSpec supplies an explicit list of secret IDs, so the [filter-only `ListSecrets` permission](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_BatchGetSecretValue.html) is not required. Note Using `tag.NAME=VALUE` additionally requires `secretsmanager:TagResource`, and a `kms_key_id` requires `kms:GenerateDataKey` and `kms:Decrypt` on that key. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext awssm://[AWS_PROFILE@]REGION[?prefix=PREFIX][&kms_key_id=KEY][&tag.NAME=VALUE...] ``` * `REGION`: AWS region (e.g., `us-east-1`). If omitted, the SDK default region chain is used. * `AWS_PROFILE`: Optional AWS profile from `~/.aws/credentials`. If omitted, the SDK default credential chain is used. * `PREFIX`: Optional root prefix prepended to all secret names. Useful when IAM policies scope access by prefix (e.g., only allow `myteam/*`). * `kms_key_id`: Optional KMS key (id, ARN, or `alias/...`) used to encrypt secrets that secretspec creates. * `tag.NAME=VALUE`: Optional tags applied to secrets that secretspec creates. Repeat for multiple tags. `kms_key_id` and `tag.NAME=VALUE` are applied **only when secretspec creates a secret** (`CreateSecret`); updating a value (`PutSecretValue`) accepts neither, and a pre-existing secret keeps the key and tags it was created with. This supports AWS “tag-on-create” guardrails, where an SCP or IAM condition denies `CreateSecret` unless required `aws:RequestTag/*` tags (and often a customer-managed key) are present in the same call. ### URI examples [Section titled “URI examples”](#uri-examples) ```text awssm://us-east-1 awssm://production@us-east-1 awssm://us-east-1?prefix=myteam awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform&tag.env=prod awssm ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) Because guardrail tags and keys usually vary per environment, they are a natural fit for a checked-in [provider alias](/reference/configuration/) in `secretspec.toml`: ```toml [providers] prod = "awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform&tag.env=prod" ``` Route secrets through the alias in project configuration: secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["prod"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Secrets are stored as `[prefix/]secretspec/{project}/{profile}/{key}`. For example, `DATABASE_URL` in project `myapp` and profile `production` is stored as `secretspec/myapp/production/DATABASE_URL`. With `?prefix=myteam`, it becomes `myteam/secretspec/myapp/production/DATABASE_URL`. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing secret instead: `item` is the secret name (or ARN), and the optional `field` selects one key of a JSON secret value. Without `field`, the whole secret string is returned. References are **read-only** in this provider. ```toml [profiles.production] # Whole secret value DATABASE_URL = { description = "DB", ref = { item = "prod/database-url" }, providers = ["awssm://us-east-1"] } # One key of a JSON secret value DB_PASSWORD = { description = "DB pw", ref = { item = "prod/db-credentials", field = "password" }, providers = ["awssm://us-east-1"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Using environment variables $ export AWS_ACCESS_KEY_ID=AKIA... $ export AWS_SECRET_ACCESS_KEY=... $ export AWS_DEFAULT_REGION=us-east-1 # Run command $ secretspec run --provider awssm://us-east-1 -- deploy # Or with IAM roles (no credentials needed) $ secretspec run --provider awssm://us-east-1 -- deploy ``` # Bitwarden Password Manager Provider > Bitwarden Password Manager secrets management integration **New in version 0.18** The [Bitwarden Password Manager](https://bitwarden.com/products/) (`bw`) provider reads and writes secrets by using the official `bw` CLI. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | -------------- | ------------------------------------------------------ | | Provider | `bw` | | URI | `bw://[COLLECTION\|ORGANIZATION@COLLECTION][?options]` | | Access | Read and write | | Best for | Existing Bitwarden Password Manager vaults and items | | Authentication | An unlocked `bw` CLI session through `BW_SESSION` | | Build feature | `bw` | ## Quick start [Section titled “Quick start”](#quick-start) Sign in, unlock the vault, and export the session returned by `bw unlock`: ```bash $ bw login $ export BW_SESSION="$(bw unlock --raw)" ``` Then write a secret and use it in a command: ```bash $ secretspec set DATABASE_URL --provider bw:// Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to bw (profile: default) $ secretspec run --provider bw:// -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * Bitwarden CLI (`bw`) * Bitwarden account * For self-hosted servers: the CLI pointed at your server with `bw config server` **before** logging in (see [Self-hosted servers](#self-hosted-servers)) * Signed in via `bw login` and unlocked with `bw unlock` * `BW_SESSION` environment variable set Build SecretSpec with `--features bw` when the provider is not included by your package. ### Authentication [Section titled “Authentication”](#authentication) The provider uses the active `bw` CLI session. Export the session key after unlocking the vault: ```bash $ export BW_SESSION="your-session-key" ``` Before reads and writes, SecretSpec requires the CLI status to be unlocked. If the CLI is signed out or the vault is locked, it reports combined guidance to run `bw login` and `bw unlock`, then set `BW_SESSION`. ### Self-hosted servers [Section titled “Self-hosted servers”](#self-hosted-servers) The `bw` CLI reads its server address from its own configuration file, written by `bw config server`. It does not accept a server through an environment variable or a per-command flag, and it refuses to change servers while a session is active. SecretSpec therefore cannot switch servers for you. Configure the CLI once, before logging in: ```bash $ bw logout # if already logged in $ bw config server https://vault.company.com $ bw login $ bw unlock $ export BW_SESSION="session-key-from-unlock" ``` With the CLI configured, `?server=` records which server the project expects. SecretSpec compares it against the CLI’s current setting before each operation and fails with the commands above when they disagree, instead of silently reading or writing secrets on the wrong server: ```toml # secretspec.toml — documents the expected server for the whole team [providers] company_vault = "bw://?server=https://vault.company.com" ``` Omit `?server=` to accept whatever server the CLI is configured for. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext bw://[collection] bw://[org@collection] bw://?server=https://vault.company.com bw://?type=login&field=password bw://?folder=team/{project}/{profile} # 0.20+ ``` * `collection`: Target collection, by name or by ID * `org@collection`: Organization and collection, each by name or by ID * `type`: Item type to require when matching an existing item and to use when creating a new one (`login`, `card`, `identity`, `sshkey`, or `securenote`) * `field`: Built-in or custom field to read or write * `folder` (0.20+): Convention item-title prefix. Supports `{project}` and `{profile}` and defaults to `secretspec/{project}/{profile}`. This is not a Bitwarden folder: Bitwarden folders are personal to each vault user and therefore cannot provide a shared project namespace. * `server`: The self-hosted server this configuration expects. This does **not** configure the CLI — it is a guard that fails with remediation steps when the `bw` CLI is pointed somewhere else. See [Self-hosted servers](#self-hosted-servers). ### Organizations and collections [Section titled “Organizations and collections”](#organizations-and-collections-018) **New in version 0.18** Names and IDs are interchangeable: SecretSpec resolves a name to the ID the `bw` CLI requires. Names match case-insensitively, and one containing a space must be percent-encoded (`bw://Acme%20Inc@dev-secrets`). The organization is a scope and an assertion rather than a filter. It selects which `dev-secrets` you mean when several organizations have one, and it must agree with the collection you named — addressing a collection that lives somewhere else is an error rather than a silent search of the wrong place. A collection identifies its own organization, so naming the organization is optional whenever the collection name is unambiguous: ```bash $ secretspec get DATABASE_URL --provider "bw://dev-secrets" ``` An address that cannot be resolved fails immediately and lists the organizations or collections that do exist. If a collection was created or shared with you recently, run `bw sync` so the CLI can see it. ### URI examples [Section titled “URI examples”](#uri-examples) ```bash # Password Manager - Personal vault $ secretspec set API_KEY --provider bw:// # Password Manager - Organization collection $ secretspec set DATABASE_URL --provider "bw://myorg@dev-secrets" # Password Manager - Self-hosted instance (CLI must already be configured # for this server; see Self-hosted servers below) $ secretspec set TOKEN --provider "bw://?server=https://vault.company.com" # Password Manager - Specific item type and field $ secretspec get 'MyApp Database' --provider 'bw://?type=login&field=username' ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) Define reusable aliases when a team uses a shared organization or collection: secretspec.toml ```toml [providers] team_vault = "bw://myorg@dev-secrets" [profiles.default.defaults] providers = ["team_vault"] ``` Profiles can select different aliases or the provider directly: secretspec.toml ```toml [profiles.development.defaults] providers = ["bw"] [profiles.production.defaults] providers = ["bw"] ``` ### Discover declarations [Section titled “Discover declarations”](#discover-declarations-018) **New in version 0.18** SecretSpec 0.18+ can initialize a manifest from the items visible through a Bitwarden provider URI. Scope discovery to a collection, and optionally an item type, so unrelated personal or organization-vault entries are not treated as application secrets: ```bash $ secretspec init --from 'bw://myorg@dev-secrets?type=login' # 0.18+ ✓ Created secretspec.toml with 8 secrets ``` In SecretSpec 0.20+, an item under the selected project/profile convention prefix becomes a convention declaration: for example, `secretspec/payments/production/API_KEY` becomes `API_KEY`. Items under another project/profile prefix are skipped. A bare existing item such as `LEGACY_TOKEN` is still discovered, but its declaration receives `ref = { item = "LEGACY_TOKEN" }` so it remains directly addressable. Discovered keys must contain only letters, numbers, and underscores, cannot start with a number, and cannot be the reserved name `defaults`. Bitwarden allows duplicate names and matches them case-insensitively; discovery stops when two selected items map to colliding keys instead of generating an ambiguous manifest. Rename an item or narrow discovery with `?type=`. Discovery never writes secret values. In SecretSpec 0.20+, `--project` and `--profile` select the convention prefix used to recognize managed items; they do not rename anything in Bitwarden. To migrate values after reviewing the declarations, run the `secretspec import` command printed by `init`. ### Environment overrides [Section titled “Environment overrides”](#environment-overrides) Environment variables take precedence over organization, collection, item type, and default field values in the provider URI. A value left in the shell can therefore change an operation even when `--provider` supplies those settings; unset unwanted overrides before running SecretSpec: ```bash $ export BITWARDEN_DEFAULT_TYPE=login $ export BITWARDEN_DEFAULT_FIELD=password $ export BITWARDEN_ORGANIZATION=myorg $ export BITWARDEN_COLLECTION=dev-secrets $ secretspec get DATABASE_PASSWORD --provider bw:// ``` Organization and collection values can be names or IDs and resolve in the same way as values in the URI. The complete precedence is: | Setting | Highest to lowest precedence | | ------------ | ------------------------------------------------------------------------------ | | Organization | `BITWARDEN_ORGANIZATION`, provider URI | | Collection | `BITWARDEN_COLLECTION`, provider URI | | Item type | `BITWARDEN_DEFAULT_TYPE`, provider URI | | Field | Secret `ref.field`, `BITWARDEN_DEFAULT_FIELD`, provider URI, item-type default | ## Storage model [Section titled “Storage model”](#storage-model) ### Convention item names [Section titled “Convention item names”](#convention-item-names-020) **Changed in version 0.20** Convention-managed items are now isolated by project and profile. SecretSpec stores `DATABASE_URL` under an item title such as `secretspec/my-project/default/DATABASE_URL`; releases through 0.19 used the bare title `DATABASE_URL`. See [Migrating bare item names](#migrating-bare-item-names-020). SecretSpec-managed convention items use the title `secretspec/{project}/{profile}/{key}` by default. Project and profile are part of the title so the same collection or personal vault can safely hold `DATABASE_URL` for several projects and environments. `?folder=` replaces the `secretspec/{project}/{profile}` prefix, and the key is appended to it. The prefix is an item-title namespace, not a Bitwarden `folderId`. Explicit `ref.item` coordinates always name the complete existing item title and do not receive the prefix. The Bitwarden provider supports every Password Manager item type. When an item type is selected through `BITWARDEN_DEFAULT_TYPE` or `?type=`, it filters reads and updates to that type and selects the type of a newly created item. If neither is set, reads and updates accept any matching type, while new items are Logins. ### Item types [Section titled “Item types”](#item-types) #### Login items [Section titled “Login items”](#login-items) ```bash # Get password field (default) $ secretspec get 'Database Login' --provider 'bw://?type=login' # Get username field $ secretspec get 'Database Login' --provider 'bw://?type=login&field=username' # Get custom field $ secretspec get 'API Service' --provider 'bw://?type=login&field=api_key' ``` #### Credit card items [Section titled “Credit card items”](#credit-card-items) ```bash # Get API key from custom field (field required) $ secretspec get 'Stripe Payment' --provider 'bw://?type=card&field=api_key' # Get card number $ secretspec get 'Company Card' --provider 'bw://?type=card&field=number' ``` #### SSH key items [Section titled “SSH key items”](#ssh-key-items) ```bash # Get private key (default) $ secretspec get 'Deploy Key' --provider 'bw://?type=sshkey' # Get passphrase $ secretspec get 'Deploy Key' --provider 'bw://?type=sshkey&field=passphrase' ``` Bitwarden requires an SSH key item to carry all three of the private key, public key and fingerprint — it rejects or discards an item that leaves any of them empty. When `set` creates one, the two fields it is not writing are therefore filled with `(not set by SecretSpec)`. Replace them in Bitwarden if you need the real values, or write them yourself with `?field=public_key` and `?field=key_fingerprint`. #### Identity items [Section titled “Identity items”](#identity-items) ```bash # Get custom field (field required) $ secretspec get 'Employee Record' --provider 'bw://?type=identity&field=employee_id' # Get email field $ secretspec get 'Personal Identity' --provider 'bw://?type=identity&field=email' ``` #### Secure note items [Section titled “Secure note items”](#secure-note-items) ```bash # Get value from secure note $ secretspec get 'Legacy Config' --provider 'bw://?type=securenote&field=config_value' ``` ### Default fields [Section titled “Default fields”](#default-fields) When no field is named, each item type uses the default below. The same default applies to reads and writes, so `secretspec set` followed by `secretspec get` returns what was written. | Item Type | Default field | Read also falls back to | | ----------- | -------------------- | --------------------------------------- | | Login | `password` | `username`, then a custom `value` field | | Secure Note | custom `value` field | the note body | | Card | `number` | a custom `value` field | | Identity | `email` | `username`, then a custom `value` field | | SSH Key | `private_key` | a custom `value` field | The default depends only on the item type, never on the secret or item name. The extra read fallbacks exist to make existing, hand-created vault items resolve; writes always target the default field itself. To address anything else, name the field explicitly with `?field=` or a `ref` mapping: ```toml [profiles.default] STRIPE_KEY = { description = "Card custom field", ref = { item = "Stripe Test Card", field = "api_key" } } DEPLOY_PUBKEY = { description = "SSH public key", ref = { item = "Deploy SSH Key", field = "public_key" } } ``` Built-in field names and aliases resolve only to that built-in field. Custom field names first match in full, case-insensitively. If there is no exact match, SecretSpec uses the first custom field whose name contains the requested text, also case-insensitively. Use the complete custom-field name to avoid an unintended partial match. `field = "notes"` addresses a Secure Note’s body. ### How items are matched [Section titled “How items are matched”](#how-items-are-matched-018) **New in version 0.18** Resolved item titles are matched **in full, case-insensitively** — `test database` finds `Test Database`, but `API_KEY` never matches `API_KEY_OLD`. The `bw` CLI itself accepts a substring here, which works well interactively because it prints the candidates and lets you choose; a name in `secretspec.toml` is resolved with nobody watching, so a partial match would quietly read — or overwrite — a neighbouring item. Bitwarden does not require names to be unique. When more than one item matches, SecretSpec refuses the address and lists the colliding IDs rather than picking one. Rename the items so the selected name is unique, or use `?type=` when the collisions have different item types. Adding `?type=` narrows the match to that item type, on both reads and writes. That is how a Card and a Login of the same name stay separately addressable: ```bash $ secretspec get API_KEY --provider "bw://?type=card" ``` ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) Use a `ref` for an existing Bitwarden item that does not use SecretSpec’s project/profile title convention, when the SecretSpec key differs from the item title, or when a specific field is required: secretspec.toml ```toml [profiles.default] DATABASE_URL = { description = "Application database", ref = { item = "MyApp Database", field = "password" }, providers = ["bw"] } ``` `ref.item` is matched against the Bitwarden item name, not its item ID. ### Migrating bare item names [Section titled “Migrating bare item names”](#migrating-bare-item-names-020) **Changed in version 0.20** Convention-managed Bitwarden items now use project- and profile-qualified titles. Version 0.20 does not fall back to bare item names. Releases through 0.19 wrote convention secrets under their bare keys. Those titles contain no project or profile ownership, so SecretSpec cannot safely guess which project should inherit one. Version 0.20 therefore does not fall back to a bare item during convention reads or writes. Rename a SecretSpec-managed item from, for example, `DATABASE_URL` to `secretspec/my-project/default/DATABASE_URL`. If an item is intentionally shared or externally managed, keep its existing title and declare it explicitly: secretspec.toml ```toml [profiles.default] DATABASE_URL = { description = "Shared database", ref = { item = "DATABASE_URL" }, providers = ["bw"] } ``` `secretspec init --from bw://` in 0.20+ generates this native `ref` form for bare existing items automatically. ## CI/CD [Section titled “CI/CD”](#cicd) Provide an unlocked session to the job as `BW_SESSION`, then select the provider as usual: ```bash $ export BW_SESSION="session-key-from-unlock" $ secretspec run --provider bw:// -- deploy ``` Treat the session key as a CI secret and avoid printing it in job logs. ## Security considerations [Section titled “Security considerations”](#security-considerations) * `BW_SESSION` unlocks the vault for the lifetime of the session, and the session can access everything granted to the signed-in account. Keep the session key out of checked-in configuration and shell history. * Scope provider URIs to the intended organization and collection when possible. * For self-hosted installations, use `?server=` as a guard against operating on a differently configured vault. * Ambiguous item names fail instead of selecting one silently; rename them or use `?type=` when the duplicates have different item types. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### CLI installation [Section titled “CLI installation”](#cli-installation) ```plaintext Bitwarden CLI (bw) is not installed. To install it: - npm: npm install -g @bitwarden/cli - Homebrew: brew install bitwarden-cli - Download: https://bitwarden.com/help/cli/ ``` ### Server mismatch [Section titled “Server mismatch”](#server-mismatch) When `?server=` names a different server than the one the `bw` CLI is configured for, the operation stops before touching the vault and reports both addresses alongside the `bw logout` / `bw config server` / `bw login` / `bw unlock` sequence needed to correct it. # Bitwarden Secrets Manager Provider > Bitwarden Secrets Manager integration Why SecretSpec 0.17+ uses the CLI Bitwarden publishes a Rust SDK, but its [SDK license](https://github.com/bitwarden/sdk-sm/blob/main/LICENSE) does not permit a compatible application built with it to be offered, licensed, or sold to third parties, and it prohibits redistribution of the SDK. Those terms mean SecretSpec cannot link the SDK while remaining a distributable open-source crate and CLI. SecretSpec therefore invokes an independently installed official `bws` executable. This process boundary lets users opt into Bitwarden’s software under Bitwarden’s terms without embedding or redistributing its SDK in SecretSpec. This is unfortunate: a normal Rust library integration would be faster, simpler to install, and would not need to pass new secret values as process arguments. If Bitwarden provides SDK terms that allow third-party redistribution, we would prefer to use the SDK directly. The [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/) (BWS) provider integrates with Bitwarden for centralized, end-to-end encrypted secret management. SecretSpec 0.17 and later invoke the separately installed official `bws` CLI instead of linking the Bitwarden SDK. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------------- | | Provider | `bws` | | URI | `bws://[SERVER_BASE@]PROJECT_UUID` | | Access | Read and write | | Best for | Machine and CI/CD secrets managed in Bitwarden | | Authentication | Machine-account access token; official `bws` CLI in SecretSpec 0.17+ | | Build feature | `bws` | | Default storage | Flat key names in the selected BWS project | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider bws://a9230ec4-5507-4870-b8b5-b3f500587e4c Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to bws (profile: default) # Run with secrets $ secretspec run --provider bws://a9230ec4-5507-4870-b8b5-b3f500587e4c -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * Bitwarden Secrets Manager subscription * SecretSpec 0.17+: official [`bws` CLI](https://bitwarden.com/help/secrets-manager-cli/) 0.3.0 or later installed and available on `PATH` * Machine account access token (`BWS_ACCESS_TOKEN` environment variable) * Build with `--features bws` Set `SECRETSPEC_BWS_CLI_PATH` to the executable path if `bws` is not on `PATH` (SecretSpec 0.17+). ### Authentication [Section titled “Authentication”](#authentication) Generate a machine account access token from the Bitwarden Secrets Manager web interface. In SecretSpec 0.15 and later, you can declare the access token as a [provider credential](/reference/provider-credentials/), for example to store it in your system keyring so it never lives in a shell profile: secretspec.toml ```toml [providers] bitwarden = { uri = "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c", credentials = { access_token = "keyring" } } ``` When no explicit `access_token` credential is supplied, the provider falls back to `BWS_ACCESS_TOKEN`: ```bash $ export BWS_ACCESS_TOKEN="0.your-access-token..." ``` SecretSpec 0.14 supports only the environment-variable form. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | -------------- | -------------------- | --------------- | | `access_token` | `BWS_ACCESS_TOKEN` | 0.15+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext bws://[SERVER_BASE@]PROJECT_UUID ``` * `PROJECT_UUID`: Your Bitwarden Secrets Manager project UUID * `SERVER_BASE` (optional): Hostname of the Bitwarden instance for EU cloud or self hosted deployments. Defaults to `vault.bitwarden.com` (US cloud) when omitted. In SecretSpec 0.17 and later, `SERVER_BASE` is passed to each CLI invocation as `--server-url https://SERVER_BASE`, overriding the CLI’s saved server configuration. Use the web vault hostname here, for example `vault.bitwarden.eu` for the EU cloud. Only a bare hostname is supported (no scheme prefix or custom port). SecretSpec 0.16 and earlier configured the same derived `/identity` and `/api` endpoints directly through the SDK. ### URI examples [Section titled “URI examples”](#uri-examples) ```text bws://a9230ec4-5507-4870-b8b5-b3f500587e4c bws://vault.bitwarden.eu@a9230ec4-5507-4870-b8b5-b3f500587e4c bws://bw.example.com@a9230ec4-5507-4870-b8b5-b3f500587e4c ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] bitwarden = { uri = "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c", credentials = { access_token = "keyring" } } [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["bitwarden"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) SecretSpec uses flat key names matching the secret key directly, such as `DATABASE_URL`. The BWS project UUID provides namespace isolation, so use separate BWS projects when applications or environments need separate values. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names a different key instead: `item` is the BWS key name (`field` is not supported). Reads and writes target that key in place. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "prod-db-connection" }, providers = ["bws://a9230ec4-5507-4870-b8b5-b3f500587e4c"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Set access token (from CI secrets) $ export BWS_ACCESS_TOKEN="$BWS_TOKEN" # Run command $ secretspec run --provider bws://a9230ec4-5507-4870-b8b5-b3f500587e4c -- deploy ``` ## Security considerations [Section titled “Security considerations”](#security-considerations) SecretSpec passes the access token to `bws` through `BWS_ACCESS_TOKEN`, not the CLI’s `--access-token` argument. The official CLI requires new or updated secret values as command-line arguments, however, so during `secretspec set` the value may briefly be visible to process-inspection tools available to the same user. This applies to the CLI-backed provider in SecretSpec 0.17 and later. # Cloudflare Secrets Store provider > Publish SecretSpec values to Cloudflare account-level Secrets Store **New in version 0.20** The [Cloudflare](https://www.cloudflare.com/) provider publishes declared values to an account-level [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/) through the Cloudflare REST API. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ---------------------------------------------------------------------------------------------------------- | | Provider | `cloudflare` (0.20+) | | URI | `cloudflare://STORE_ID[?account_id=ACCOUNT_ID][&OPTIONS]` | | Access | Write, delete, and discover names; plaintext values cannot be read back | | Best for | Publishing secrets to Workers and other Cloudflare services from a separate source of truth | | Authentication | API token or credentials from `wrangler auth token --json` | | Availability | SecretSpec 0.20+; included in official and default builds (`cloudflare` feature for custom minimal builds) | | Default storage | Account secret named `{key}` in the selected store | ## Quick start [Section titled “Quick start”](#quick-start) Find the account ID and Secrets Store ID in the Cloudflare dashboard or with Wrangler, then authenticate and configure an alias: ```bash $ wrangler login $ wrangler secrets-store store list --remote ``` secretspec.toml ```toml [providers] cloudflare_prod = "cloudflare://0123456789abcdef0123456789abcdef?account_id=abcdef0123456789abcdef0123456789&auth=wrangler" [profiles.production] DATABASE_URL = { description = "Production database URL" } ``` ```bash # Publish or replace the account secret $ secretspec set DATABASE_URL --profile production --provider cloudflare_prod # Remove it $ secretspec delete DATABASE_URL --profile production --provider cloudflare_prod ``` Cloudflare never returns plaintext through its management API, so `secretspec get`, `check`, and `run` cannot resolve a value from this provider. Keep the authoritative value in a readable provider and select `cloudflare_prod` explicitly when publishing it. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites-020) **New in version 0.20** * SecretSpec 0.20 or newer * A Cloudflare account with a Secrets Store * Account **Secrets Store Write** permission for publishing and deletion * The account ID and Secrets Store ID The official SecretSpec CLI includes this provider. Custom minimal Rust builds enable it with `--features cloudflare`. ### Wrangler authentication [Section titled “Wrangler authentication”](#wrangler-authentication-020) **New in version 0.20** With `auth=wrangler`, SecretSpec runs: ```bash $ wrangler auth token --json ``` Wrangler can return an API token, a refreshed OAuth token from `wrangler login`, or legacy API-key/email credentials. SecretSpec uses the returned credential only in HTTPS request headers. It never passes the account secret value to Wrangler. Wrangler supports named authentication profiles: ```text cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=wrangler&wrangler_profile=production ``` If the executable has another name or location, set `SECRETSPEC_WRANGLER_PATH`. SecretSpec never invokes `npx` automatically. ### Authentication with provider credentials [Section titled “Authentication with provider credentials”](#authentication-with-provider-credentials-020) **New in version 0.20** For CI or a machine identity, declare the `api_token` [provider credential](/reference/provider-credentials/): secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.cloudflare_prod] uri = "cloudflare://0123456789abcdef0123456789abcdef?account_id=abcdef0123456789abcdef0123456789&auth=token" credentials = { api_token = "bootstrap" } ``` Store it once: ```bash $ secretspec config provider login cloudflare_prod Enter api_token for provider 'cloudflare_prod' (source: bootstrap): **** ``` Use a scoped user or account API token with **Secrets Store Write** on only the required account. Do not use the full-access Global API Key for new setups. ### Environment fallback [Section titled “Environment fallback”](#environment-fallback-020) **New in version 0.20** `CLOUDFLARE_API_TOKEN` supplies the `api_token` credential when no explicit provider credential exists. `CLOUDFLARE_ACCOUNT_ID` supplies the account ID when the URI omits `account_id`. The default `auth=auto` uses the provider credential or `CLOUDFLARE_API_TOKEN` first, then falls back to Wrangler. Use `auth=token` to require a token or `auth=wrangler` to require Wrangler credentials. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ----------- | ---------------------- | --------------- | | `api_token` | `CLOUDFLARE_API_TOKEN` | 0.20+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format-020) **New in version 0.20** ```text cloudflare://STORE_ID[?account_id=ACCOUNT_ID][&scopes=LIST][&auth=MODE][&wrangler_profile=NAME] ``` * `STORE_ID` is required and selects the account-level Secrets Store. * `account_id` selects the Cloudflare account and falls back to `CLOUDFLARE_ACCOUNT_ID`. * `scopes` is a comma-separated list applied when a secret is created or replaced. It defaults to `workers`. Supported values are `workers`, `ai_gateway`, `dex`, `access`, `containers`, and `websearch`. * `auth` is `auto` (default), `token`, or `wrangler`. * `wrangler_profile` selects a named Wrangler auth profile and requires `auth=wrangler`. ### URI examples [Section titled “URI examples”](#uri-examples-020) **New in version 0.20** ```text cloudflare://STORE_ID?account_id=ACCOUNT_ID cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=token cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=wrangler cloudflare://STORE_ID?account_id=ACCOUNT_ID&scopes=workers,containers cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=wrangler&wrangler_profile=production ``` ## Storage model [Section titled “Storage model”](#storage-model-020) **New in version 0.20** The provider maps a declaration key directly to an account-secret name. For example, `DATABASE_URL` maps to: ```text account: ACCOUNT_ID store: STORE_ID secret: DATABASE_URL ``` Project and profile names are not added to the secret name. The store selected by the provider alias supplies isolation. Use a different alias and store when two profiles must hold different values for the same key. Cloudflare Workers can bind that account secret to any binding name; the SecretSpec key does not need to match the Worker’s binding variable. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets-020) **New in version 0.20** A [`ref`](/reference/configuration/#secret-references) changes the Cloudflare secret name updated or deleted by SecretSpec: secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "Production database URL", ref = { item = "PRIMARY_DATABASE_URL" } } ``` The reference remains write-only: it can select an existing name but cannot retrieve its plaintext value. ## Discover secret names [Section titled “Discover secret names”](#discover-secret-names-020) **New in version 0.20** Cloudflare’s list API exposes names, IDs, scopes, comments, and status without returning values. SecretSpec uses that metadata for declaration discovery: ```bash $ secretspec init \ --from 'cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=wrangler' \ --project my-app --profile production ``` The generated manifest contains required declarations for active or pending secret names, not defaults or values. ## CI/CD [Section titled “CI/CD”](#cicd-020) **New in version 0.20** Use a short-lived or account-owned token scoped to Secrets Store Write: ```yaml - run: secretspec set DATABASE_URL --profile production --provider cloudflare_prod env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} ``` The account ID and store ID are attribution, not credentials, and can stay in the checked-in provider URI. ## Security considerations and limitations [Section titled “Security considerations and limitations”](#security-considerations-and-limitations-020) **New in version 0.20** * Cloudflare’s management API accepts values for creation and replacement but never returns them. Plaintext access exists only inside a Cloudflare service with a Secrets Store binding. This provider therefore cannot support `get`, `check`, `run`, fallback reads, generation-on-miss, prompting-on-miss, or value comparisons. * Secret values are serialized directly into an HTTPS request body. They do not appear in the provider URI, command arguments, Wrangler input, or SecretSpec diagnostics. * HTTP redirects are rejected so credentials and secret-bearing request bodies remain confined to Cloudflare’s API origin. * `secretspec set` lists metadata to resolve an existing name to the secret ID, then creates or patches it. `secretspec delete` uses the same metadata lookup and remains idempotent when the name is absent. * A replacement applies the `scopes` configured in the provider URI. Review those scopes because changing them affects which Cloudflare services may bind the secret. * Secret values cannot exceed Cloudflare’s 65,536-byte limit. * Cloudflare Secrets Store is currently a beta service; API behavior and scope availability may change upstream. # Dashlane Provider > Read-only access to a Dashlane vault through the Dashlane CLI **New in version 0.18** The [Dashlane](https://www.dashlane.com/) provider reads secrets from a Dashlane vault through the [Dashlane CLI](https://cli.dashlane.com/) (`dcli`). It is a **read-only** provider: `dcli` can list and read vault items but has no command that creates or edits one, so items are authored in a Dashlane app and read from here. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------- | | Provider | `dashlane` | | URI | `dashlane://[ITEM_TYPE]` | | Access | Read-only | | Best for | Teams already keeping developer secrets in Dashlane | | Authentication | `dcli` device registration, or `DASHLANE_SERVICE_DEVICE_KEYS` | | Network | Local reads; `dcli` re-syncs when its copy is over an hour old | | Default storage | Item titled `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # In a Dashlane app, create a secure note titled: # secretspec/myproject/default/DATABASE_URL # with the connection string as its content, then sync the CLI: $ dcli sync # Check secrets are available $ secretspec check --provider dashlane ✓ All required secrets are configured # Run with secrets $ secretspec run --provider dashlane -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) Install the Dashlane CLI: ```bash # macOS $ brew install dashlane/tap/dashlane-cli # Linux: download the dcli-linux-x64 binary from # https://github.com/Dashlane/dashlane-cli/releases ``` ### Authentication [Section titled “Authentication”](#authentication) Run `dcli sync` once to register this device. It prompts for your email and a second factor (email code, TOTP, or Duo push), then for your master password. Supported primary methods are master password and self-hosted SSO; password-less authentication is not supported by `dcli`. By default the master password is saved in the OS keychain. `dcli lock` locks the vault again, and `dcli logout` clears both the local database and the keychain entry. SecretSpec checks `dcli status` before reading and reports an unregistered device or a locked vault. It never answers a `dcli` prompt — reads run with stdin closed, so an unauthenticated CLI fails immediately rather than leaving `secretspec run` waiting on a password. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | --------------------- | ------------------------------ | --------------- | | `service_device_keys` | `DASHLANE_SERVICE_DEVICE_KEYS` | 0.18+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext dashlane://[ITEM_TYPE] ``` `ITEM_TYPE` restricts the search to one Dashlane content type: `secret`, `note`, or `password` (`login` is accepted as an alias for `password`). Omit it to search secrets, then logins, then notes — the order `dcli read` resolves a name in, so a title held by both a login and a note resolves the same way here as it does through `dcli`. Pinning the type is worth doing when you know where your secrets live: each content type searched costs one `dcli` invocation. ### URI examples [Section titled “URI examples”](#uri-examples) ```plaintext dashlane:// dashlane://note dashlane://secret dashlane://password ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] vault = "dashlane://note" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["vault"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) A convention secret reads the vault item **titled** `secretspec/{project}/{profile}/{key}` — for example `secretspec/myproject/production/DATABASE_URL`. Projects and profiles stay isolated because the title carries both. The value comes from the item’s default field: `content` for a secret or a secure note, `password` for a login. Titles are matched exactly, case-insensitively. Because Dashlane does not enforce unique titles, SecretSpec refuses a title that matches more than one item instead of picking one; point the secret at a single item with a `ref` naming its identifier to resolve the collision. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) To read an item you already have, name it with a `ref`: secretspec.toml ```toml [profiles.default] # By title GITHUB_TOKEN = { description = "GitHub token", ref = { item = "GitHub personal access token" }, providers = ["dashlane"] } # By identifier — stable across renames, and never ambiguous STRIPE_KEY = { description = "Stripe key", ref = { item = "D47734C4-0ABE-423A-8633-6B9F10A38905" }, providers = ["dashlane"] } # A named field of a login DB_USER = { description = "Database user", ref = { item = "Production database", field = "login" }, providers = ["dashlane://password"] } ``` Find an item’s identifier by listing its content type — `dcli secret`, `dcli note`, or `dcli password` with `-o json`. The `id` is emitted wrapped in braces; both forms work here. `ref` supports the `item` and `field` coordinates. Dashlane has no vaults or sections, so those coordinates are rejected rather than ignored. ## CI/CD [Section titled “CI/CD”](#cicd) Register a non-interactive device from a workstation: ```bash $ dcli devices register "ci-runner" ``` This prints device credentials once. Store them and expose them to the runner as `DASHLANE_SERVICE_DEVICE_KEYS`: .github/workflows/ci.yml ```yaml - run: secretspec run --provider dashlane -- npm test env: DASHLANE_SERVICE_DEVICE_KEYS: ${{ secrets.DASHLANE_SERVICE_DEVICE_KEYS }} ``` The credentials can also be sourced from another provider: secretspec.toml ```toml [providers] dashlane_ci = { uri = "dashlane://note", credentials = { service_device_keys = "keyring" } } ``` Dashlane recommends a dedicated account for non-interactive devices. OTP at every login and SSO are not supported for them. ## Troubleshooting and limitations [Section titled “Troubleshooting and limitations”](#troubleshooting-and-limitations) **`secretspec set` fails.** Add or edit the item in a Dashlane app, run `dcli sync`, then read it here. Give the secret a writable provider as well if you need to set values from SecretSpec. **A new item is invisible until the CLI syncs.** `dcli` reads a local copy of the vault. SecretSpec never asks it to sync, but `dcli` syncs itself whenever its last sync is over an hour old, so a read is usually local and occasionally a network round-trip. Run `dcli sync` after adding an item rather than waiting for that. On a device you logged in yourself, `dcli configure disable-auto-sync true` keeps reads offline; with `DASHLANE_SERVICE_DEVICE_KEYS` it does not, for the reason below. **Injected credentials read from their own `dcli` state.** `dcli` prefers a device it has already registered over `DASHLANE_SERVICE_DEVICE_KEYS`, so with credentials set SecretSpec points it at a private state directory of its own, under the SecretSpec cache and named after a hash of the credentials. Without that, a machine already logged in — or a second alias carrying different credentials — would silently read the wrong vault. Two consequences: that state starts empty, so its first read syncs; and `dcli configure disable-auto-sync true`, which records the setting against the device in whichever state directory it is run from, does not carry over to it. Reads for injected credentials therefore sync on `dcli`’s hourly schedule, and there is no supported way to hold them offline. **Only three content types are readable.** `dcli` exposes listers for secrets, secure notes, and logins. Passkeys, personal info, payments, and IDs have no CLI surface and cannot be read. **Secrets need a Business plan.** The `secret` content type is not available on personal plans, where `dashlane://secret` finds nothing. Use secure notes there. ## Security considerations [Section titled “Security considerations”](#security-considerations) Reads decrypt the local vault in a `dcli` subprocess and pass the value back over a pipe; SecretSpec never writes it to disk or to a log. A read can reach the network, because `dcli` re-syncs when its copy is over an hour stale. `dcli password` copies a password to the system clipboard when it is run without an output format, so SecretSpec always requests JSON explicitly. The private state directory those credentials get is created owner-only (`0700` on Unix) before `dcli` runs, because the database inside it holds the registered device and the synced vault, and `dcli` would otherwise leave both readable by every user on the machine. If SecretSpec cannot create it, the read fails rather than falling back to the shared `dcli` state. `DASHLANE_SERVICE_DEVICE_KEYS` grants full read access to the vault. Treat it as highly sensitive; Dashlane prefixes it with `dls_` so secret scanners can recognize it. # Dotenv Provider > Traditional .env file storage for secrets Caution We do not recommend the Dotenv provider for new projects. Use it only for legacy workflows that still require `.env` compatibility. Read [Where .env Went Wrong](/blog/where-env-went-wrong/) to learn why. The [Dotenv](https://github.com/cachix/dotenv-ng) provider stores secrets in local `.env` files for development setups and compatibility with existing tools. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ----------------------------------------------------------- | | Provider | `dotenv` | | URI | `dotenv[:path]` | | Access | Read and write | | Best for | Local development and compatibility with `.env`-based tools | | Authentication | None | | Default storage | `.env` next to `secretspec.toml` (plain text) | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Initialize from existing .env $ secretspec init --from .env # Set a secret $ secretspec set DATABASE_URL --provider dotenv Enter value for DATABASE_URL: postgresql://localhost/mydb # Run with secrets $ secretspec run --provider dotenv -- npm start ``` ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text # Default (.env next to secretspec.toml) dotenv # Custom paths dotenv:.env.local dotenv:config/.env dotenv:/absolute/path/.env # Home-relative path (0.18+) dotenv:~/.config/my-project/.env ``` Starting in SecretSpec 0.18, a leading `~` path component expands to the current user’s home directory. ### Environment variable [Section titled “Environment variable”](#environment-variable) ```bash $ export SECRETSPEC_PROVIDER=dotenv:.env.local ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] local = "dotenv:.env.local" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["local"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) **Changed in version 0.20** Dotenv parsing and rendering use dotenv-ng syntax. Dollar signs and expressions such as `$TOKEN` and `${TOKEN}` are literal; the provider does not substitute them from the process environment. When writing, SecretSpec leaves values unquoted when they already round-trip and otherwise double-quotes and escapes them. Keys may include hyphens, leading digits, leading dots, and Unicode, but not whitespace, `=`, `#`, or control characters. Dotenv uses standard `KEY=VALUE` pairs: .env ```dotenv DATABASE_URL=postgresql://localhost/mydb API_KEY=sk-1234567890 DEBUG=true # Comments supported # Multi-line values must be quoted PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA... -----END RSA PRIVATE KEY-----" ``` The file itself provides the namespace. Project and profile names are not included in keys; use a different file when environments need separate values: ```bash $ secretspec run --provider dotenv:.env.production -- node server.js ``` ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) By default each secret reads the key named after it. A secret’s [`ref`](/reference/configuration/#secret-references) field reads a key stored under a different name: `item` is the `.env` key (`field` is not supported). Reads and writes target that key in place; the secret’s own name is ignored. ```toml [profiles.default] DATABASE_URL = { description = "DB", ref = { item = "POSTGRES_URL" }, providers = ["dotenv://.env.shared"] } ``` ## Security considerations [Section titled “Security considerations”](#security-considerations) Caution Secrets are stored in plain text. Use this provider only where that is acceptable, and always add secret-bearing `.env` files to `.gitignore`. # EJSON Provider > Read secrets from EJSON encrypted files **New in version 0.20** The [`ejson`](https://github.com/Shopify/ejson) provider reads string values from EJSON encrypted files. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | --------------------------------------------------------------- | | Provider | `ejson` | | URI | `ejson:PATH` | | Access | Read-only | | Best for | Existing EJSON files stored with application source | | Authentication | A `private_key` SecretSpec provider credential | | Build feature | `ejson` | | Default storage | JSON Pointer `/{project}/{profile}/{key}` in one encrypted file | ## Quick start [Section titled “Quick start”](#quick-start) Configure an existing EJSON file and supply its private key from an exact Google Cloud Secret Manager reference: secretspec.toml ```toml [providers.ejson_keys] uri = "gcsm://ejson-private-keys" [providers.app_ejson] uri = "ejson:config/secrets.production.ejson" [providers.app_ejson.credentials.private_key] provider = "ejson_keys" ref = { item = "EJSON_PRIVATE_KEY", version = "1" } [profiles.production] API_TOKEN = { description = "Application API token", providers = ["app_ejson"], ref = { item = "/api_token" } } ``` Read the value or resolve it through an SDK: ```bash $ secretspec get API_TOKEN --profile production ``` ```bash $ secretspec run --profile production -- your-command ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * EJSON 1.1.0 or later available as `ejson` on `PATH`; 1.1.0 introduced `--key-from-stdin` * An existing encrypted EJSON file * The matching 64-character hexadecimal private key * Build SecretSpec with `--features ejson` when the provider is not included by your package Install the EJSON CLI through your package manager. Keep the encrypted file in source control if that matches its owning workflow. Keep the private key in a provider such as Google Cloud Secret Manager, keyring, or another store that can supply an exact SecretSpec credential reference. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) The private key belongs in the provider alias’s `credentials` map. It must not appear in the EJSON URI, application environment, or command arguments. | Credential | Environment fallback | Available since | | ------------- | -------------------- | --------------- | | `private_key` | — | 0.20+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. A configured source is authoritative. SecretSpec reads it before constructing the EJSON provider and passes the resulting value only to the EJSON child process through stdin. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text ejson:PATH ``` `PATH` is the encrypted EJSON file. Relative paths resolve from the directory containing `secretspec.toml`, not the shell’s current directory. The default path is `secrets.ejson`. User information, ports, query options, and fragments are rejected. In particular, the private key cannot be added to the URI. ### URI examples [Section titled “URI examples”](#uri-examples) ```text ejson:secrets.ejson ejson:config/secrets.production.ejson ejson:///var/run/application/secrets.ejson ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) Use one alias for the key source and one for the EJSON file: secretspec.toml ```toml [providers.keys] uri = "gcsm://my-key-project" [providers.encrypted] uri = "ejson:config/secrets.production.ejson" [providers.encrypted.credentials.private_key] provider = "keys" ref = { item = "EJSON_PRIVATE_KEY", version = "1" } ``` The public key remains embedded in the EJSON file. The credential reference names the Google Cloud Secret Manager secret and version that store its matching private key. No public-key URI option is needed. ## Storage model [Section titled “Storage model”](#storage-model) Convention-addressed secrets use an RFC 6901 JSON Pointer with project and profile isolation: ```text /{project}/{profile}/{key} ``` For project `my-app`, profile `production`, and secret `API_TOKEN`, the decrypted JSON shape is: ```json { "my-app": { "production": { "API_TOKEN": "secret-value" } } } ``` `~` and `/` inside a component are escaped as `~0` and `~1`. The selected value must be a JSON string. Missing pointers are treated as missing secrets; numbers, booleans, objects, arrays, and null are rejected as secret values. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) Use `ref.item` to name any existing string with an RFC 6901 JSON Pointer: ```toml [profiles.production] DATABASE_PASSWORD = { description = "Existing EJSON database password", providers = ["app_ejson"], ref = { item = "/database/password" } } ``` The provider supports only `item`. Extra coordinates such as `field` and `version` are rejected. The provider is read-only, so `secretspec set`, generated-value persistence, and import destinations are unavailable. ## Private-key source [Section titled “Private-key source”](#private-key-source) The private key can come from any SecretSpec provider that can read the configured reference. The Google Cloud Secret Manager example uses Application Default Credentials; configure those credentials for the runtime environment and grant access only to the referenced private-key secret. ## Security considerations and limitations [Section titled “Security considerations and limitations”](#security-considerations-and-limitations) * The current EJSON CLI decrypts the complete document before SecretSpec selects requested JSON Pointers. Each nonempty `get_many` call decrypts once for its complete batch; later calls decrypt again. * Encrypted input and decrypted output are each limited to 16 MiB. Full-document buffering creates multiple transient copies, so use smaller, trusted files. * The private key and decrypted document exist transiently in process memory. The provider does not write a decrypted file or export values to the environment itself. * Treat the `ejson` executable and the process `PATH` as trusted. The executable receives the private key on stdin and writes the complete decrypted document to stdout. * On Unix, the configured final path is opened with no-follow and nonblocking semantics. SecretSpec copies the ciphertext into an anonymous file descriptor inherited by EJSON, so later path or temporary-name replacement cannot change the document. Other platforms use a private named ciphertext snapshot. Parent-directory resolution still requires a trusted path. * Private-key delivery, EJSON execution, and output collection share one 30-second deadline. On Unix, cleanup stops the CLI process group on every exit. * EJSON leaves string properties whose names begin with `_` unencrypted. Treat them as public metadata, never secrets. * A SecretSpec scope limits returned values but is not an authorization boundary. Anyone with the private key can decrypt the complete EJSON file. * Use separate EJSON files and keypairs when workloads have different trust boundaries or rotation requirements. * SecretSpec does not watch the file or reload application clients automatically. A caller must perform another resolution and replace its own credential consumers. # Environment Variable Provider > Read-only access to environment variables The [Environment Variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html) provider reads secrets directly from process environment variables. This is a **read-only** provider designed for CI/CD compatibility and containerized environments. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ----------------------------------------------------- | | Provider | `env` | | URI | `env://` | | Access | Read-only | | Best for | CI/CD, containers, and temporary overrides | | Authentication | None | | Default storage | Current process environment; values are not persisted | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set environment variables $ export DATABASE_URL="postgresql://localhost/mydb" $ export API_KEY="sk-1234567890" # Check secrets are available $ secretspec check --provider env ✓ All required secrets are configured # Run with environment variables $ secretspec run --provider env -- npm start ``` ## Configuration [Section titled “Configuration”](#configuration) The env provider accepts no configuration options: ```bash # All these are equivalent $ secretspec check --provider env $ secretspec check --provider env: $ secretspec check --provider env:// ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] injected = "env" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["injected"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Convention secrets read the environment variable with the same name. The provider reads only the current process environment, never writes variables, and does not persist values. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field reads a different variable, which is useful when your infrastructure already exposes a value under another name: `item` is the variable name, case-sensitive and preserved verbatim (`field` is not supported). Like the rest of this provider, references are read-only. ```toml [profiles.default] DATABASE_URL = { description = "DB", ref = { item = "POSTGRES_CONNECTION_STRING" }, providers = ["env"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```yaml # GitHub Actions - name: Run with secrets env: DATABASE_URL: ${{ secrets.DATABASE_URL }} API_KEY: ${{ secrets.API_KEY }} run: | secretspec run --provider env -- npm run deploy ``` ## When to use [Section titled “When to use”](#when-to-use) * Running in CI/CD pipelines where secrets are injected as environment variables * Testing with temporary environment variables * Working with containerized applications that use environment variables # File Provider > Store each secret in one plaintext file beneath a local directory **New in version 0.19** The [file](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html) provider reads and writes one plaintext UTF-8 file per secret. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | -------------- | --------------------------------------------------- | | Provider | `file` (0.19+) | | URI | `file:ROOT` | | Access | Read, write, and delete | | Best for | Local fixtures and file-mounted secrets | | Authentication | Filesystem permissions | | Availability | Built in (0.19+) | | Storage root | Required; relative to `secretspec.toml` or absolute | ## Quick start [Section titled “Quick start”](#quick-start) Choose a directory, exclude it from version control, and route a declaration to it: .gitignore ```text /.secrets/ ``` secretspec.toml ```toml [providers] local_files = "file:./.secrets" [profiles.development] API_TOKEN = { description = "Local API token", providers = ["local_files"] } ``` ```bash $ secretspec set API_TOKEN --profile development $ secretspec get API_TOKEN --profile development $ secretspec run --profile development -- npm start ``` The stored value is `.secrets//development/API_TOKEN`, where `` is `[project].name` from the manifest. ## Setup [Section titled “Setup”](#setup) The provider has no external dependency or credential. The SecretSpec process needs read access to the configured directory and write access for `set`, generated values, imports, cache writes, and deletes. Relative roots resolve from the directory containing `secretspec.toml`, not from the shell’s current directory. Every `file` provider URI must include a root; the bare `file` provider name is rejected. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text file:ROOT ``` `ROOT` is required and names the directory that contains the project/profile tree. It can be relative, absolute, or home-relative. The URI accepts no query options or user information. ### URI examples [Section titled “URI examples”](#uri-examples) ```text file:./.secrets # .secrets beside secretspec.toml file:///var/lib/app/secrets # Absolute directory file:~/.local/share/myapp # Home-relative directory ``` Use `file:./...` for relative paths containing spaces. SecretSpec percent- encodes the path when reporting the provider URI. ### Project configuration [Section titled “Project configuration”](#project-configuration) Check in the provider alias, but never the directory’s plaintext contents: secretspec.toml ```toml [providers] local_files = "file:./.secrets" [profiles.default] DATABASE_URL = { description = "Database connection string", providers = ["local_files"] } ``` The alias is shared configuration. Each machine supplies its own directory contents. ## Storage model [Section titled “Storage model”](#storage-model) Convention addresses use this path beneath `ROOT`: ```text {project}/{profile}/{key} ``` Project and profile are therefore isolated even when one user-global file provider serves several projects. Each component must be one safe filename; slashes, backslashes, `.` and `..` are rejected in convention components. Values are read and written exactly as UTF-8 text. Leading and trailing whitespace, newlines, CRLF line endings, and multiline content are preserved. Binary files are rejected by the provider’s text interface; use a SecretSpec 0.19+ `encoding` when the stored file should contain a textual encoding of binary data. Writes use a temporary file in the destination directory and atomically replace the entry after flushing it. On Unix, SecretSpec creates new directories with mode `0700` and new entry files with mode `0600`. Existing directory permissions are not changed. ## Use existing files [Section titled “Use existing files”](#use-existing-files) A native `ref.item` is a relative path beneath `ROOT`. It replaces the entire `{project}/{profile}/{key}` convention path: secretspec.toml ```toml [providers] runtime_files = "file:///run/secrets" [profiles.production] DATABASE_PASSWORD = { description = "Database password mounted by the runtime", providers = ["runtime_files"], ref = { item = "database/password" } } ``` This reads `/run/secrets/database/password`. `item` may contain nested relative components, but absolute paths, empty components, `.`, `..`, backslashes, and symbolic links inside the configured root are rejected. `field`, `vault`, `section`, and `version` do not have file equivalents and are rejected. Referenced files are writable when filesystem permissions allow it. Treat runtime-managed mounts as read-only unless their owner explicitly permits SecretSpec to replace or delete entries. ## Extract from a document [Section titled “Extract from a document”](#extract-from-a-document-019) **New in version 0.19** **Changed in version 0.20** Structured extraction now supports INI documents with `format = "ini"`. Several declarations can select values from one JSON file without making JSON part of the provider itself: secretspec.toml ```toml [providers] runtime_files = "file:///run/secrets" [profiles.production] # extract is available in SecretSpec 0.19+ DATABASE_USER = { description = "Database user", providers = ["runtime_files"], ref = { item = "application.json" }, extract = { format = "json", pointer = "/database/user" } } DATABASE_PASSWORD = { description = "Database password", providers = ["runtime_files"], ref = { item = "application.json" }, extract = { format = "json", pointer = "/database/password" } } ``` An INI file is selected the same way with `format = "ini"` (0.20+), where `/key` reads an unsectioned key and `/section/key` reads a key in a named section: secretspec.toml ```toml [profiles.production] # format = "ini" requires SecretSpec 0.20+ DATABASE_PASSWORD = { description = "Database password", providers = ["runtime_files"], ref = { item = "application.ini" }, extract = { format = "ini", pointer = "/database/password" } } ``` The file provider returns the complete UTF-8 document; SecretSpec then applies the pointer as a provider-independent stored-value transform. Extracted declarations are read-only so `set`, `delete`, generation, prompting, and import cannot overwrite or remove the containing file. See [Structured Extraction](/reference/configuration/#structured-extraction-019) for value rendering, error behavior, and composition with `encoding`. ## CI/CD and containers [Section titled “CI/CD and containers”](#cicd-and-containers) Use a read-only mounted directory with explicit refs when the runtime already publishes one file per secret. For example, mount the source at `/run/secrets` and use `file:///run/secrets` as the provider alias. SecretSpec reads only the files declared by the active profile; it does not enumerate or inject every file in the mount. The ordinary convention is useful when SecretSpec owns the directory. Give each job a private root and let `{project}/{profile}/{key}` keep jobs and environments separate. ## Security considerations [Section titled “Security considerations”](#security-considerations) Plaintext storage The file provider does not encrypt values. Keep its root out of version control, backups, build artifacts, container layers, and shared directories. Use an encrypted provider when the filesystem is not an acceptable trust boundary. SecretSpec rejects symbolic links within the configured store so an entry cannot deliberately redirect reads or writes outside its root. It also rejects lexical traversal through `ref.item`. The configured root itself remains the operator’s trust boundary: protect it with filesystem ownership and mount permissions, and do not let untrusted processes modify it while SecretSpec is running. Deleting an entry removes only its file. Empty project and profile directories remain in place. # Fly.io secrets provider > Publish SecretSpec values to Fly.io application secrets through flyctl **New in version 0.20** The [Fly.io](https://fly.io/) provider publishes declared values to an application’s encrypted secret vault through [`flyctl`](https://fly.io/docs/flyctl/). ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ----------------------------------------------------------------------- | | Provider | `fly` (0.20+) | | URI | `fly://APP[?stage=true][&detach=true]` | | Access | Write, delete, and discover names; plaintext values cannot be read back | | Best for | Publishing secrets to a Fly.io app from a separate source of truth | | Authentication | `flyctl` login or an app-scoped deploy token | | Availability | Built into SecretSpec 0.20+ | | Default storage | Fly app secret named `{key}` | ## Quick start [Section titled “Quick start”](#quick-start) Complete [Setup](#setup) first, then use the checked-in alias from the project configuration below: ```bash # Publish or replace DATABASE_URL, reading the value securely from the terminal $ secretspec set DATABASE_URL --profile production --provider fly_prod # Remove the Fly app secret $ secretspec delete DATABASE_URL --profile production --provider fly_prod ``` Fly.io never returns the plaintext value, so `secretspec get`, `check`, and `run` cannot resolve a value from this provider. Keep the authoritative value in a readable provider and use `fly_prod` explicitly when publishing it. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * SecretSpec 0.20 or newer * A Fly.io application * [`flyctl`](https://fly.io/docs/flyctl/install/) installed on `PATH` * Permission to list and change the selected app’s secrets For local use, authenticate the CLI normally: ```bash $ fly auth login ``` If the executable has another name or location, set `SECRETSPEC_FLYCTL_PATH` to it. ### Authentication with provider credentials [Section titled “Authentication with provider credentials”](#authentication-with-provider-credentials) SecretSpec 0.20+ declares `access_token` as a [provider credential](/reference/provider-credentials/). An app-scoped deploy token is the narrowest standard Fly.io token that can update one app: ```bash $ fly tokens create deploy -a my-app -x 720h ``` Load that token from a bootstrap provider instead of committing it in the URI: secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.fly_prod] uri = "fly://my-app" credentials = { access_token = "bootstrap" } [profiles.production] DATABASE_URL = { description = "Production database URL" } ``` Store the token once: ```bash $ secretspec config provider login fly_prod Enter access_token for provider 'fly_prod' (source: bootstrap): **** ``` SecretSpec passes the token only in the child process environment and passes the application secret value over stdin. Neither value is placed in `flyctl`’s process arguments. SecretSpec removes both Fly token variables from the child environment, then re-injects only the token selected through the provider credential mechanism. This prevents `flyctl` from independently choosing an ambient token with different precedence. ### Environment fallback [Section titled “Environment fallback”](#environment-fallback) In CI, set `FLY_API_TOKEN` (preferred) or `FLY_ACCESS_TOKEN`. An explicit `access_token` provider credential takes precedence, followed by those two variables in that order. If none is set, `flyctl` uses its existing login session. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | -------------- | ------------------------------------ | --------------- | | `access_token` | `FLY_API_TOKEN` → `FLY_ACCESS_TOKEN` | 0.20+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text fly://APP[?stage=true][&detach=true] ``` * `APP` is required and always passed through `--app`; the provider does not depend on a nearby `fly.toml`. * `stage=true` registers changes without immediately updating existing Machines. * `detach=true` starts the Machine update but returns without monitoring it. Only the literal values `true` and `false` are accepted. Explicit `false` values behave like omitted options and are left out of the provider’s canonical URI. ### URI examples [Section titled “URI examples”](#uri-examples) ```text fly://my-app fly://my-app?stage=true fly://my-app?detach=true fly://my-app?stage=true&detach=true ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) Use one alias per Fly app. Because an app is the isolation boundary, profiles that deploy to different apps should select different aliases: secretspec.toml ```toml [providers] fly_staging = "fly://my-app-staging?stage=true" fly_prod = "fly://my-app-production" [profiles.staging] DATABASE_URL = { description = "Staging database URL" } [profiles.production] DATABASE_URL = { description = "Production database URL" } ``` ```bash $ secretspec set DATABASE_URL --profile staging --provider fly_staging $ fly secrets deploy --app my-app-staging $ secretspec set DATABASE_URL --profile production --provider fly_prod ``` ## Storage model [Section titled “Storage model”](#storage-model) The provider maps a declaration’s key directly to a Fly application secret. For example, `DATABASE_URL` in any SecretSpec project or profile maps to: ```text app: URI authority (for example, my-app-production) secret: DATABASE_URL ``` Project and profile names are not added to the secret name. The app selected by the alias supplies that isolation and lets the value appear under the expected environment-variable name inside every Machine. By default, `flyctl secrets set` updates the app’s Machines. This restarts them and resets their ephemeral filesystems. Use `stage=true` to group multiple changes before running `fly secrets deploy --app APP` yourself. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A [`ref`](/reference/configuration/#secret-references) changes the Fly secret name updated or deleted by SecretSpec: secretspec.toml ```toml [providers] fly_prod = "fly://my-app-production" [profiles.production] DATABASE_URL = { description = "Production database URL", ref = { item = "PRIMARY_DATABASE_URL" } } ``` With `--provider fly_prod`, `set` writes `PRIMARY_DATABASE_URL` and `delete` removes it. A ref still cannot read that value; Fly.io exposes only names, digests, and deployment status. ## Discover secret names [Section titled “Discover secret names”](#discover-secret-names) `flyctl secrets list --json` exposes enough metadata for SecretSpec to discover declarations without reading values: ```bash $ secretspec init --from fly://my-app-production \ --project my-app --profile production ``` The generated manifest contains required declarations for the listed names, not defaults or secret values. ## CI/CD [Section titled “CI/CD”](#cicd) Install `flyctl`, provide an expiring app-scoped deploy token, and select the provider alias explicitly. For example: ```yaml - uses: superfly/flyctl-actions/setup-flyctl@master - run: secretspec set DATABASE_URL --profile production --provider fly_prod env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} ``` Use the shortest practical token lifetime and scope it to the app named in the provider URI. ## Security considerations and limitations [Section titled “Security considerations and limitations”](#security-considerations-and-limitations) * Fly.io’s API servers encrypt application secrets but cannot decrypt them. This provider therefore cannot support `get`, `check`, `run`, fallback reads, generation-on-miss, prompting-on-miss, or value comparisons. * `secretspec set` uses `flyctl secrets set NAME=-` and writes the value to the child process’s stdin. The secret is not exposed in argv or the provider URI. Because `flyctl` trims stdin values, SecretSpec refuses values with leading or trailing whitespace instead of silently storing a different value. * A normal write or delete updates the app’s Machines unless `stage=true` is configured. Review the rollout effect before using the provider in a loop. * `secretspec delete` first lists names so it can report whether anything was removed. The listing never contains plaintext values. # Fly.io provider name > The Fly.io provider uses the fly name and URI scheme in SecretSpec 0.20+. **Changed in version 0.20** The pre-release [`flyctl`](https://fly.io/docs/flyctl/) provider name was replaced by `fly`. Use the [`fly` provider guide](/providers/fly/) and configure Fly.io application secrets with a `fly://APP` URI. The provider still invokes the `flyctl` executable internally. Only the SecretSpec provider name and URI scheme changed. # The [Fly.io](https://fly.io/) provider is named `fly` [Section titled “The Fly.io provider is named fly”](#the-flyio-provider-is-named-fly) # Google Cloud Secret Manager Provider > Google Cloud Secret Manager integration The [Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) provider integrates with GCP for centralized secret management. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------- | | Provider | `gcsm` | | URI | `gcsm://PROJECT_ID` | | Access | Read and write; secret references are read-only | | Best for | Workloads and teams on Google Cloud | | Authentication | Google Application Default Credentials | | Build feature | `gcsm` | | Default storage | `secretspec2--{project}--{profile}--{key}` (0.20+) | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider gcsm://my-gcp-project Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to gcsm (profile: default) # Run with secrets $ secretspec run --provider gcsm://my-gcp-project -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * Google Cloud CLI (`gcloud`) * GCP project with Secret Manager API enabled * Build with `--features gcsm` ### Authentication [Section titled “Authentication”](#authentication) Google Cloud Secret Manager uses Application Default Credentials. For local development: ```bash $ gcloud auth application-default login ``` In Google Cloud runtimes, Application Default Credentials use the attached service account automatically. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext gcsm://PROJECT_ID ``` * `PROJECT_ID`: Your GCP project ID ### URI examples [Section titled “URI examples”](#uri-examples) ```text gcsm://my-gcp-project ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] google = "gcsm://my-gcp-project" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["google"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) **Changed in version 0.20** Releases through 0.19 used `secretspec-{project}-{profile}-{key}`. SecretSpec joins the project, profile, and key with validated `--` boundaries. Distinct logical addresses therefore cannot collapse onto one GCSM secret when a project or profile contains a single internal hyphen. For example, project `myapp`, profile `production`, and key `DATABASE_URL` map to: ```text secretspec2--myapp--production--DATABASE_URL ``` Each component may contain ASCII letters, digits, underscores, and single internal hyphens. A component cannot start or end with `-` or contain `--`, because those forms could overlap a boundary. The complete GCSM id must fit the service’s 255-character limit. Releases through 0.19 accepted project, profile, and key names the new layout cannot represent, such as a project directory named `my--app`. Reads of such an address keep serving the 0.19 secret and print a warning, but writes fail until the name changes. Rename the offending component and run `secretspec set` to store the value under the new id, or address the secret with an explicit [`ref`](/reference/configuration/#secret-references), which is exempt from the convention. ### Reading legacy secrets [Section titled “Reading legacy secrets”](#reading-secrets-stored-by-019) **Changed in version 0.20** Reads now try the collision-safe ID first, then fall back to the 0.19 ID with a warning. Writes use only the collision-safe ID. SecretSpec 0.20 reads the new id first. When that secret holds no value, the read falls back to the 0.19 `secretspec-{project}-{profile}-{key}` id and returns its latest value, printing one warning per run. A project upgraded from 0.19 therefore keeps working with no migration step. With secret-level IAM, an unbound new id can return `PERMISSION_DENIED` instead of `NOT_FOUND`. SecretSpec still probes the legacy id in that case and uses it when readable. If the legacy id supplies no value, the original denial remains an error; failures other than the expected permission denial from a legacy-id probe are also reported rather than treated as a missing secret. The fallback is a read. Nothing is created, copied, or deleted, so the upgrade needs no new permissions: credentials holding only `roles/secretmanager.secretAccessor`, the usual CI principal, keep working unchanged. Writes always use the new id. Running `secretspec set` for a secret is what moves it, and afterwards reads stop consulting the legacy id. The 0.19 secret is left in place, so an older SecretSpec keeps reading the value it knows and a rollback needs no recovery step. Two consequences are worth planning for: * A secret still served by the fallback depends on the 0.19 id continuing to exist. Delete legacy secrets only after the values that matter have been written under the new id. * While a secret is served by the fallback, a 0.19 writer and a 0.20 writer update different ids. Point every writer at the same SecretSpec version, or set the secret with 0.20 to settle it on the new id. Only the value is read across. Labels, rotation settings, secret-level IAM bindings, and other resource metadata belong to the legacy secret; reproduce any such configuration when you write the secret under its new id. If the legacy id had already received writes from colliding logical addresses, the provider cannot determine which historical version belonged to which address. An explicit `ref` is a native address and is never renamed or migrated: ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "secretspec-myapp-production-DATABASE_URL" }, providers = ["google"] } ``` ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing secret instead: `item` is the secret id, and the optional `version` pins a version (defaults to latest; `field` is not supported). References are **read-only** in this provider. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "database-url" }, providers = ["gcsm://my-gcp-project"] } SIGNING_KEY = { description = "Key", ref = { item = "signing-key", version = "3" }, providers = ["gcsm://my-gcp-project"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Set credentials $ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json" # Run command $ secretspec run --provider gcsm://my-gcp-project -- deploy ``` # Gopass Provider > GPG-encrypted, git-synced password store integration **New in version 0.15** The [Gopass](https://www.gopass.pw/) provider integrates with gopass, a multi-user, multi-store abstraction layer on top of `pass` that keeps secrets GPG-encrypted and syncs them via git. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ----------------------------------------------------- | | Provider | `gopass` | | URI | `gopass://[folder_prefix]` | | Access | Read and write | | Best for | GPG-encrypted, git-synced, multi-user password stores | | Authentication | The GPG key configured for the password store | | Availability | SecretSpec 0.15+ | | Default storage | `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider gopass Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret DATABASE_URL saved to gopass # Get a secret $ secretspec get DATABASE_URL --provider gopass postgresql://localhost/mydb # Run with secrets $ secretspec run --provider gopass -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) Install the `gopass` CLI and initialize a password store: ```bash # macOS $ brew install gopass # Debian/Ubuntu $ sudo apt install gopass # NixOS $ nix-env -iA nixpkgs.gopass ``` ### Authentication [Section titled “Authentication”](#authentication) SecretSpec uses the GPG identities configured by `gopass`. Confirm that the target store is initialized and can be unlocked before using the provider. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext gopass://[folder_prefix] ``` * `folder_prefix`: Optional path prefix supporting `{project}`, `{profile}`, and `{key}` placeholders. Defaults to `secretspec/{project}/{profile}/{key}`. ### URI examples [Section titled “URI examples”](#uri-examples) ```text gopass gopass://secretspec/shared/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] team = "gopass://" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["team"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Each secret is stored under `secretspec/{project}/{profile}/{key}`. Gopass encrypts the entry with GPG and can synchronize the password store through git. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing entry instead: `item` is the full entry path, including any mount-point prefix for multi-store setups (`field` is not supported). Reads and writes target that entry in place. ```toml [profiles.production] DATABASE_URL = { description = "Production DB", ref = { item = "work-store/infra/postgres" }, providers = ["gopass"] } ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Shared secrets [Section titled “Shared secrets”](#shared-secrets) By default, secrets are stored under `secretspec/{project}/{profile}/{key}`, which isolates them per project. To share secrets across projects, use a custom folder prefix via the URI: \~/.config/secretspec/config.toml ```toml [defaults.providers] shared = "gopass://secretspec/shared/{profile}/{key}" ``` The URI supports `{project}`, `{profile}`, and `{key}` placeholders. By omitting `{project}`, multiple projects can read and write the same store entry: ```toml # secretspec.toml (in project-A and project-B) [profiles.default] ARTIFACTORY_USER = { description = "Artifactory user", providers = ["shared"] } ``` Both projects will resolve `ARTIFACTORY_USER` from `secretspec/shared/default/ARTIFACTORY_USER`. ## Troubleshooting and limitations [Section titled “Troubleshooting and limitations”](#troubleshooting-and-limitations) Only the first line of an entry is read back — if an entry was written outside of `secretspec` and contains multiple lines, everything after the first line is discarded on `get`. # Infisical Provider > Infisical integration **New in version 0.16** The [Infisical](https://infisical.com) provider integrates with Infisical over its REST API, for both Infisical Cloud and self-hosted instances. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------- | | Provider | `infisical` | | URI | `infisical://[host]/PROJECT_ID[?options]` | | Access | Read and write; version-pinned references are read-only | | Best for | Infisical Cloud or self-hosted Infisical deployments | | Authentication | Universal Auth machine identity or access token | | Availability | SecretSpec 0.16+; requires the `infisical` build feature | | Default storage | Key `{key}` in `{path}/{project}/{profile}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Store a secret $ secretspec set DATABASE_URL --provider "infisical://app.infisical.com/7e2f1a4c-...?env=dev" # Verify every secret is set $ secretspec check --provider "infisical://app.infisical.com/7e2f1a4c-...?env=dev" # Run with secrets injected $ secretspec run --provider "infisical://app.infisical.com/7e2f1a4c-...?env=dev" -- npm start ``` Secrets sharing a folder are fetched in one request, so a run costs one call per folder rather than one per secret. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * An Infisical project * A machine identity with access to it * Build with `--features infisical` ### Universal Auth machine identity [Section titled “Universal Auth machine identity”](#universal-auth-machine-identity) Create a machine identity in Infisical, grant it access to the project, and set: ```bash $ export INFISICAL_CLIENT_ID=... $ export INFISICAL_CLIENT_SECRET=... ``` The provider exchanges these for a short-lived access token once per run. ### Access token [Section titled “Access token”](#access-token) A token minted elsewhere can be used directly: ```bash $ export INFISICAL_TOKEN=... ``` Service tokens are not supported: Infisical deprecated them in favour of machine identities. ### Credentials from another provider [Section titled “Credentials from another provider”](#credentials-from-another-provider) A machine identity’s credentials can live in another store rather than in the environment, declared as [provider credentials](/reference/provider-credentials/): secretspec.toml ```toml [providers.infisical] uri = "infisical://app.infisical.com/7e2f1a4c-..." [providers.infisical.credentials] client_id = "keyring" client_secret = "keyring" ``` The provider declares `client_id` and `client_secret` for Universal Auth, and `token` for a ready-made access token. Each falls back to its corresponding environment variable when it is not declared. Use `secretspec config provider login infisical` to store declared credentials. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | --------------- | ------------------------- | --------------- | | `client_id` | `INFISICAL_CLIENT_ID` | 0.16+ | | `client_secret` | `INFISICAL_CLIENT_SECRET` | 0.16+ | | `token` | `INFISICAL_TOKEN` | 0.16+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext infisical://[host]/{project-id}[?env=slug&path=/prefix&tls=false] ``` * `host`: the Infisical instance (falls back to `INFISICAL_DOMAIN`, then the legacy `INFISICAL_API_URL`, then `app.infisical.com`) * `{project-id}`: the project’s **UUID**, from Project Settings → Project ID * `?env=`: environment slug. Without it, the SecretSpec profile names the environment * `?path=`: folder prefix holding SecretSpec’s secrets (default: `/secretspec`) * `?tls=false`: disable TLS, for self-hosted instances served over plain HTTP Infisical’s API addresses a project by UUID, not by the slug shown in its UI. ### URI examples [Section titled “URI examples”](#uri-examples) ```text infisical://app.infisical.com/7e2f1a4c-... infisical://eu.infisical.com/7e2f1a4c-... infisical://vault.example.com/7e2f1a4c-... infisical://localhost:8080/7e2f1a4c-...?tls=false ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] infisical = "infisical://app.infisical.com/7e2f1a4c-...?env=prod" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["infisical"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) ### Profiles and environments [Section titled “Profiles and environments”](#profiles-and-environments) By default a SecretSpec profile names the Infisical environment: a `production` profile reads the `production` environment, and `dev` reads `dev`. New Infisical projects come with `dev`, `staging` and `prod`, so this works out of the box for profiles named after them. A project whose environments do not correspond to profiles pins one with `?env=`: ```bash # Every profile reads Infisical's "dev" environment $ secretspec run --provider "infisical://app.infisical.com/7e2f1a4c-...?env=dev" -- npm start ``` Profiles stay separate either way: the profile names the folder as well as the environment, so pinning `?env=` cannot make two profiles share a secret. To route each profile to a different environment, give each one its own alias: ```toml [providers] infisical_dev = "infisical://app.infisical.com/7e2f1a4c-...?env=dev" infisical_prod = "infisical://app.infisical.com/7e2f1a4c-...?env=prod" [profiles.production] DATABASE_URL = { description = "Production database", providers = ["infisical_prod"] } ``` ### Secret naming [Section titled “Secret naming”](#secret-naming) Secrets are stored under the folder `{path}/{project}/{profile}`, in the environment named by the profile (or by `?env=`): ```plaintext project "myapp", profile "prod", key "DATABASE_URL" -> environment prod folder /secretspec/myapp/prod key DATABASE_URL ``` Keys are stored exactly as written: Infisical accepts any non-empty key, so nothing is rewritten and two keys can never collide. Folder names are narrower — letters, digits, dashes and underscores — so a project or profile Infisical cannot spell is refused rather than quietly renamed. Folders are created as needed when writing a secret. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret can name one Infisical secret by its own coordinates, instead of SecretSpec’s layout: ```toml [providers] infisical_prod = "infisical://app.infisical.com/7e2f1a4c-...?env=prod" [profiles.production] DATABASE_URL = { description = "Postgres DSN", ref = { item = "/infra/shared/DB_PASSWORD" }, providers = ["infisical_prod"] } API_KEY = { description = "Pinned key", ref = { item = "/infra/API_KEY", version = "3" }, providers = ["infisical_prod"] } ``` * `item`: the folder and key. A leading slash names the environment’s root — `/infra/shared/DB_PASSWORD` is read from `/infra/shared`. Without one, the folder is read under the configured prefix, so `team/DB_PASSWORD` means `/secretspec/team/DB_PASSWORD` and a bare `DB_PASSWORD` sits at `/secretspec` itself * `version`: an Infisical secret version. Version-pinned refs are read-only, since a past version cannot be rewritten A ref names a folder and key but never an environment. The environment comes from `?env=`, or from the profile the run resolves under (0.20+) — the same environment a convention secret reads in that run. With the profile supplying the environment, one alias serves every profile and secrets can be named flat, with no `{project}/{profile}` folders: ```toml [providers] # `production` reads Infisical's production environment, `dev` reads dev infisical_flat = { uri = "infisical://app.infisical.com/7e2f1a4c-...", ref = { item = "/{key}" } } ``` See [Secret References](/concepts/references/#different-coordinates-per-provider-019) for the alias-level `ref` template. A [provider credential](/reference/provider-credentials/) is the exception: it belongs to its alias rather than to a profile, and is read the same way whichever profile runs, so a credential declared with a `ref` needs `?env=`. Infisical secrets are single values with no sub-components, so `field`, `section` and `vault` are rejected. ## CI/CD [Section titled “CI/CD”](#cicd) Use Universal Auth credentials stored in the CI platform, or provide an access token minted by your deployment environment: ```bash $ export INFISICAL_CLIENT_ID="$CI_INFISICAL_CLIENT_ID" $ export INFISICAL_CLIENT_SECRET="$CI_INFISICAL_CLIENT_SECRET" $ secretspec run --provider "infisical://app.infisical.com/7e2f1a4c-...?env=prod" -- deploy ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Imported folders [Section titled “Imported folders”](#imported-folders) A folder that imports another resolves the imported keys too, with Infisical’s own precedence: a secret defined directly in the folder wins over an imported one, and a later import wins over an earlier one. This matches their CLI, so a value reads the same way through either tool. ### Secret references inside values [Section titled “Secret references inside values”](#secret-references-inside-values) Values are read with Infisical’s own `${...}` references expanded, matching its CLI, so a value of `postgres://${DB_USER}@host` arrives resolved. ### Self-hosting [Section titled “Self-hosting”](#self-hosting) Point the URI at the instance, or set `INFISICAL_DOMAIN`: ```bash $ export INFISICAL_DOMAIN=https://vault.example.com $ secretspec run --provider "infisical:///7e2f1a4c-..." -- npm start ``` Infisical’s legacy `INFISICAL_API_URL` is honoured too, so an instance already configured for their CLI works unchanged. `INFISICAL_DOMAIN` wins when both are set, matching the CLI. ### Approval policies [Section titled “Approval policies”](#approval-policies) A project under an approval policy turns a write into a change request: Infisical stores nothing until a human merges it. `secretspec set` reports that rather than claiming the secret was stored, so the value is written once the request is approved. ## Troubleshooting and limitations [Section titled “Troubleshooting and limitations”](#troubleshooting-and-limitations) * The project is addressed by UUID; Infisical’s API does not accept a project slug * A project or profile whose name is not spellable as an Infisical folder (letters, digits, dashes, underscores) is refused rather than rewritten * A ref reads the environment the profile names unless `?env=` pins one. Infisical answers a missing secret, folder, environment and project with the same 404. Starting in SecretSpec 0.20, if every requested secret gets that ambiguous response, SecretSpec checks the environment root once. A missing environment or project then becomes an error naming the selected environment and whether the profile or `?env=` selected it; a missing secret or folder in an existing environment remains unset so provider fallback still works * `secretspec import infisical://…` is not supported: the provider does not enumerate existing secrets, so import has nothing to discover * The domain variables name a host, not a path: an instance served under a sub-path (`https://example.com/infisical`) is not addressable. A trailing `/api` is the exception and is accepted, since Infisical’s own CLI takes the domain in that form * If a profile does not match an environment slug — Infisical’s own default projects use `dev`, `staging` and `prod` — pin the right one with `?env=` # KeePass KDBX Provider > Store SecretSpec values in an encrypted KeePass database **New in version 0.17** The [KeePass KDBX](https://keepass.info/) provider reads and writes encrypted databases directly, without requiring KeePass or KeePassXC to be installed. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ---------------------------------------------------------------------------------------------- | | Provider | `kdbx` | | URI | `kdbx:PATH[?keyfile=PATH][&prefix=TEMPLATE]` | | Access | KDBX 3 read; KDBX 4 read and write | | Best for | Local, portable KeePass-compatible encrypted storage | | Authentication | Master password, key file, or both | | Build feature | `kdbx` (0.17+) | | Default storage | Nested groups `secretspec` → `{project}` → `{profile}`; entry titled `{key}`; field `Password` | ## Quick start [Section titled “Quick start”](#quick-start) secretspec.toml ```toml [providers] kdbx = { uri = "kdbx:./secrets.kdbx", credentials = { password = "keyring" } } ``` ```bash # Store the database master password in the bootstrap provider. $ secretspec config provider login kdbx Enter password for provider 'kdbx' (source: keyring): **** # Set a secret in an existing KDBX 4 database, or create a new KDBX 4 database. $ secretspec set DATABASE_URL --provider kdbx Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret DATABASE_URL saved to kdbx $ secretspec get DATABASE_URL --provider kdbx postgresql://localhost/mydb $ secretspec run --provider kdbx -- npm start ``` ## Setup [Section titled “Setup”](#setup) The provider is built into standard SecretSpec 0.17 binaries. Custom builds must enable the `kdbx` feature. ### Authentication [Section titled “Authentication”](#authentication) Load the semantic `password` [provider credential](/reference/provider-credentials/) from a bootstrap provider such as the system keyring. This keeps the KDBX master password out of shell profiles and child-process environments: secretspec.toml ```toml [providers] kdbx = { uri = "kdbx:./secrets.kdbx", credentials = { password = "keyring" } } ``` Store the declared credential once: ```bash $ secretspec config provider login kdbx Enter password for provider 'kdbx' (source: keyring): **** ``` `SECRETSPEC_KDBX_PASSWORD` is available as a fallback for environments without a suitable bootstrap provider. Avoid it for normal interactive use, and do not persist the master password in a shell profile. Use `?keyfile=PATH` for a KeePass key file. When both a password and key file are configured, both are required to unlock the database, matching KeePass. Relative database and key-file paths resolve from the directory containing `secretspec.toml`. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ---------- | -------------------------- | --------------- | | `password` | `SECRETSPEC_KDBX_PASSWORD` | 0.17+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text kdbx:PATH[?keyfile=PATH][&prefix=TEMPLATE] ``` * `PATH` is the KDBX database. Use `./` for a relative path so its spelling and case are preserved as a URI path. * `keyfile` is an optional KeePass key file. * `prefix` changes the convention entry path. It accepts `{project}`, `{profile}`, and `{key}` placeholders and defaults to `secretspec/{project}/{profile}/{key}`. ### URI examples [Section titled “URI examples”](#uri-examples) ```text kdbx:./secrets.kdbx kdbx:/var/lib/myapp/secrets.kdbx kdbx:./secrets.kdbx?keyfile=./secrets.key kdbx:./shared.kdbx?prefix=teams/{project}/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] local_vault = { uri = "kdbx:./secrets.kdbx?keyfile=./secrets.key", credentials = { password = "keyring" } } [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["local_vault"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Yes: the `/` characters in the default convention address separate nested KeePass groups. The path starts inside the database’s root group; neither the database filename nor the root group’s display name is part of it. SecretSpec then uses the final path component as the entry title and stores the value in the entry’s protected `Password` field. For this configuration: secretspec.toml ```toml [project] name = "my-app" revision = "1.0" [profiles.default] DATABASE_URL = { description = "Database URL" } ``` the KeePass tree is: ```text Database root (its name does not matter) └── secretspec group └── my-app group ([project].name) └── default group (active profile) └── DATABASE_URL entry (secret key) └── Password = ``` ### Set up an entry manually [Section titled “Set up an entry manually”](#set-up-an-entry-manually) 1. Open the database file named by the provider URI, such as `secrets.kdbx` for `kdbx:./secrets.kdbx`. The file itself can have any name. 2. Directly below the database’s root group, create a group named `secretspec`. 3. Inside it, create a group whose name exactly matches `[project].name` in `secretspec.toml`. 4. Inside the project group, create a group whose name exactly matches the active profile, such as `default`. 5. Inside the profile group, create an entry whose **Title** exactly matches the secret key, such as `DATABASE_URL`, and put the secret value in its **Password** field. Do not rename the database or its root group to `secretspec`; `secretspec` is a child group of the root. Group names and entry titles are case-sensitive. If you want SecretSpec to write to a manually created database, save it as KDBX 4. You can also let `secretspec set` create the missing groups and entry automatically. Reads open KDBX 3 and KDBX 4 databases. Writes create KDBX 4 databases and atomically replace an existing KDBX 4 file only after the complete encrypted replacement has been flushed. KDBX 3 databases must be upgraded with KeePass or KeePassXC before SecretSpec can write them. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) Use [`ref`](/reference/configuration/#secret-references) to name an existing entry by its complete group path and title. The optional `field` selects a standard or custom entry field; it defaults to `Password`. ```toml [profiles.production] DATABASE_PASSWORD = { description = "Existing KeePass entry", ref = { item = "Infrastructure/PostgreSQL", field = "Password" }, providers = ["local_vault"] } DATABASE_USERNAME = { description = "Username from the same entry", ref = { item = "Infrastructure/PostgreSQL", field = "UserName" }, providers = ["local_vault"] } ``` Entry and group names are matched exactly. Duplicate titles within one group, or duplicate group names under one parent, are rejected as ambiguous instead of selecting an arbitrary value. Empty path components are not supported. The `Title` field is readable but not writable because it forms part of the entry address; rename entries in KeePass or KeePassXC. ## Security considerations and limitations [Section titled “Security considerations and limitations”](#security-considerations-and-limitations) * Never place the master password in the URI. Use the `password` provider credential from a bootstrap provider. `SECRETSPEC_KDBX_PASSWORD` is a discouraged fallback for environments without one; reported provider URIs never contain the password. * Keep key files separate from the KDBX database when possible. Possessing both removes the extra protection a key file provides. * SecretSpec serializes KDBX operations within one process and replaces files atomically, but KDBX is still a local file rather than a multi-writer service. Avoid editing the same database simultaneously in SecretSpec and KeePass. * Writing uses the `keepass` crate’s KDBX 4 writer. Back up important databases before first use with a new SecretSpec or `keepass` version. # Keeper Secrets Manager Provider > Keeper Secrets Manager integration through Keeper's official Rust SDK **New in version 0.18** The [Keeper Secrets Manager](https://www.keepersecurity.com/secrets-manager.html) provider reads and writes records available to a Keeper Secrets Manager application. SecretSpec links [Keeper’s official Rust SDK](https://github.com/Keeper-Security/secrets-manager/tree/master/sdk/rust), so no separate `ksm` executable is required. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------------------- | | Provider | `keeper` (0.18+) | | URI | `keeper://FOLDER_UID[?config_file=PATH]` | | Access | Read, write, and delete | | Best for | Machine and CI/CD secrets managed in Keeper | | Authentication | KSM configuration, or a one-time token while creating a file configuration | | Availability | SecretSpec 0.18+; requires the `keeper` build feature | | Default storage | Login record `secretspec/{project}/{profile}/{key}`, field `password` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Store a secret as a Keeper record $ secretspec set DATABASE_URL \ --provider "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" # Read it back $ secretspec get DATABASE_URL \ --provider "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" # Resolve the profile and run a command $ secretspec run \ --provider "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" \ -- npm start ``` The folder must be shared with the KSM application and grant edit permission for writes. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * SecretSpec 0.18+ built with the `keeper` feature * A Keeper Secrets Manager application * A shared folder available to that application * A bound KSM client configuration, or a one-time access token for initial binding Keeper’s Rust SDK is embedded in SecretSpec. Keeper Commander and the `ksm` CLI are useful for application setup, but neither is needed when SecretSpec runs. ### Authentication with KSM\_CONFIG [Section titled “Authentication with KSM\_CONFIG”](#authentication-with-ksm_config) Set `KSM_CONFIG` to the JSON or Base64 KSM client configuration: ```bash $ export KSM_CONFIG="$KEEPER_CLIENT_CONFIG" $ secretspec check --provider "keeper://SHARED_FOLDER_UID" ``` The configuration contains private client keys. Treat the whole value as a secret. SecretSpec 0.18+ also declares `config` as a [provider credential](/reference/provider-credentials/), so the configuration can be loaded from another provider instead of the environment: secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.keeper] uri = "keeper://SHARED_FOLDER_UID" credentials = { config = "bootstrap" } [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["keeper"] } ``` When no explicit `config` credential is supplied, the provider falls back to `KSM_CONFIG`. ### Authentication with a configuration file [Section titled “Authentication with a configuration file”](#authentication-with-a-configuration-file) Use `config_file` to read and update a Keeper SDK configuration file: secretspec.toml ```toml [providers] keeper = "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" ``` Relative paths are resolved from the directory containing `secretspec.toml`. Without `config_file`, the SDK honors `KSM_CONFIG_FILE`, then uses its `client-config.json` default. ### Bind with a one-time token [Section titled “Bind with a one-time token”](#bind-with-a-one-time-token) The `token` provider credential, or `KSM_TOKEN` fallback, can bind a new client. Pair it with file storage so the SDK can persist the generated client keys for later SecretSpec processes: ```bash $ export KSM_TOKEN="US:ONE_TIME_TOKEN" $ secretspec check \ --provider "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" $ unset KSM_TOKEN ``` A Keeper one-time token cannot be reused. Remove it after the first successful request and retain the generated configuration file securely. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ---------- | -------------------- | --------------- | | `config` | `KSM_CONFIG` | 0.18+ | | `token` | `KSM_TOKEN` | 0.18+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text keeper://FOLDER_UID[?config_file=PATH] ``` * `FOLDER_UID` is required. It is the case-sensitive UID of a shared folder or one of its subfolders. New convention records are created there. * `config_file` is optional. It selects a Keeper SDK client configuration file. ### URI examples [Section titled “URI examples”](#uri-examples) ```text keeper://SHARED_FOLDER_UID keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] keeper_prod = "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["keeper_prod"] } API_KEY = { description = "API key", providers = ["keeper_prod"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Convention secrets use Keeper login records: ```text record title: secretspec/{project}/{profile}/{key} field: password ``` For example, project `storefront`, profile `production`, and key `DATABASE_URL` map to record title `secretspec/storefront/production/DATABASE_URL`. SecretSpec fetches the application’s available records once for a batch of secret requests, then resolves all requested titles and fields locally. A duplicate convention title is rejected as ambiguous. ## Use existing records [Section titled “Use existing records”](#use-existing-records) A secret’s [`ref`](/reference/configuration/#secret-references) can select an existing Keeper record by exact title or record UID. `field` selects a standard field type/label or custom field label and defaults to `password`: secretspec.toml ```toml [providers] keeper = "keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["keeper"], ref = { item = "KEEPER_RECORD_UID", field = "Database URL" } } ``` Reads and writes target the existing field in place. A write through `ref` never creates a missing record or field; create it in Keeper first. Use a record UID when duplicate titles exist. ## CI/CD [Section titled “CI/CD”](#cicd) Store the bound KSM configuration as one protected CI secret: ```bash $ export KSM_CONFIG="$CI_KEEPER_CONFIG" $ secretspec run --provider "keeper://SHARED_FOLDER_UID" -- ./deploy ``` The KSM application should receive access only to the folders the job needs. Grant edit permission only when the job must run `set`, refresh a Keeper-backed cache, or otherwise write records. ## Security considerations [Section titled “Security considerations”](#security-considerations) The official SDK encrypts and decrypts Keeper records inside the SecretSpec process. Secret values and KSM credentials are not placed in child-process arguments. A `config_file` contains long-lived private client keys and must be protected like any other secret; prefer `KSM_CONFIG` or a provider credential when persistent local files are undesirable. # Keyring Provider > Secure system credential store integration The [Keyring](https://github.com/open-source-cooperative/keyring-rs) provider stores secrets in your system’s native credential store. Recommended for local development. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------- | | Provider | `keyring` | | URI | `keyring://[folder_prefix]` | | Access | Read and write | | Best for | Secure local development | | Authentication | Current operating-system user | | Default storage | `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider keyring Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret DATABASE_URL saved to keyring # Get a secret $ secretspec get DATABASE_URL --provider keyring postgresql://localhost/mydb # Run with secrets $ secretspec run --provider keyring -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Supported platforms [Section titled “Supported platforms”](#supported-platforms) * **macOS**: Keychain * **Windows**: Credential Manager * **Linux**: Secret Service (GNOME Keyring, KWallet) ### Linux prerequisites [Section titled “Linux prerequisites”](#linux-prerequisites) Linux only - install if missing: ```bash # Debian/Ubuntu $ sudo apt-get install gnome-keyring # Fedora $ sudo dnf install gnome-keyring # Arch $ sudo pacman -S gnome-keyring ``` ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext keyring://[folder_prefix] ``` * `folder_prefix`: Optional path prefix supporting `{project}`, `{profile}`, and `{key}` placeholders. Defaults to `secretspec/{project}/{profile}/{key}`. ### URI examples [Section titled “URI examples”](#uri-examples) ```text keyring keyring://shared/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] local = "keyring://" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["local"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Each secret is stored under `secretspec/{project}/{profile}/{key}` as the keyring service, with the current system username as the account. Project and profile names keep convention secrets isolated. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an exact keyring entry instead, useful for reading a credential another application already stored: `item` is the service, and the optional `field` is the account (defaults to the current system username). Reads and writes target that entry in place. ```toml [profiles.default] API_TOKEN = { description = "Token", ref = { item = "com.example.app", field = "me@example.com" }, providers = ["keyring"] } ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Shared secrets [Section titled “Shared secrets”](#shared-secrets) By default, secrets are stored under `secretspec/{project}/{profile}/{key}`, which isolates them per project. To share secrets across projects, use a custom folder prefix via the URI: \~/.config/secretspec/config.toml ```toml [defaults.providers] shared = "keyring://secretspec/shared/{profile}/{key}" ``` The URI supports `{project}`, `{profile}`, and `{key}` placeholders. By omitting `{project}`, multiple projects can read and write the same keyring entry: ```toml # secretspec.toml (in project-A and project-B) [profiles.default] ARTIFACTORY_USER = { description = "Artifactory user", providers = ["shared"] } ``` Both projects will resolve `ARTIFACTORY_USER` from keyring service `secretspec/shared/default/ARTIFACTORY_USER`. # Kubernetes Provider > Kubernetes ConfigMap & Secerts **New in version 0.20** The [Kubernetes](https://kubernetes.io/) provider reads from and writes to Kubernetes ConfigMaps or Secrets. # At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------- | | Provider | `kubernetes` | | URI | `k8s+://NAME[@NAMESPACE]` | | Access | Read and write | | Best for | Accessing values stores in Kubernetes | | Authentication | Current cluster set in kubectl configuration | | Default storage | `secretspec--{project}--{profile}--{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider k8s+secret://secret-name Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret DATABASE_URL saved to kubernetes # Get a secret $ secretspec get DATABASE_URL --provider k8s+secret://secret-name postgresql://localhost/mydb # Run with secrets $ secretspec run --provider k8s+secret://secret-name -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Kubernetes cluster * Cluster connection configured in `$KUBECONFIG` (or `$HOME/.kube/config` as fallback) * Build with `--features kubernetes` ### Authentication [Section titled “Authentication”](#authentication) Uses whatever authentication method is configured in the cluster configuration used by kubectl. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext k8s+KIND://NAME[@NAMESPACE] ``` * `NAME`: Name of the Kubernetes object * `KIND`: Kind of the Kubernetes object. Only supports `configmap` or `secret`. * `NAMESPACE`: Optional namespace where the Kubernetes object exists in. If omitted, will use the cluster’s default namespace. ### URI examples [Section titled “URI examples”](#uri-examples) ```plaintext k8s+configmap://db-config@db-postgres k8s+configmap://db-config k8s+secret://db-credentials@db-postgres ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] kube = "k8s+configmap://db-config@db-postgres" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["kube"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Each secret is stored as a key in the Kubernetes ConfigMap or Secret. Each secret is stored as `secretspec--{project}--{profile}--{key}` under `.data`. A key cannot exceed 253 characters. Each component can only contain alphanumeric characters, underscores, periods, and internal hyphens. SecretSpec joins the project, profile, and key with validated `--` boundaries. Distinct logical addresses therefore cannot collapse onto one GCSM secret when a project or profile contains a single internal hyphen. As a consequence, a component cannot start or end with `-` or contain `--`, because those forms could overlap a boundary. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing secret instead: `item` is the secret name stored in `.data.item` of the Kubernetes object. Reads and writes target that entry in place. ```toml [profiles.default] API_TOKEN = { description = "Token", ref = { item = "com.example.app" }, providers = ["k8s+secret://app-config"] } ``` # LastPass Provider > LastPass password manager integration The [LastPass](https://www.lastpass.com/) provider integrates with LastPass password manager for secure cloud-based secret storage. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------- | | Provider | `lastpass` | | URI | `lastpass://[item_template]` | | Access | Read and write | | Best for | Teams already using LastPass | | Authentication | An authenticated `lpass` CLI session | | Default storage | `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider lastpass Enter value for DATABASE_URL: postgresql://localhost/mydb # Get a secret $ secretspec get DATABASE_URL --provider lastpass # Run with secrets $ secretspec run --provider lastpass -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) Install LastPass CLI: ```bash # macOS $ brew install lastpass-cli # Linux (apt) $ sudo apt install lastpass-cli # NixOS $ nix-env -iA nixpkgs.lastpass-cli ``` ### Authentication [Section titled “Authentication”](#authentication) ```bash # Standard login $ lpass login your-email@example.com # Trust device (reduces MFA prompts) $ lpass login --trust your-email@example.com ``` ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext lastpass://[item_template] ``` `item_template` is optional and replaces the default `secretspec/{project}/{profile}/{key}` layout. It supports the `{project}`, `{profile}`, and `{key}` placeholders. Include `{key}` unless every SecretSpec key should resolve to the same LastPass item. ### URI examples [Section titled “URI examples”](#uri-examples) ```text # Default SecretSpec layout lastpass # Keep SecretSpec items in a team folder lastpass://Work/SecretSpec/{project}/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] team = "lastpass://" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["team"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) By default, each secret maps to an item named `secretspec/{project}/{profile}/{key}`. A custom `item_template` replaces that layout; include all placeholders needed to keep secrets distinct. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing item instead: `item` is the full item name, including any folder (`field` is not supported). Reads and writes target that item in place. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "Shared-Infra/Production DB" }, providers = ["lastpass"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Disable interactive pinentry and authenticate with a CI-managed password $ export LPASS_DISABLE_PINENTRY=1 $ echo "$LASTPASS_PASSWORD" | lpass login --trust your-email@example.com $ secretspec run --provider lastpass -- deploy ``` # Null Provider > Use committed defaults, ephemeral generation, or run-time prompts without storage **New in version 0.19** The [null](https://man7.org/linux/man-pages/man4/null.4.html) provider always reports that a value is missing. SecretSpec can then use the declaration’s committed `default`, generate a fresh value, or—in SecretSpec 0.19+—ask the operator during `run` when `prompt = true`. This is useful for non-sensitive environment configuration and values that should exist for only one invocation or resolution. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | -------- | ------------------------------------------------------------------------------------------ | | Provider | `null` (0.19+) | | URI | `null://` | | Access | Always returns missing; ordinary writes are rejected | | Best for | Team-shared defaults, ephemeral generated values, and operator-supplied run values (0.19+) | | Storage | None | ## Quick start [Section titled “Quick start”](#quick-start) Route committed defaults to `null`: secretspec.toml ```toml [profiles.default] SPRING_PROFILES_ACTIVE = { description = "Spring application profile", default = "local", providers = ["null"] } [profiles.staging] SPRING_PROFILES_ACTIVE = { default = "staging" } ``` ```bash $ secretspec run --profile staging -- mvn spring-boot:run ``` This keeps the application mode aligned with the SecretSpec profile and its secrets. The same pattern works for values such as `LOCAL_PORT`. ## Ephemeral generation [Section titled “Ephemeral generation”](#ephemeral-generation) **New in version 0.19** Route a generated secret to `null` when each materializing resolution should receive a fresh value without storing it in a provider: secretspec.toml ```toml [profiles.default] SESSION_SECRET = { description = "Per-run session secret", type = "base64", generate = { bytes = 32 }, providers = ["null"] } ``` `secretspec run` generates `SESSION_SECRET` once for the resolved environment and gives that value to the child process. A later `run`, `get`, `check`, or SDK value-carrying resolution generates a new value. Value-free reports mark the secret as generated without minting it. ## Ephemeral operator input [Section titled “Ephemeral operator input”](#ephemeral-operator-input-019) **New in version 0.19** Combine `prompt = true` with `null` when the value must always come from the operator and must never be stored: secretspec.toml ```toml [profiles.default] DEPLOY_PASSWORD = { description = "One-time deployment password", required = true, prompt = true, providers = ["null"] } ``` `secretspec run -- ./deploy` reads the value through a hidden controlling terminal prompt, without consuming the child’s stdin. The answer is present in the child environment for that invocation and is then discarded. It is never passed to `null.set()` or written to a cache. A noninteractive run fails before the child starts; other commands and SDK resolution do not prompt. ## How it works [Section titled “How it works”](#how-it-works) SecretSpec normally asks the selected provider before using a default or generating a missing secret. `null` cannot read or store values: reads always report a missing value, and every ordinary write is rejected. The missing read lets SecretSpec use the committed default or generator without provider I/O. The provider has no options, credentials, feature flag, or persistent state. Use it on declarations with defaults, enabled generation, or `prompt = true` (0.19+). Here `prompt` chooses operator input while `null` chooses ephemeral handling; with a writable provider the same prompted answer would be saved. Required declarations with none of those remain missing, and explicit writes are rejected. Defaults are public configuration Manifest defaults are committed to version control in plaintext. Use `default` only for non-sensitive values. Generated values are not committed, but still exist in the resolving process and its configured delivery boundary. Ephemeral means unstable Generated and prompted values are shared only within one resolution or child invocation. Do not use `null` for credentials that another process, machine, or later invocation must retrieve. Use a writable provider for those values. # 1Password Provider > 1Password secrets management integration The [1Password](https://1password.com/) provider integrates with 1Password for team-based secret management with advanced access controls. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | --------------------------------------------------------------------------- | | Provider | `onepassword` | | URI | `onepassword://[account@]vault` | | Access | Read and write | | Best for | Team-managed secrets in 1Password vaults | | Authentication | Desktop app integration, a service account token, or a legacy shell session | | Default storage | Secure Note `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider onepassword://Production Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to onepassword (profile: default) # Get a secret $ secretspec get DATABASE_URL --provider onepassword://Production # Run with secrets $ secretspec run --provider onepassword://Production -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * 1Password CLI (`op`) * 1Password account Choose one of the following authentication methods. ### Desktop app integration (recommended for local dev) [Section titled “Desktop app integration (recommended for local dev)”](#desktop-app-integration-recommended-for-local-dev) In the 1Password desktop app, open **Settings → Developer** and enable **“Integrate with 1Password CLI”**. Once enabled, `op` calls made by `secretspec` are unlocked through the desktop app via biometrics (Touch ID / Windows Hello / system password) — no shell session needed and nothing expires from under you. Under desktop integration, `op whoami` reports `account is not signed in` even when secret access works, so `secretspec` probes auth via `op vault list` instead. It also strips any `OP_SESSION_*` environment variables from spawned `op` processes, so a stale `eval $(op signin)` session in your shell can’t shadow the desktop integration. #### Linux note [Section titled “Linux note”](#linux-note) On Linux, the desktop integration requires the `op` binary to be in the `onepassword-cli` group with the setgid bit set — the desktop app verifies the caller’s GID over its unlock socket. On NixOS this is handled automatically by `programs._1password.enable = true`. A plain `pkgs._1password-cli` install (e.g. via `nix-env` or Home Manager only) does **not** carry the setgid bit and desktop integration will fail; use the NixOS module, or fall back to a service account token for headless setups. ### Service account token [Section titled “Service account token”](#service-account-token) In SecretSpec 0.15 and later, you can declare the token as a [provider credential](/reference/provider-credentials/), for example to load it from your keyring: secretspec.toml ```toml [providers] op = { uri = "onepassword://Production", credentials = { service_account_token = "keyring" } } ``` When no explicit `service_account_token` is supplied, the provider falls back to `OP_SERVICE_ACCOUNT_TOKEN`. The `onepassword+token://` scheme selects service account authentication and takes the token from one of those two sources. Caution From SecretSpec 0.19 on, the token may not be written into the URI itself (`onepassword+token://token@vault` and `onepassword+token://account:token@vault` are rejected). A URI reaches committed manifests, shell history, and CI logs, so the token belongs in a provider credential or the environment. Keep the scheme and drop the token: `onepassword+token://Production`. ### Manual signin (legacy) [Section titled “Manual signin (legacy)”](#manual-signin-legacy) Run `eval $(op signin)` to set per-shell `OP_SESSION_*` tokens. These expire after 30 minutes of inactivity; if they expire mid-session, `secretspec` falls back to desktop integration when available. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ----------------------- | -------------------------- | --------------- | | `service_account_token` | `OP_SERVICE_ACCOUNT_TOKEN` | 0.15+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext onepassword://[account@]vault onepassword+token://vault ``` * `account`: Optional account shorthand * `vault`: Target vault name (defaults to “Private”) The `onepassword+token://` form selects service account authentication; supply the token as the `service_account_token` provider credential or through `OP_SERVICE_ACCOUNT_TOKEN`. The URI names a vault only; item paths (e.g. `onepassword://Vault/item/field`) are rejected. To name a specific item, see [Use existing secrets](#use-existing-secrets). ### URI examples [Section titled “URI examples”](#uri-examples) ```text onepassword://Production onepassword://work@DevVault onepassword+token://Production onepassword:// ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] team = "onepassword://Production" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["team"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) SecretSpec creates Secure Notes named `secretspec/{project}/{profile}/{key}` in the selected vault. The secret value is stored in the note’s `value` field. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) If your secrets already live in 1Password items you manage yourself, name those items with the [`ref`](/reference/configuration/#secret-references) field and route the secret at a vault with `providers`: secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "Production DB", ref = { item = "Postgres", field = "connection-url" }, providers = ["onepassword://Infra"] } STRIPE_API_KEY = { description = "Stripe key", ref = { item = "Stripe", field = "api key" }, providers = ["onepassword://Infra"] } ``` The coordinates translate to 1Password as follows: * `item`: the item title or UUID. Spaces are fine. * `field`: the field label. Without `field`, the item is read like a convention secret (its value or password field), and writes edit the `value` field. * `vault`: overrides the URI’s default vault for this one secret, e.g. `ref = { vault = "Production", item = "infra", field = "token" }`. * `section`: addresses a field inside a section; requires `field`. Writes go through `op item edit`: `secretspec set` updates the referenced field in place, adding the field to the item if it is missing. Items are never created through a ref. A ref does not pin the store. Provider resolution works as usual, so a `providers` chain can fall back to other stores, and `--provider dotenv:.env.fixtures` redirects these secrets to a fixtures file during tests. Native reference strings from the 1Password app’s **Copy Secret Reference** (`op://vault/item/field`) are not accepted directly; pasting one into `ref` produces an error that spells out the translation: ```toml # op://Infra/Postgres/connection-url becomes: DATABASE_URL = { description = "Production DB", ref = { vault = "Infra", item = "Postgres", field = "connection-url" }, providers = ["onepassword://Infra"] } ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Profile configuration [Section titled “Profile configuration”](#profile-configuration) secretspec.toml ```toml [providers] development = "onepassword://Development" production = "onepassword://Production" [profiles.development.defaults] providers = ["development"] [profiles.production.defaults] providers = ["production"] ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Set token $ export OP_SERVICE_ACCOUNT_TOKEN="ops_eyJ..." # Run command $ secretspec run --provider onepassword://Production -- deploy ``` # OpenBao Provider > OpenBao integration, available in SecretSpec 0.17+ **New in version 0.17** The [OpenBao](https://openbao.org/) provider integrates with OpenBao’s KV (Key-Value) secrets engine using OpenBao’s own provider identity and configuration conventions. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ------------------------------------------------------------- | | Provider | `openbao` (0.17+) | | URI | `openbao://[namespace@]host[:port][/mount][?options]` | | Access | Read, write, and delete; secret references are read-only | | Best for | Open-source, policy-controlled secret infrastructure | | Authentication | Token, AppRole, or JWT/OIDC | | Build feature | `openbao` (0.17+) | | Default storage | KV path `secretspec/{project}/{profile}/{key}`, field `value` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash $ export BAO_TOKEN=hvs.your-token-here $ secretspec set DATABASE_URL --provider openbao://bao.example.com:8200 Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to openbao (profile: default) ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * A running OpenBao server * Authentication credentials * KV secrets engine enabled (v1 or v2) * Build with `--features openbao` ### Environment compatibility [Section titled “Environment compatibility”](#environment-compatibility) For the variables defined by the OpenBao CLI, the provider follows its documented convention: `BAO_ADDR`, `BAO_NAMESPACE`, `BAO_TOKEN`, and `BAO_TOKEN_PATH` take precedence over their `VAULT_*` counterparts. SecretSpec additionally defines OpenBao-prefixed provider inputs for AppRole and JWT authentication. These are consumed by SecretSpec, not by the `bao` CLI, and retain the corresponding `VAULT_*` names as compatibility fallbacks. ### Token authentication [Section titled “Token authentication”](#token-authentication) Token authentication is the default. SecretSpec checks these sources in order: 1. The alias’s `token` provider credential 2. `BAO_TOKEN`, then `VAULT_TOKEN` 3. The file selected by `BAO_TOKEN_PATH`, then `VAULT_TOKEN_PATH` 4. The OpenBao CLI’s default `~/.vault-token` ```bash $ export BAO_TOKEN=hvs.your-token-here ``` ### AppRole authentication [Section titled “AppRole authentication”](#approle-authentication) **Changed in version 0.18** `BAO_SECRET_ID` (or its `VAULT_SECRET_ID` fallback) and the `secret_id` provider credential may be omitted when the AppRole is configured with `bind_secret_id=false`. SecretSpec then sends only `role_id` and lets OpenBao apply the role’s remaining login constraints. Select AppRole with `?auth=approle`. OpenBao roles bind a SecretID by default, so the usual configuration provides both inputs: ```bash $ export BAO_ROLE_ID=your-role-id $ export BAO_SECRET_ID=your-secret-id ``` These are SecretSpec provider inputs, not OpenBao CLI variables. `VAULT_ROLE_ID` and `VAULT_SECRET_ID` remain accepted as fallbacks. Prefer semantic provider credentials when configuring an alias: secretspec.toml ```toml [providers.bao_approle] uri = "openbao://bao.example.com:8200/secret?auth=approle" [providers.bao_approle.credentials] role_id = { provider = "onepassword", ref = { vault = "Infra", item = "bao-approle", field = "role_id" } } secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "bao-approle", field = "secret_id" } } ``` Disabling SecretID binding removes AppRole’s usual second credential. Keep the server default unless the workload deliberately relies on another trust boundary, such as a tightly controlled Agent host and network constraints. ### Custom authentication mounts [Section titled “Custom authentication mounts”](#custom-authentication-mounts-018) **New in version 0.18** AppRole and JWT methods mounted somewhere other than their defaults can be selected with `?auth_mount=`. The value is relative to `/v1/auth`: ```text openbao://bao.example.com:8200/secret?auth=approle&auth_mount=platform-approle openbao://bao.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=ci ``` The provider logs in at `/v1/auth/platform-approle/login` and `/v1/auth/ci-jwt/login`, respectively. The KV mount remains the provider URI path (`secret` in these examples). ### JWT / OIDC authentication [Section titled “JWT / OIDC authentication”](#jwt--oidc-authentication) Select JWT with `?auth=jwt`. The provider performs the `auth/jwt/login` exchange itself. The JWT comes from SecretSpec’s `BAO_JWT` input, then the `VAULT_JWT` compatibility fallback. Otherwise, in a GitHub Actions or Forgejo job with `id-token: write`, the provider mints one from the runner’s OIDC identity. Starting with SecretSpec 0.18, the role may be omitted when the JWT auth mount has a `default_role`; OpenBao then selects that role during login. An explicit SecretSpec role still takes precedence. * `?role=`, `BAO_JWT_ROLE`, or `VAULT_JWT_ROLE`; optional with a server-configured `default_role` (0.18+) * `?audience=`, `BAO_JWT_AUDIENCE`, or `VAULT_JWT_AUDIENCE` ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ----------- | ----------------------------------- | --------------- | | `role_id` | `BAO_ROLE_ID` → `VAULT_ROLE_ID` | 0.17+ | | `secret_id` | `BAO_SECRET_ID` → `VAULT_SECRET_ID` | 0.17+ | | `token` | `BAO_TOKEN` → `VAULT_TOKEN` | 0.17+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text openbao://[namespace@]host[:port][/mount][?key=value&...] ``` * `host[:port]`: OpenBao address (falls back through `BAO_ADDR`, `VAULT_ADDR`) * `mount`: KV engine mount path (default: `secret`) * `namespace@`: Optional namespace (falls back through `BAO_NAMESPACE`, `VAULT_NAMESPACE`) * `?auth=approle`: Use AppRole authentication (default: `token`) * `?auth=jwt`: Use JWT/OIDC authentication; a server-configured `default_role` can supply the role when using SecretSpec 0.18+ * `?auth_mount=` (0.18+): Non-default AppRole or JWT mount beneath `/v1/auth` * `?role=`: OpenBao role for JWT auth * `?audience=`: Audience requested from the CI OIDC issuer * `?kv=1`: Use KV v1 (default: v2) * `?tls=false`: Disable TLS for development servers ### Concurrent resolution [Section titled “Concurrent resolution”](#concurrent-resolution) * One HTTP client is reused per provider instance (connection pool / h2 reuse). * Concurrent unique-address fetches are capped at 8 by default. * Override the cap with `SECRETSPEC_PROVIDER_CONCURRENCY` (integer ≥ 1) when your OpenBao proxy tolerates more or less parallel load. ### URI examples [Section titled “URI examples”](#uri-examples) ```text openbao://bao.example.com:8200/secret openbao://team-a@bao.example.com:8200/secret openbao://bao.example.com:8200/secret?auth=approle # SecretSpec 0.18+ openbao://bao.example.com:8200/secret?auth=approle&auth_mount=platform-approle openbao://bao.example.com:8200/secret?auth=jwt&role=ci # SecretSpec 0.18+, with default_role configured on the JWT auth mount openbao://bao.example.com:8200/secret?auth=jwt ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] bao_prod = "openbao://bao.example.com:8200/secret" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["bao_prod"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Each secret is stored at `secretspec/{project}/{profile}/{key}` under the configured mount, with its value in a field named `value`. For KV v2, `DATABASE_URL` for project `myapp` and profile `production` is read from `GET /v1/secret/data/secretspec/myapp/production/DATABASE_URL`. ## Provider caching [Section titled “Provider caching”](#provider-caching) A KV v2 mount can hold a [cached provider route’s](/concepts/providers/caching/) entries. OpenBao expires them itself: the cache’s `max_age` is written to the path’s `delete_version_after` metadata, so a cached copy of another store’s secret stops existing at that age even if SecretSpec never runs again. This needs write access to the path’s metadata as well as its data, and KV v1 is refused as a cache because it has no expiry. Deleting — [`cache clear`](/reference/cli/#cache-clear-017) and automatic invalidation — removes the KV path’s metadata and every version, and is confined to entries SecretSpec owns: a secret reference is never deleted, since the path it names is managed outside SecretSpec. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing KV entry: `item` is the path relative to the mount, and `field` selects the field to read. References are **read-only** so a single-field write cannot overwrite the entry’s other fields. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "myapp/config", field = "db_url" }, providers = ["openbao://bao.example.com:8200/secret"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) AppRole avoids placing a user token in the CI environment: ```bash $ export BAO_ROLE_ID="$CI_ROLE_ID" $ export BAO_SECRET_ID="$CI_SECRET_ID" $ secretspec export --format gha --provider "openbao://bao.example.com:8200/secret?auth=approle" ``` With GitHub Actions or Forgejo Actions `id-token: write`, JWT/OIDC avoids a static authentication credential: ```bash $ secretspec export --format gha --provider "openbao://bao.example.com:8200/secret?auth=jwt&role=ci" ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### KV version 1 [Section titled “KV version 1”](#kv-version-1) ```bash $ secretspec set DATABASE_URL --provider "openbao://bao.example.com:8200/secret?kv=1" ``` ### OpenBao namespaces [Section titled “OpenBao namespaces”](#openbao-namespaces) ```bash $ secretspec check --provider openbao://team-a@bao.example.com:8200/secret $ export BAO_NAMESPACE=team-a $ secretspec check --provider openbao://bao.example.com:8200/secret ``` ### Development mode [Section titled “Development mode”](#development-mode) ```bash $ bao server -dev -dev-root-token-id="dev-only-token" $ export BAO_TOKEN="dev-only-token" $ secretspec check --provider "openbao://127.0.0.1:8200/secret?tls=false" ``` # Pass Provider > Unix password manager integration with GPG encryption The [Pass](https://www.passwordstore.org/) provider stores secrets using the Unix password manager `pass` (password-store). Secrets are GPG-encrypted for secure local development. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | --------------------------------------------------- | | Provider | `pass` | | URI | `pass://[folder_prefix][?store_dir=/path/to/store]` | | Access | Read and write | | Best for | Local, GPG-encrypted secret storage | | Authentication | The GPG key configured for the password store | | Default storage | `secretspec/{project}/{profile}/{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider pass Enter value for DATABASE_URL: postgresql://localhost/mydb # Run with secrets $ secretspec run --provider pass -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) ```bash # Debian/Ubuntu $ sudo apt-get install pass # Fedora $ sudo dnf install pass # Arch $ sudo pacman -S pass # macOS $ brew install pass ``` ### Authentication [Section titled “Authentication”](#authentication) SecretSpec uses the GPG identity configured for the password store. Initialize the store once if needed: ```bash $ pass init ``` ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext pass://[folder_prefix][?store_dir=/path/to/store] ``` * `folder_prefix`: Optional path prefix supporting `{project}`, `{profile}`, and `{key}` placeholders. Defaults to `secretspec/{project}/{profile}/{key}`. * `store_dir`: Optional password store directory. When set, it is exported as `PASSWORD_STORE_DIR` for every `pass` invocation, overriding the default `~/.password-store`. The variable is scoped to the spawned `pass` process and does not affect secretspec’s own environment. ### URI examples [Section titled “URI examples”](#uri-examples) ```text pass pass://shared/{profile}/{key} pass://?store_dir=/path/to/store ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] local = "pass://" [profiles.default] DATABASE_URL = { description = "Database URL", providers = ["local"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Secrets are stored with a hierarchical path structure: `secretspec/{project}/{profile}/{key}` For example, with project “myapp” and profile “default”: ```bash $ pass show secretspec/myapp/default/DATABASE_URL postgresql://localhost/mydb ``` ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing store entry instead, letting you read credentials you already keep in `pass`: `item` is the entry path (`field` is not supported). Reads and writes target that entry in place. ```toml [profiles.default] GITHUB_TOKEN = { description = "GH token", ref = { item = "github/token" }, providers = ["pass"] } ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Shared secrets [Section titled “Shared secrets”](#shared-secrets) By default, secrets are stored under `secretspec/{project}/{profile}/{key}`, which isolates them per project. To share secrets across projects, use a custom folder prefix via the URI: \~/.config/secretspec/config.toml ```toml [defaults.providers] shared = "pass://secretspec/shared/{profile}/{key}" ``` The URI supports `{project}`, `{profile}`, and `{key}` placeholders. By omitting `{project}`, multiple projects can read and write the same pass entry: ```toml # secretspec.toml (in project-A and project-B) [profiles.default] ARTIFACTORY_USER = { description = "Artifactory user", providers = ["shared"] } ``` Both projects will resolve `ARTIFACTORY_USER` from pass entry `secretspec/shared/default/ARTIFACTORY_USER`. # Passbolt Provider > Store and read SecretSpec values in a self-hosted Passbolt server **New in version 0.19** The [Passbolt](https://www.passbolt.com/) provider reads and writes resources in a self-hosted Passbolt server through the community-maintained [`go-passbolt-cli`](https://github.com/passbolt/go-passbolt-cli). ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | --------------------------------------------------------------------------------------------------- | | Provider | `passbolt` (0.19+) | | URI | `passbolt://[?server=URL][&folder=ID][&template=PATTERN]` | | Access | Read and write | | Best for | Teams using a self-hosted Passbolt server | | Authentication | OpenPGP private key and passphrase, through provider credentials or `go-passbolt-cli` configuration | | Availability | Built into SecretSpec 0.19+ | | Default storage | Resource `secretspec/{project}/{profile}/{key}`, field `password` | ## Quick start [Section titled “Quick start”](#quick-start) Complete [Setup](#setup) first, then use the provider alias from the project configuration below: ```bash # Store a secret in Passbolt $ secretspec set DATABASE_URL --provider passbolt_team # Read it back $ secretspec get DATABASE_URL --provider passbolt_team # Resolve the active profile and run a command $ secretspec run --provider passbolt_team -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * SecretSpec 0.19 or newer * A Passbolt account with permission to read the selected resources and update resources when using `set` * [`go-passbolt-cli`](https://github.com/passbolt/go-passbolt-cli) installed as `passbolt` on `PATH` When the executable has another name or location, set `SECRETSPEC_PASSBOLT_CLI_PATH` to its path. For example, `go install` currently names the executable `go-passbolt-cli`: ```bash $ export SECRETSPEC_PASSBOLT_CLI_PATH="$(go env GOPATH)/bin/go-passbolt-cli" ``` Run `passbolt verify` once when your deployment uses the CLI’s server verification workflow. ### Authentication with provider credentials [Section titled “Authentication with provider credentials”](#authentication-with-provider-credentials) SecretSpec 0.19+ declares the OpenPGP `private_key` and `passphrase` as [provider credentials](/reference/provider-credentials/). Load both from a bootstrap provider instead of putting them in `secretspec.toml` or the Passbolt URI: secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.passbolt_team] uri = "passbolt://?server=https://pass.example.com" credentials = { private_key = "bootstrap", passphrase = "bootstrap" } [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["passbolt_team"] } ``` Store the two declared credentials once: ```bash $ secretspec config provider login passbolt_team Enter private_key for provider 'passbolt_team' (source: bootstrap): **** Enter passphrase for provider 'passbolt_team' (source: bootstrap): **** ``` The provider passes the private key and passphrase only to the child process’s environment, not its command-line arguments. ### Environment fallback [Section titled “Environment fallback”](#environment-fallback) For environments without a bootstrap provider, use these fallbacks: ```bash $ export SECRETSPEC_PASSBOLT_SERVER=https://pass.example.com $ export SECRETSPEC_PASSBOLT_PRIVATE_KEY="$(cat private-key.asc)" $ export SECRETSPEC_PASSBOLT_PASSPHRASE="$CI_PASSBOLT_PASSPHRASE" ``` `SECRETSPEC_PASSBOLT_PRIVATE_KEY_FILE` can select a private-key file instead of an inline key. An explicit `private_key` provider credential takes precedence; without one, the key-file fallback takes precedence over `SECRETSPEC_PASSBOLT_PRIVATE_KEY`. ### Use the CLI configuration [Section titled “Use the CLI configuration”](#use-the-cli-configuration) Alternatively, save the server, key, passphrase, and optional MFA settings in the CLI’s own configuration: ```bash $ passbolt configure \ --serverAddress https://pass.example.com \ --userPrivateKeyFile private-key.asc \ --userPassword "$PASSBOLT_PASSPHRASE" ``` When none of the provider credentials or `SECRETSPEC_PASSBOLT_*` fallbacks are set, SecretSpec inherits that CLI configuration. For MFA accounts, configure `go-passbolt-cli` for non-interactive TOTP before using it through SecretSpec. The CLI supports TOTP MFA only; accounts whose policy requires Duo or YubiKey cannot authenticate through this provider. An interactive password or TOTP prompt cannot be answered by a provider operation, so SecretSpec reports an actionable error instead of the CLI’s raw end-of-file message. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ------------- | --------------------------------- | --------------- | | `private_key` | `SECRETSPEC_PASSBOLT_PRIVATE_KEY` | 0.19+ | | `passphrase` | `SECRETSPEC_PASSBOLT_PASSPHRASE` | 0.19+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text passbolt://[?server=URL][&folder=ID][&template=PATTERN] ``` * `server` overrides the server stored in the CLI configuration or `SECRETSPEC_PASSBOLT_SERVER`. * `folder` scopes resource-name lookups and creates new convention resources inside that folder. * `template` replaces the complete convention resource name. It supports `{project}`, `{profile}`, and `{key}` and defaults to `secretspec/{project}/{profile}/{key}`. ### URI examples [Section titled “URI examples”](#uri-examples) ```text passbolt:// passbolt://?server=https://pass.example.com passbolt://?folder=a9230ec4-5507-4870-b8b5-b3f500587e4c passbolt://?template=teams/{project}/{profile}/{key} passbolt://?server=https://pass.example.com&folder=a9230ec4-5507-4870-b8b5-b3f500587e4c&template=teams/{project}/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] bootstrap = "keyring://" [providers.passbolt_team] uri = "passbolt://?server=https://pass.example.com&folder=a9230ec4-5507-4870-b8b5-b3f500587e4c" credentials = { private_key = "bootstrap", passphrase = "bootstrap" } [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["passbolt_team"] } API_KEY = { description = "API key", providers = ["passbolt_team"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Every convention secret maps to one Passbolt resource: ```text resource name: secretspec/{project}/{profile}/{key} field: password ``` For project `storefront`, profile `production`, and key `DATABASE_URL`, the resource is named `secretspec/storefront/production/DATABASE_URL`. Exact-name duplicates are rejected as ambiguous; SecretSpec never chooses one arbitrarily. A custom `template` may intentionally omit a placeholder, but doing so reduces isolation. Omitting `{key}`, for example, makes every declaration in that project/profile target the same resource and password field. ## Use existing resources [Section titled “Use existing resources”](#use-existing-resources) A secret’s [`ref`](/reference/configuration/#secret-references) selects an existing Passbolt resource by UUID or exact name. The optional `field` is one of `password` (the default), `username`, `uri`, or `description`: secretspec.toml ```toml [providers] passbolt_team = "passbolt://?server=https://pass.example.com" [profiles.production] STRIPE_SECRET_KEY = { description = "Stripe key", providers = ["passbolt_team"], ref = { item = "a9230ec4-5507-4870-b8b5-b3f500587e4c" } } SERVICE_USER = { description = "Service account user", providers = ["passbolt_team"], ref = { item = "Payments service account", field = "username" } } ``` UUIDs are recommended because Passbolt permits duplicate names. Reads and writes target the existing resource in place. A write through `ref` never creates a missing name- or UUID-addressed resource; create and share it in Passbolt first. These coordinates cover the standard fields exposed by `go-passbolt-cli`. Passbolt resource types that omit the selected field read as unset, and custom resource-type fields are not addressable through this provider. ## Discover declarations [Section titled “Discover declarations”](#discover-declarations) SecretSpec 0.19+ can create a manifest from convention resources without reading their values: ```bash $ secretspec init \ --from "passbolt://?server=https://pass.example.com&folder=a9230ec4-5507-4870-b8b5-b3f500587e4c" \ --project storefront \ --profile production ``` Discovery requires `?folder=` because the CLI cannot safely scope account-wide listings by a resource-name prefix. SecretSpec renders the configured `template` for that project and profile, lists only that folder, and turns the part represented by `{key}` into secret names. The template must contain `{key}` exactly once. Nested matches and duplicates are rejected. ## CI/CD [Section titled “CI/CD”](#cicd) Prefer provider credentials sourced from a CI bootstrap provider. When that is not available, inject the inline private key and passphrase through protected CI variables: ```bash $ export SECRETSPEC_PASSBOLT_PRIVATE_KEY="$CI_PASSBOLT_PRIVATE_KEY" $ export SECRETSPEC_PASSBOLT_PASSPHRASE="$CI_PASSBOLT_PASSPHRASE" $ secretspec run --provider "passbolt://?server=https://pass.example.com" -- ./deploy ``` Grant the CI identity read access only to the resources it needs. Grant update permission only when the job must run `set` or persist generated values. ## Security considerations and limitations [Section titled “Security considerations and limitations”](#security-considerations-and-limitations) * Provider credentials and inline authentication material are passed through the child environment and are never included in the reported provider URI. * `go-passbolt-cli` currently accepts resource values for create/update only as command-line flags. Values written by `secretspec set`, `check`, generation, or import are therefore visible in the `passbolt` child process’s argv (for example through `ps` or `/proc//cmdline`) until that process exits. Use the provider read-only when this exposure is unacceptable. * Empty writes are rejected because the CLI treats empty update fields as a successful no-op. * Name lookups list the configured folder, or the accessible account when no folder is configured. Prefer UUID refs and a folder scope in large accounts. * A folder limits lookup and creation, but it is not an independent permission boundary. Passbolt evaluates access to an existing item from that resource’s permissions, which may differ from the folder’s permissions. # Proton Pass Provider > Proton Pass integration via the official pass-cli Why `pass-cli` upgrades can break this provider Proton Pass has no public API and no SDK, so this provider drives the official `pass-cli` executable and inherits whatever that executable does. Proton support confirmed in August 2026 that its support for `pass-cli` covers only what the [`pass-cli` changelog](https://github.com/protonpass/pass-cli/blob/main/CHANGELOG.md) publishes, that backward incompatible changes can ship in a patch release with no advance notice, and that it is reasonable to expect more of them. Three such releases have already changed behaviour SecretSpec depends on, most recently `pass-cli` 2.2.4, which broke every Proton Pass operation until SecretSpec 0.19. See [`pass-cli` compatibility](#pass-cli-compatibility). The practical consequence is that upgrading `pass-cli` can break secret resolution on a machine where nothing about SecretSpec changed, and the repair then waits on a SecretSpec release. Install a `pass-cli` version you have tested and upgrade it deliberately, as described in [Pinning a `pass-cli` version](#pinning-a-pass-cli-version). This is unfortunate. Providers built on a versioned API or on a vendor interface with a compatibility policy do not break this way. If Proton publishes a stable API, an SDK, or a compatibility policy for `pass-cli`, we would build on it instead. The [Proton Pass](https://proton.me/pass) provider integrates with Proton Pass for end-to-end encrypted cloud secret storage. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ------------------------------------------------------------------------------------------------------- | | Provider | `protonpass` | | URI | `protonpass://[vault_name[/title-template]]` | | Access | Read and write | | Best for | End-to-end encrypted cloud storage through Proton Pass | | Authentication | A `pass-cli` login or personal access token | | Default storage | Note item `{project}/{profile}/{key}` in the `secretspec` vault | | Requires | Official `pass-cli`, pinned to a version you have tested (see [compatibility](#pass-cli-compatibility)) | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # Set a secret $ secretspec set DATABASE_URL --provider protonpass://Personal Enter value for DATABASE_URL: postgresql://localhost/mydb # Get a secret $ secretspec get DATABASE_URL --provider protonpass://Personal # Run with secrets $ secretspec run --provider protonpass://Personal -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * Proton Pass CLI (`pass-cli`) - download from [proton.me/pass/download](https://proton.me/pass/download) * A Proton account, signed in via `pass-cli login` * A vault to store secrets in (e.g. `pass-cli vault create secretspec`) * A `pass-cli` version that works with your SecretSpec release, see [`pass-cli` compatibility](#pass-cli-compatibility) ### Authentication [Section titled “Authentication”](#authentication) For local use, sign in interactively: ```bash $ pass-cli login ``` For CI, use a personal access token as shown in [CI/CD](#cicd). ## `pass-cli` compatibility [Section titled “pass-cli compatibility”](#pass-cli-compatibility) Each of these `pass-cli` releases changed behaviour the provider relies on: | `pass-cli` | What changed | SecretSpec | | ------------------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------- | | 2.0.3 (2026-05-19) | `item list` output shape | Handled in 0.12.1+ | | 2.1.0 (2026-05-20) | Agent sessions reject audited item operations that carry no reason | Handled in 0.12.0+, see [Agent sessions](#agent-sessions) | | 2.2.4 (2026-07-31) | `pass-cli test` removed | Handled in 0.19+ ([#279](https://github.com/cachix/secretspec/issues/279)) | SecretSpec probes the session once per run before any read or write. SecretSpec 0.18.0 and earlier probe with `pass-cli test`, so on `pass-cli` 2.2.4 and later every Proton Pass operation fails with: ```text Provider operation failed: error: unrecognized subcommand 'test' ``` SecretSpec 0.19+ tries `pass-cli info` and falls back to `pass-cli test`, so it works with every `pass-cli` release regardless of which check that release carries. `info` is preferred because it runs behind the CLI’s authentication gate and so reports whether a valid session is present, while `test` only proved that Proton’s servers were reachable. A `pass-cli` carrying neither is reported as incompatible with your SecretSpec release rather than surfacing the CLI’s usage text. On SecretSpec 0.18.0 and earlier, use `pass-cli` 2.2.3, the last release published before `pass-cli test` was removed. ### Pinning a `pass-cli` version [Section titled “Pinning a pass-cli version”](#pinning-a-pass-cli-version) Install a specific release instead of tracking the latest build, and point SecretSpec at it with `SECRETSPEC_PROTONPASS_CLI_PATH`: ```bash $ curl -Lo ~/.local/bin/pass-cli-2.2.3 \ https://github.com/protonpass/pass-cli/releases/download/2.2.3/pass-cli-linux-x86_64 $ chmod +x ~/.local/bin/pass-cli-2.2.3 $ export SECRETSPEC_PROTONPASS_CLI_PATH="$HOME/.local/bin/pass-cli-2.2.3" ``` Every [release](https://github.com/protonpass/pass-cli/releases) publishes a `.sha256` file next to each binary; verify it before use. Pin the same version in CI rather than installing the latest `pass-cli` on each run, and treat a `pass-cli` upgrade as a change worth testing: run `secretspec check` against the new version before rolling it out. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext protonpass://[vault_name[/title-template]] ``` * `vault_name`: Target vault (defaults to `secretspec`) * `title-template`: Item title pattern supporting `{project}`, `{profile}`, `{key}` placeholders ### URI examples [Section titled “URI examples”](#uri-examples) ```text # Default vault ("secretspec") protonpass:// # Specific vault protonpass://Work # Specific vault and custom title template protonpass://Work/{project}/{profile}/{key} ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] team = "protonpass://Work" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["team"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Secrets are stored as note items. The vault defaults to `secretspec`, and the item title defaults to `{project}/{profile}/{key}`. The URI can select another vault or replace the title template. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing item instead: `item` is the exact item title, whose note is read (`field` is not supported). Reads and writes target that item in place. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "Production Database" }, providers = ["protonpass://Work"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash # Create a token $ pass-cli personal-access-token create --name ci --expiration 1y # Authenticate in CI (store the token as a CI secret) $ pass-cli login --pat $PROTON_PASS_PAT $ secretspec run -- deploy ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Agent sessions [Section titled “Agent sessions”](#agent-sessions) `pass-cli` 2.1.0 introduced agent sessions, which require a `PROTON_PASS_AGENT_REASON` to be set for audited item operations (reading, creating, and deleting items). SecretSpec sets this automatically, so existing secrets resolve correctly under an agent session. The reason recorded in the Proton Pass audit log is resolved in this order: 1. The `--reason` flag (or `SECRETSPEC_REASON` environment variable): ```bash $ secretspec run --reason "Deploying app from CI" -- ./deploy.sh ``` When using the Rust SDK, set it for the session with `with_reason`: ```rust use secretspec::Secrets; let spec = Secrets::load()?.with_reason("Deploying app from CI"); ``` 2. The `PROTON_PASS_AGENT_REASON` environment variable read by `pass-cli`: ```bash $ export PROTON_PASS_AGENT_REASON="Deploying app from CI" ``` 3. A default that identifies the secretspec version (e.g. `secretspec/0.11.0 (https://secretspec.dev)`). To force a meaningful reason instead of falling back to the default, use the [`require_reason`](/reference/configuration/#requiring-a-reason-for-secret-access) policy in `secretspec.toml`. It defaults to `"agents"`, so sessions SecretSpec detects as AI agents must explain why they read a secret. Detection is heuristic; set it to `true` to require a reason from every SecretSpec caller. secretspec then refuses operations through SecretSpec that do not supply an explicit reason. # Scaleway Secret Manager Provider > Scaleway Secret Manager integration **New in version 0.17** The [Scaleway Secret Manager](https://www.scaleway.com/en/secret-manager/) provider stores secrets in Scaleway Secret Manager through its `v1beta1` REST API. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | -------------------------------------------------------------- | | Provider | `scaleway` | | URI | `scaleway://[REGION][?project_id=UUID&path=/folder]` | | Access | Read and write; secret references are read-only | | Best for | Workloads and teams on Scaleway | | Authentication | API secret key (`X-Auth-Token`) | | Build feature | `scaleway` | | Default storage | folder `[{base}/]secretspec/{project}/{profile}`, name `{key}` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash $ export SCW_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx $ export SCW_DEFAULT_PROJECT_ID=11111111-2222-3333-4444-555555555555 # Set a secret $ secretspec set DATABASE_URL --provider scaleway://fr-par Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to scaleway (profile: default) # Run with secrets $ secretspec run --provider scaleway://fr-par -- npm start ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Scaleway account with Secret Manager enabled * An API key (access key + secret key) with Secret Manager permissions * Build with `--features scaleway` ### Authentication [Section titled “Authentication”](#authentication) The provider authenticates with a Scaleway API **secret key**, sent in the `X-Auth-Token` header. It is read from, in order: 1. The `secret_key` provider credential 2. The `SCW_SECRET_KEY` environment variable The target project is read from `?project_id=` in the URI, falling back to `SCW_DEFAULT_PROJECT_ID`. The region is the URI host, falling back to `SCW_DEFAULT_REGION`, and finally `fr-par`. Secret Manager is available in the `fr-par`, `nl-ams`, and `pl-waw` regions. ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ------------ | -------------------- | --------------- | | `secret_key` | `SCW_SECRET_KEY` | 0.17+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```plaintext scaleway://[REGION][?project_id=UUID][&path=/folder] ``` * `REGION`: Scaleway region (e.g. `fr-par`). If omitted, `SCW_DEFAULT_REGION` is used, then `fr-par`. * `project_id`: Target project UUID. If omitted, `SCW_DEFAULT_PROJECT_ID` is used. * `path`: Optional base folder prepended to the convention hierarchy. Defaults to `/` (root). ### URI examples [Section titled “URI examples”](#uri-examples) ```text scaleway://fr-par scaleway://nl-ams?project_id=11111111-2222-3333-4444-555555555555 scaleway://fr-par?project_id=11111111-2222-3333-4444-555555555555&path=/myteam scaleway:// ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) The region and project usually vary per environment, so they are a natural fit for a checked-in [provider alias](/reference/configuration/) in `secretspec.toml` (the secret key stays in the environment, never the URI): ```toml [providers] scw = "scaleway://fr-par?project_id=11111111-2222-3333-4444-555555555555" ``` secretspec.toml ```toml [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["scw"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Scaleway secret names may not contain `/` — that character separates folders — so the SecretSpec convention lives in the folder hierarchy rather than the name. A secret is stored in the folder `[{base}/]secretspec/{project}/{profile}` with the key as its name. For example, `DATABASE_URL` in project `myapp` and profile `production` is stored at folder `/secretspec/myapp/production` with name `DATABASE_URL`. With `?path=/myteam`, the folder becomes `/myteam/secretspec/myapp/production`. Each write appends a new secret version; reads return the latest enabled version. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing Scaleway secret instead. `item` is the secret’s absolute path (folder + name, e.g. `/prod/db-url`); the optional `field` selects one key of a JSON (`key_value`) secret, and the optional `version` pins a revision number (default: latest enabled). References are **read-only** in this provider. ```toml [profiles.production] # Whole secret value, latest enabled revision DATABASE_URL = { description = "DB", ref = { item = "/prod/database-url" }, providers = ["scaleway://fr-par"] } # One key of a JSON secret, pinned to revision 3 DB_PASSWORD = { description = "DB pw", ref = { item = "/prod/db-credentials", field = "password", version = "3" }, providers = ["scaleway://fr-par"] } ``` ## CI/CD [Section titled “CI/CD”](#cicd) ```bash $ export SCW_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx $ export SCW_DEFAULT_PROJECT_ID=11111111-2222-3333-4444-555555555555 $ export SCW_DEFAULT_REGION=fr-par $ secretspec run --provider scaleway://fr-par -- deploy ``` # SOPS Provider > Store secrets in SOPS-encrypted files **New in version 0.17** The [`sops`](https://getsops.io) provider reads and writes secrets in files encrypted with SOPS. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | -------------- | --------------------------------------------------------- | | Provider | `sops` | | URI | `sops://[PATH][?options]` | | Access | Read and write | | Best for | Encrypted files stored alongside a project | | Authentication | SOPS key configuration or SecretSpec provider credentials | | Build feature | `sops` | ## Quick start [Section titled “Quick start”](#quick-start) After installing SOPS and configuring a creation rule or another encryption method, write a secret to an encrypted YAML file: ```bash $ secretspec set DATABASE_URL --provider sops://secrets.enc.yaml ``` Use the same provider to inject the secret into a command: ```bash $ secretspec run --provider sops://secrets.enc.yaml -- npm start ``` See [Setup](#setup) if SOPS does not already know which keys to use. ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * The SOPS CLI available within the environment: * [Manually download a release binary](https://github.com/getsops/sops/releases) * [Use the SOPS Nix package](https://search.nixos.org/packages?channel=unstable\&query=sops#show=sops) * Install with a package manager: ```bash # Homebrew $ brew install sops # Arch $ sudo pacman -S sops ``` * The keys or credentials required by the selected SOPS encryption method * Build SecretSpec with `--features sops` when the provider is not included by your package For a new file, SOPS needs either encryption options in the provider URI or a matching creation rule in `.sops.yaml`. Generate an age identity for your own project and print its recipient: ```bash $ age-keygen -o key.txt $ age-keygen -y key.txt age1... $ export SOPS_AGE_KEY_FILE="$PWD/key.txt" ``` Keep `key.txt` secret and out of version control. `SOPS_AGE_KEY_FILE` makes the identity available for decryption. Copy the `age1...` recipient printed by the second command into the creation rule (the value below is a placeholder, not a usable recipient): .sops.yaml ```yaml creation_rules: - path_regex: secrets\.enc\.yaml$ age: "YOUR_AGE_RECIPIENT" ``` ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) Secret values used to authenticate SOPS belong in a provider alias’s `credentials` map, not in the SOPS URI. | Credential | Environment fallback | Available since | | --------------------------- | --------------------------- | --------------- | | `age_key` | `SOPS_AGE_KEY` | 0.17+ | | `aws_secret_access_key` | `AWS_SECRET_ACCESS_KEY` | 0.17+ | | `azure_client_secret` | `AZURE_CLIENT_SECRET` | 0.17+ | | `hc_vault_token` | `VAULT_TOKEN` | 0.17+ | | `huawei_sdk_ak` | `HUAWEICLOUD_SDK_AK` | 0.17+ | | `huawei_sdk_sk` | `HUAWEICLOUD_SDK_SK` | 0.17+ | | `google_oauth_access_token` | `GOOGLE_OAUTH_ACCESS_TOKEN` | 0.17+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. For example, this alias loads an age identity from the system keyring and passes it only to the SOPS child process. Replace `YOUR_AGE_RECIPIENT` with the recipient printed by `age-keygen -y` during setup: secretspec.toml ```toml [providers.sops_age] uri = "sops://secrets.enc.yaml?age_recipients=YOUR_AGE_RECIPIENT" [providers.sops_age.credentials] age_key = "keyring" [profiles.production.defaults] providers = ["sops_age"] ``` When a credential is not declared on the alias, SOPS can still use its normal environment variable. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text sops://[path/to/secret][?key=value[&key=value]...] ``` * `path/to/secret` — optional absolute or relative path to the encrypted file; defaults to `secrets.enc.yaml`. Relative paths are resolved from the directory containing `secretspec.toml`, not from the shell’s current directory. * Secrets in multiple files? To represent more than one file, use a templated path. This supports separate SOPS-encrypted files per profile or a hierarchical directory structure. A templated path must include both `{project}` and `{profile}` placeholders. Examples: * `sops://secrets-dir/{project}.{profile}.enc.json` * `sops://secrets-dir/{project}/{profile}.enc.yaml` * `?key=value` — optional query parameter; see [Query parameters](#query-parameters) * `&key=value` — additional parameters Relative `age_key_file`, `age_ssh_private_key_file`, and `sops_config` query values are resolved from the same manifest directory. ### Project configuration [Section titled “Project configuration”](#project-configuration) Use an alias to keep the storage path and encryption settings in `secretspec.toml`: secretspec.toml ```toml [providers] encrypted_file = "sops://secrets/{project}/{profile}.enc.yaml" [profiles.default.defaults] providers = ["encrypted_file"] ``` Templated paths must contain both `{project}` and `{profile}`. Use a single-file URI when every project and profile should share one encrypted document. ### Query parameters [Section titled “Query parameters”](#query-parameters) Except for the SecretSpec-specific `format` parameter, see the [SOPS documentation](https://getsops.io/docs/#usage) for the purpose and usage of each parameter. #### SecretSpec [Section titled “SecretSpec”](#secretspec) | Provider URL Query Parameter Name | Purpose | | --------------------------------- | ----------------------------------------------------------------------------------------------------------- | | format | Overrides the extension-based file format detection. Valid values: `dotenv` `env` `ini` `json` `yaml` `yml` | #### SOPS [Section titled “SOPS”](#sops) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | sops\_config | SOPS\_CONFIG | | sops\_decryption\_order | SOPS\_DECRYPTION\_ORDER | | sops\_editor | SOPS\_EDITOR | | sops\_enable\_local\_keyservice | SOPS\_ENABLE\_LOCAL\_KEYSERVICE | | sops\_keyservice | SOPS\_KEYSERVICE | #### Age [Section titled “Age”](#age) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | age\_key\_cmd | SOPS\_AGE\_KEY\_CMD | | age\_key\_file | SOPS\_AGE\_KEY\_FILE | | age\_recipients | SOPS\_AGE\_RECIPIENTS | | age\_ssh\_private\_key\_cmd | SOPS\_AGE\_SSH\_PRIVATE\_KEY\_CMD | | age\_ssh\_private\_key\_file | SOPS\_AGE\_SSH\_PRIVATE\_KEY\_FILE | #### AWS [Section titled “AWS”](#aws) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | aws\_access\_key\_id | AWS\_ACCESS\_KEY\_ID | | aws\_profile | AWS\_PROFILE | | aws\_region | AWS\_REGION | | kms\_arn | SOPS\_KMS\_ARN | #### GCP [Section titled “GCP”](#gcp) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | gcp\_kms\_client\_type | SOPS\_GCP\_KMS\_CLIENT\_TYPE | | gcp\_kms\_endpoint | SOPS\_GCP\_KMS\_ENDPOINT | | gcp\_kms\_ids | SOPS\_GCP\_KMS\_IDS | | gcp\_kms\_universe\_domain | SOPS\_GCP\_KMS\_UNIVERSE\_DOMAIN | #### Azure [Section titled “Azure”](#azure) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | azure\_client\_id | AZURE\_CLIENT\_ID | | azure\_keyvault\_urls | SOPS\_AZURE\_KEYVAULT\_URLS | | azure\_tenant\_id | AZURE\_TENANT\_ID | #### PGP [Section titled “PGP”](#pgp) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | pgp\_fp | SOPS\_PGP\_FP | #### GPG [Section titled “GPG”](#gpg) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | gpg\_exec | SOPS\_GPG\_EXEC | #### HashiCorp Vault/OpenBao [Section titled “HashiCorp Vault/OpenBao”](#hashicorp-vaultopenbao) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | hc\_vault\_addr | VAULT\_ADDR | | hc\_vault\_allowlist | SOPS\_HC\_VAULT\_ALLOWLIST | #### Huawei Cloud [Section titled “Huawei Cloud”](#huawei-cloud) | Provider URL Query Parameter Name | Corresponding Environment Variable | | --------------------------------- | ---------------------------------- | | huawei\_kms\_ids | SOPS\_HUAWEICLOUD\_KMS\_IDS | | huawei\_sdk\_project\_id | HUAWEICLOUD\_SDK\_PROJECT\_ID | Each query option in the environment-mapping tables is exported to the SOPS child process and overrides the same inherited environment variable. SOPS configuration resolves in this order: `sops_config` in the URI, a `.sops.yaml` discovered from the manifest directory or one of its parents, then an inherited `SOPS_CONFIG`. Provider credentials similarly override their matching secret environment fallbacks, but only for the SOPS child process. ## Storage model [Section titled “Storage model”](#storage-model) The URI shape determines the on-disk layout. This matters when editing a file with `sops` directly and when diagnosing a value set under the wrong profile. For a **single YAML or JSON file**, convention-addressed secrets are written at `[project][profile][key]`. For example, `secretspec set API_KEY --profile production` in project `my-app` writes: secrets.enc.yaml (decrypted view) ```yaml my-app: production: API_KEY: secret-value ``` The equivalent selector passed to `sops set` is `["my-app"]["production"]["API_KEY"]`. Reads also retain compatibility with older `[profile][key]` and root `[key]` layouts, but new convention writes use the fully namespaced path. For a **templated YAML or JSON URI**, `{project}` and `{profile}` already select the file, so the key is flat inside it: ```toml [providers] prod_sops = "sops://secrets/{project}/{profile}.enc.yaml" ``` secrets/my-app/production.enc.yaml (decrypted view) ```yaml API_KEY: secret-value ``` Single-file dotenv is always a flat `API_KEY=value` document. Single-file INI uses the selected profile as its section; templated INI uses `[DEFAULT]` because the profile is already represented by the filename. The following table is the write layout: | Format | Single file | Templated path | | ----------- | ------------------------- | ---------------- | | YAML / JSON | `[project][profile][key]` | root `[key]` | | dotenv | root `[key]` | root `[key]` | | INI | `[profile][key]` | `[DEFAULT][key]` | In SecretSpec 0.19+, `secretspec set` and interactive `secretspec check` print the resolved provider URI, profile, file, and selector before prompting for or writing the value. This makes an accidentally omitted `--profile` visible before the encrypted file changes. ### Format handling [Section titled “Format handling”](#format-handling) SecretSpec asks SOPS to emit JSON when decrypting and selects the secret from that JSON representation. When `?format=` is present, SecretSpec passes the corresponding SOPS input type where SOPS supports one, so filenames such as `.env.production.enc?format=dotenv` work correctly. SOPS does not support `--input-type ini`, so `?format=ini` is accepted only when the filename itself ends in `.ini`. Without `?format=`, the filename must end in `.yaml`, `.yml`, `.json`, `.env`, `.dotenv`, or `.ini`; an unrecognized extension is reported as a configuration error. When a selected YAML or JSON node is not a string, reads return its compact JSON representation. Writes always store the supplied secret as a string value. In a single dotenv file, the same key cannot hold different values for different profiles. Use a templated path such as `secrets/{project}/.env.{profile}.enc?format=dotenv` when profiles need separate dotenv values. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) SOPS supports `ref = { item = "..." }` against a **single-file** provider URI. For YAML, JSON, and dotenv, the item names a root key and does not add the project/profile convention path. For INI, it names a key in the `[DEFAULT]` section: secretspec.toml ```toml [providers] shared_sops = "sops://shared.enc.yaml" [profiles.production] EXTERNAL_TOKEN = { description = "Token already managed in shared.enc.yaml", ref = { item = "existing_token" }, providers = ["shared_sops"] } ``` This YAML example reads or writes root selector `["existing_token"]`; the INI equivalent is `["DEFAULT"]["existing_token"]`. This provider treats the value at the selected key as the complete secret, so it supports only `item` and rejects `field` and other extra ref coordinates. A templated SOPS URI also rejects refs: without convention `project`/`profile` inputs, SecretSpec cannot choose which templated file the external item belongs to. See [Secret References](/concepts/references/) for the general model. ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Pass age settings in the provider URI [Section titled “Pass age settings in the provider URI”](#pass-age-settings-in-the-provider-uri) The setup above uses `.sops.yaml` and `SOPS_AGE_KEY_FILE`. To keep both settings in the provider URI instead, derive the recipient from the same identity: ```bash $ AGE_RECIPIENT="$(age-keygen -y key.txt)" $ secretspec set DATABASE_URL --provider "sops://secrets.enc.json?age_key_file=key.txt&age_recipients=${AGE_RECIPIENT}" ``` ## CI/CD [Section titled “CI/CD”](#cicd) Make the selected SOPS key service available to the job through provider credentials or its standard environment variables, then run SecretSpec with the configured alias: ```bash $ secretspec run --profile production --provider sops_age -- deploy ``` Provider credentials are exposed only to the SOPS child process. Store their backing values in the CI platform’s secret store rather than in the provider URI. ## Security considerations [Section titled “Security considerations”](#security-considerations) * Commit only the SOPS-encrypted files, never decrypted copies or private key material. * Keep secret authentication values in provider credentials or SOPS environment variables instead of URI query parameters. * Review the resolved file and selector shown by `secretspec set` and interactive `secretspec check` in SecretSpec 0.19+ before confirming a write. * Use templated paths when profiles must not share the same dotenv key. # systemd Credential Provider > Read secrets passed to a service through systemd credentials **New in version 0.17** The [systemd](https://systemd.io/) credential provider reads credentials that the service manager passed to the current process. It is a read-only delivery provider: systemd selects and optionally decrypts each credential before SecretSpec starts, and SecretSpec reads the resulting file from `$CREDENTIALS_DIRECTORY`. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | -------------- | ---------------------------------------------------------------------- | | Provider | `systemd-credential` (0.17+) | | URI | `systemd-credential://` | | Access | Read-only | | Best for | Services that receive application or provider credentials from systemd | | Authentication | Filesystem access granted by systemd to the service user | | Storage | Immutable runtime files managed by systemd | ## Quick start [Section titled “Quick start”](#quick-start) Declare a secret that reads from the credential with the same name: secretspec.toml ```toml [profiles.production] DATABASE_PASSWORD = { description = "Production database password", providers = ["systemd-credential"] } ``` Pass that credential to the service: /etc/systemd/system/myapp.service ```ini [Service] LoadCredential=DATABASE_PASSWORD:/etc/myapp/database-password Environment=SECRETSPEC_PROFILE=production ExecStart=/usr/bin/secretspec --file /etc/myapp/secretspec.toml run -- /usr/bin/myapp ``` systemd copies the value into the service’s private credential directory. SecretSpec reads it there and resolves `DATABASE_PASSWORD` normally. For confidential data stored in a unit or credential store, prefer `LoadCredentialEncrypted=` or `SetCredentialEncrypted=`. systemd decrypts the credential before SecretSpec reads it, so the provider behaves the same way for encrypted and plaintext sources. ## Setup [Section titled “Setup”](#setup) The process must be started by systemd with at least one service credential. systemd sets `$CREDENTIALS_DIRECTORY` to an absolute directory containing one immutable file per credential. Running the provider outside that execution context returns an error explaining that the variable is missing. The provider has no build feature and takes no URI configuration: ```toml [providers] service = "systemd-credential://" ``` An authority, path, or query on the URI is rejected. Select a differently named credential with a secret reference instead. ## Use an existing credential name [Section titled “Use an existing credential name”](#use-an-existing-credential-name) Convention addresses use the SecretSpec key as the systemd credential name and do not include the project or profile. If the names differ, set `ref.item`: ```toml [profiles.production] DATABASE_PASSWORD = { description = "Production database password", providers = ["systemd-credential"], ref = { item = "myapp.database-password" } } ``` ```ini [Service] LoadCredential=myapp.database-password:/etc/myapp/database-password ``` Only the `item` coordinate is supported. Credential names must be a single filename; nested paths and traversal components are rejected. ## Supply another provider’s credential [Section titled “Supply another provider’s credential”](#supply-another-providers-credential) Because `systemd-credential` is a regular read-only SecretSpec provider, an alias can use it as a provider-credential source: ```toml [providers] bootstrap = "systemd-credential://" remote = { uri = "onepassword://Production", credentials = { service_account_token = "bootstrap" } } ``` The service unit must pass a credential named `service_account_token`. SecretSpec reads it into memory and hands it directly to the target provider. Service isolation Every process in the same service runs under the same credential access boundary. If `secretspec run` starts the application in that service, the application can also access the service’s credential directory. Put a high-value bootstrap credential in a separate SecretSpec broker or provisioning service when the application itself must not be able to read it. ## Storage and security model [Section titled “Storage and security model”](#storage-and-security-model) This provider does not persist, encrypt, or decrypt values. Those properties come from the systemd unit: * `LoadCredential=` loads a file or socket source. * `LoadCredentialEncrypted=` loads a systemd-encrypted credential. * `SetCredentialEncrypted=` embeds encrypted credential data in the unit. * `$CREDENTIALS_DIRECTORY` contains the plaintext runtime value while the service is active. SecretSpec refuses symlinks, directories, non-UTF-8 values, and credential names that could escape the credential directory. SecretSpec’s provider API is text-based; binary systemd credentials are therefore not supported. Changing a source credential does not modify an already-running service’s immutable runtime credential. Restart the service to load the new value. # Vault Provider > HashiCorp Vault integration The [Vault](https://developer.hashicorp.com/vault) provider integrates with HashiCorp Vault for centralized secret management using the KV (Key-Value) secrets engine. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | --------------- | ---------------------------------------------------------------- | | Provider | `vault` | | URI | `vault://[namespace@]host[:port][/mount][?options]` | | Access | Read, write, and delete (0.17+); secret references are read-only | | Best for | Self-managed, policy-controlled secret infrastructure | | Authentication | Token or AppRole; JWT/OIDC (0.17+) | | Build feature | `vault` | | Default storage | KV path `secretspec/{project}/{profile}/{key}`, field `value` | ## Quick start [Section titled “Quick start”](#quick-start) ```bash # With default "secret" mount $ secretspec set DATABASE_URL --provider vault://vault.example.com:8200 Enter value for DATABASE_URL: postgresql://localhost/mydb ✓ Secret 'DATABASE_URL' saved to vault (profile: default) ``` ## Setup [Section titled “Setup”](#setup) ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * A running Vault server * Authentication credentials * KV secrets engine enabled (v1 or v2) * Build with `--features vault` ### Token authentication [Section titled “Token authentication”](#token-authentication) Token authentication is the default. SecretSpec reads `VAULT_TOKEN` or `~/.vault-token`: ```bash $ export VAULT_TOKEN=hvs.your-token-here ``` ### AppRole authentication [Section titled “AppRole authentication”](#approle-authentication) **Changed in version 0.18** `VAULT_SECRET_ID` or the `secret_id` provider credential may be omitted when the AppRole is configured with `bind_secret_id=false`. SecretSpec then sends only `role_id` and lets Vault apply the role’s remaining login constraints. Select AppRole with `?auth=approle`. Vault roles bind a SecretID by default, so the usual configuration provides both environment variables: ```bash $ export VAULT_ROLE_ID=your-role-id $ export VAULT_SECRET_ID=your-secret-id ``` Starting with SecretSpec 0.15, these credentials can instead be read from another provider so they do not live in a shell profile: secretspec.toml ```toml [providers.vault_approle] uri = "vault://vault.example.com:8200/secret?auth=approle" [providers.vault_approle.credentials] role_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "role_id" } } secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "secret_id" } } ``` SecretSpec 0.14 supports only `VAULT_ROLE_ID` and `VAULT_SECRET_ID`. Disabling SecretID binding removes AppRole’s usual second credential. Keep the server default unless the workload deliberately relies on another trust boundary, such as a tightly controlled Agent host and network constraints. ### Custom authentication mounts [Section titled “Custom authentication mounts”](#custom-authentication-mounts-018) **New in version 0.18** AppRole and JWT methods mounted somewhere other than their defaults can be selected with `?auth_mount=`. The value is relative to `/v1/auth`: ```text vault://vault.example.com:8200/secret?auth=approle&auth_mount=platform-approle vault://vault.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=ci ``` The provider logs in at `/v1/auth/platform-approle/login` and `/v1/auth/ci-jwt/login`, respectively. The KV mount remains the provider URI path (`secret` in these examples). ### JWT / OIDC authentication [Section titled “JWT / OIDC authentication”](#jwt--oidc-authentication-017) **New in version 0.17** Select JWT with `?auth=jwt`. The provider performs the `auth/jwt/login` exchange itself. The JWT comes from `VAULT_JWT` when set. Otherwise, in a GitHub Actions or Forgejo job with `id-token: write`, the provider mints one from the runner’s OIDC identity, so CI stores no static secret. Starting with SecretSpec 0.18, the role may be omitted when the JWT auth mount has a `default_role`; Vault then selects that role during login. An explicit SecretSpec role still takes precedence. Both `role` and `audience` accept a URI query parameter or an environment variable: * `?role=` or `VAULT_JWT_ROLE`; optional with a server-configured `default_role` (0.18+) * `?audience=` or `VAULT_JWT_AUDIENCE`, matched against the role’s `bound_audiences` ## Provider credentials [Section titled “Provider credentials”](#provider-credentials) | Credential | Environment fallback | Available since | | ----------- | -------------------- | --------------- | | `role_id` | `VAULT_ROLE_ID` | 0.15+ | | `secret_id` | `VAULT_SECRET_ID` | 0.15+ | | `token` | `VAULT_TOKEN` | 0.15+ | See the complete [provider credential reference](/reference/provider-credentials/) for all supported providers and environment fallbacks. ## Configuration [Section titled “Configuration”](#configuration) ### URI format [Section titled “URI format”](#uri-format) ```text vault://[namespace@]host[:port][/mount][?key=value&...] ``` * `host[:port]`: Vault server address (falls back to `VAULT_ADDR`) * `mount`: KV engine mount path (default: `secret`) * `namespace@`: Optional Vault namespace (also reads `VAULT_NAMESPACE`) * `?auth=approle`: Use AppRole authentication (default: `token`) * `?auth=jwt` (0.17+): Use JWT/OIDC authentication; a server-configured `default_role` can supply the role when using SecretSpec 0.18+ * `?auth_mount=` (0.18+): Non-default AppRole or JWT mount beneath `/v1/auth` * `?role=` (0.17+): Vault role for JWT auth (or `VAULT_JWT_ROLE`) * `?audience=` (0.17+): OIDC audience (or `VAULT_JWT_AUDIENCE`) * `?kv=1`: Use KV v1 (default: v2) * `?tls=false`: Disable TLS for development servers ### Concurrent resolution [Section titled “Concurrent resolution”](#concurrent-resolution) * One HTTP client is reused per provider instance (connection pool / h2 reuse). * Concurrent unique-address fetches are capped at 8 by default. * Override the cap with `SECRETSPEC_PROVIDER_CONCURRENCY` (integer ≥ 1) when your Vault proxy tolerates more or less parallel load. ### URI examples [Section titled “URI examples”](#uri-examples) ```text vault://vault.example.com:8200/secret vault://team-a@vault.example.com:8200/secret vault://vault.example.com:8200/secret?auth=approle # SecretSpec 0.18+ vault://vault.example.com:8200/secret?auth=approle&auth_mount=platform-approle # SecretSpec 0.17+ vault://vault.example.com:8200/secret?auth=jwt&role=ci # SecretSpec 0.18+, with default_role configured on the JWT auth mount vault://vault.example.com:8200/secret?auth=jwt ``` ### Project configuration [Section titled “Project configuration”](#project-configuration) secretspec.toml ```toml [providers] vault_prod = "vault://vault.example.com:8200/secret" [profiles.production] DATABASE_URL = { description = "Database URL", providers = ["vault_prod"] } ``` ## Storage model [Section titled “Storage model”](#storage-model) Each secret is stored at `secretspec/{project}/{profile}/{key}` under the configured mount, with its value in a field named `value`. For KV v2, `DATABASE_URL` for project `myapp` and profile `production` is read from `GET /v1/secret/data/secretspec/myapp/production/DATABASE_URL`. ## Provider caching [Section titled “Provider caching”](#provider-caching-017) **New in version 0.17** A KV v2 mount can hold a [cached provider route’s](/concepts/providers/caching/) entries. Vault expires them itself: the cache’s `max_age` is written to the path’s `delete_version_after` metadata, so a cached copy of another store’s secret stops existing at that age even if SecretSpec never runs again. secretspec.toml ```toml [providers] slow = "onepassword://Production" shared_cache = "vault://vault.example.com:8200/secret" myprovider = { fallback = ["slow"], cache = { provider = "shared_cache", max_age = "8h" } } ``` This needs write access to the path’s metadata as well as its data. KV v1 has no expiry and is refused as a cache, rather than storing a copy that would never expire. Deleting — [`cache clear`](/reference/cli/#cache-clear-017) and automatic invalidation — removes the KV path’s metadata and every version, so no soft-deleted version keeps the value recoverable. It is confined to entries SecretSpec owns: a secret reference is never deleted, since the path it names is managed outside SecretSpec. ## Use existing secrets [Section titled “Use existing secrets”](#use-existing-secrets) A secret’s [`ref`](/reference/configuration/#secret-references) field names an existing KV entry: `item` is the KV path relative to the mount, and `field` selects the field to read. `field` is required because KV entries are maps. References are **read-only** in this provider. ```toml [profiles.production] DATABASE_URL = { description = "DB", ref = { item = "myapp/config", field = "db_url" }, providers = ["vault://vault.example.com:8200/secret"] } ``` The mount is not a ref coordinate: it comes from the provider URI (`secret` in the example). To read one secret from a different mount, give that secret a provider entry whose URI names the mount. ## CI/CD [Section titled “CI/CD”](#cicd) SecretSpec 0.16 can use AppRole to keep a user token out of the environment by logging in from `VAULT_ROLE_ID` and `VAULT_SECRET_ID`: ```bash $ export VAULT_ROLE_ID="$CI_VAULT_ROLE_ID" $ export VAULT_SECRET_ID="$CI_VAULT_SECRET_ID" $ secretspec export --format gha --provider "vault://vault.example.com:8200/secret?auth=approle" ``` SecretSpec 0.17 adds a tokenless JWT/OIDC path. Under GitHub Actions or Forgejo Actions with `id-token: write`, the provider mints the job’s OIDC token and logs in with a role bound to the workflow’s claims: ```bash $ secretspec export --format gha --provider "vault://vault.example.com:8200/secret?auth=jwt&role=ci" ``` ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### KV version 1 [Section titled “KV version 1”](#kv-version-1) ```bash $ secretspec set DATABASE_URL --provider "vault://vault.example.com:8200/secret?kv=1" ``` ### Vault namespaces [Section titled “Vault namespaces”](#vault-namespaces) ```bash $ secretspec check --provider vault://team-a@vault.example.com:8200/secret $ export VAULT_NAMESPACE=team-a $ secretspec check --provider vault://vault.example.com:8200/secret ``` ### Development mode [Section titled “Development mode”](#development-mode) ```bash $ vault server -dev $ export VAULT_TOKEN=hvs.dev-root-token $ secretspec check --provider "vault://127.0.0.1:8200/secret?tls=false" ``` # Quick Start > Get up and running with SecretSpec in minutes ## Installation [Section titled “Installation”](#installation) Choose your preferred installation method: * Static Binary ```bash $ curl -sSL https://install.secretspec.dev | sh ``` SecretSpec 0.20+ provides static musl CLI binaries for x64 and arm64 Linux, so the installer works directly on Alpine without a glibc compatibility layer. The static installer also installs `secretspec-update`. Run it to install the latest SecretSpec release: ```bash $ secretspec-update ``` If you installed SecretSpec through Nix or another package manager, update it through that package manager instead. * Brew ```powershell brew install secretspec ``` * WinGet ```powershell winget install --exact --id Cachix.SecretSpec ``` * Devenv.sh Add to your `devenv.nix`: ```nix { config, ... }: { # Secrets are automatically populated from secretspec.toml env.DATABASE_URL = config.secretspec.secrets.DATABASE_URL; env.REDIS_URL = config.secretspec.secrets.REDIS_URL; } ``` * Nix ```bash $ nix-env -iA secretspec -f https://github.com/NixOS/nixpkgs/tarball/nixpkgs-unstable ``` ## Getting started [Section titled “Getting started”](#getting-started) Start with one project and the system keyring. You will declare the secrets the application expects, store one value, and run the application with that value in its environment. ### 1. Initialize `secretspec.toml` [Section titled “1. Initialize secretspec.toml”](#1-initialize-secretspectoml) From your project directory, create a manifest: ```bash $ secretspec init ``` ```text ✓ Created secretspec.toml with 0 secrets Next steps: 1. secretspec config global init # Set up user defaults (0.17+) 2. secretspec check # Verify all secrets are set 3. secretspec run -- your-command # Run with secrets ``` This example assumes the project does not have a `.env` file. If one exists, `init` discovers its secret names automatically and reports the number of declarations it created. Review those declarations instead of replacing them in the next step. See [Migration](/migration/) for details. ### 2. Declare your secrets [Section titled “2. Declare your secrets”](#2-declare-your-secrets) Edit `secretspec.toml` so it describes what your application expects: secretspec.toml ```toml [project] name = "my-app" revision = "1.0" [profiles.default] DATABASE_URL = { description = "PostgreSQL connection string", required = true } SENTRY_DSN = { description = "Error reporting endpoint", required = false } ``` `DATABASE_URL` must be available before the application can run. `SENTRY_DSN` is optional, so leaving it unset does not block resolution. The manifest contains declarations, not secret values, and is safe to commit. ### 3. Store and use a secret [Section titled “3. Store and use a secret”](#3-store-and-use-a-secret) Store the required value in your system keyring: ```bash $ secretspec set DATABASE_URL --provider keyring ``` ```text Enter value for DATABASE_URL (profile: default): ******** ✓ Secret 'DATABASE_URL' saved to keyring (profile: default) ``` Start your application with the resolved values in its environment: ```bash $ secretspec run --provider keyring -- npm start ``` ### 4. Configure your personal defaults [Section titled “4. Configure your personal defaults”](#4-configure-your-personal-defaults) The commands above use `--provider keyring` explicitly. SecretSpec 0.17+ can save your preferred backend and default profile as preferences for your user: ```bash $ secretspec config global init # 0.17+ ? Select your preferred provider backend: > keyring: Uses system keychain (Recommended) kdbx: KeePass KDBX databases (0.17+) onepassword: 1Password password manager keeper: Keeper Secrets Manager (0.18+) via official Rust SDK dotenv: Traditional .env files file: Plaintext files, one per secret (0.19+) env: Read-only environment variables null: Use defaults, generation, or run prompts without storage (0.19+) systemd-credential: Read-only systemd service credentials (0.17+) fly: Fly.io application secrets via flyctl, write-only (0.20+) cloudflare: Cloudflare Secrets Store, write-only (0.20+) pass: Unix password manager with GPG encryption gopass: Gopass CLI password manager with GPG encryption (0.15+) protonpass: Proton Pass via official pass-cli passbolt: Passbolt self-hosted password manager (0.19+) via go-passbolt-cli lastpass: LastPass password manager dashlane: Dashlane password manager, read-only (0.18+) gcsm: Google Cloud Secret Manager awssm: AWS Secrets Manager awsps: AWS Systems Manager Parameter Store (0.18+) scaleway: Scaleway Secret Manager (0.17+) vault: HashiCorp Vault secret management openbao: OpenBao secret management (0.17+) bw: Bitwarden Password Manager (0.18+) bws: Bitwarden Secrets Manager akv: Azure Key Vault aac: Azure App Configuration (0.20+) infisical: Infisical secret management (0.16+) ejson: EJSON encrypted files, read-only (0.20+) age: age-encrypted file (0.17+) sops: SOPS encrypted files (0.17+) kubernetes: Kubernetes (0.20+) ? Select your default profile: development > default none ✓ Configuration saved to /home/user/.config/secretspec/config.toml ``` These preferences are stored in `~/.config/secretspec/config.toml`. They are not written to the project, committed to version control, or shared with other users. They become your personal defaults across projects and can still be overridden by project configuration or command-line options. You can now omit the provider from everyday commands: ```bash $ secretspec set DATABASE_URL $ secretspec check $ secretspec run -- npm start ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * Continue with the commands in [Basic Usage](/basic-usage/) * Bring existing values into SecretSpec with the [Migration guide](/migration/) * Learn about [Profiles](/concepts/profiles/) to manage environment-specific configurations * Explore different [Providers](/concepts/providers/) for secret storage * Choose an [SDK](/sdk/overview/) to resolve secrets from your application # CLI Commands Reference > Complete reference for SecretSpec CLI commands The SecretSpec CLI provides commands for managing secrets across different providers and profiles. ## Global Options [Section titled “Global Options”](#global-options) These options are available on every command: | Option | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-f, --file ` | Path to `secretspec.toml` (default: auto-detect). Env: `SECRETSPEC_FILE` | | `--reason ` | Reason for accessing secrets, recorded by providers that support audit logging (e.g. Proton Pass agent sessions). Takes precedence over `PROTON_PASS_AGENT_REASON`. Env: `SECRETSPEC_REASON` | | `--caller ` | Software integration invoking SecretSpec; recorded separately from the user reason (0.20+) | | `--caller-version ` | Version of `--caller`; requires `--caller` (0.20+) | | `--caller-operation ` | Integration operation; requires `--caller` (0.20+) | | `--caller-resource ` | Non-secret resource being accessed; requires `--caller` (0.20+) | ```bash $ secretspec run --reason "Deploying web frontend" -- ./deploy.sh ``` SecretSpec 0.20+ lets a Git integration identify itself without replacing the user-supplied reason: ```bash $ secretspec get GITHUB_TOKEN \ --caller git \ --caller-version 2.51.0 \ --caller-operation credential_get \ --caller-resource github.com \ --reason "push the release tag" ``` Caller context is caller-asserted audit metadata, not an authenticated identity, and never satisfies `require_reason`. Do not put credentials or secret values in these fields. ## Commands [Section titled “Commands”](#commands) ### init [Section titled “init”](#init) Initialize a new `secretspec.toml` from declarations discovered in a provider. Dotenv files are supported in every current release. SecretSpec 0.18+ accepts any provider that implements reflection, including age files, AWS Parameter Store, and Bitwarden Password Manager vaults. ```bash $ secretspec init [--from ] [--project ] [--profile ] ``` **Options:** * `--from ` - Provider URI to discover (default: `dotenv://.env`); use a `dotenv://` URI for dotenv files * `--project ` - Project used to render the provider namespace (SecretSpec 0.18+; default: current directory name) * `-P, --profile ` - Profile used to render the provider namespace and written to the manifest (SecretSpec 0.18+; default: `default`) Reflection creates declarations only: values are never written to the manifest. Configure the discovered provider as the profile’s source to keep using it, or run `secretspec import` afterward to copy the declared values to a different destination. **Examples:** ```bash $ secretspec init --from dotenv://.env.example ✓ Created secretspec.toml with 5 secrets # SecretSpec 0.18+: discover one rendered Parameter Store hierarchy $ secretspec init \ --from 'awsps://us-east-1?template=/{profile}/{project}/{key}' \ --project payments \ --profile production ✓ Created secretspec.toml with 12 secrets # SecretSpec 0.18+: discover items in one Bitwarden collection $ secretspec init --from 'bw://dev-secrets?type=login' ✓ Created secretspec.toml with 8 secrets ``` For Bitwarden in SecretSpec 0.20+, items under the selected `secretspec/{project}/{profile}/` title prefix become convention declarations; bare existing items are emitted with explicit `ref.item` coordinates. ### config global init [Section titled “config global init”](#config-global-init) Initialize user-global configuration. The explicit `global` namespace is available in SecretSpec 0.17+; without options, the command prompts for the provider and profile. ```bash $ secretspec config global init [--provider ] [--profile ] # 0.17+ ``` SecretSpec 0.17+ accepts `--provider` and `--profile` so installations can save both defaults without interaction. Each omitted option still prompts; use `--profile none` to clear the saved default profile. The corresponding `SECRETSPEC_PROVIDER` and `SECRETSPEC_PROFILE` environment variables are also accepted. Project requirements remain in `secretspec.toml`; the namespace makes it clear that this command writes user-wide defaults. The legacy `secretspec config init` spelling remains supported as a hidden alias. **Example:** ```bash $ secretspec config global init # 0.17+ ? Select your preferred provider backend: > keyring: System keychain ? Select your default profile: > development ✓ Configuration saved to ~/.config/secretspec/config.toml ``` ```bash # SecretSpec 0.17+: save both defaults without prompting $ secretspec config global init --provider env --profile default ✓ Configuration saved to ~/.config/secretspec/config.toml ``` ### config global show [Section titled “config global show”](#config-global-show) Display current user-global configuration. The explicit namespace is available in SecretSpec 0.17+; `secretspec config show` remains a hidden alias. ```bash $ secretspec config global show # 0.17+ ``` **Example:** ```bash $ secretspec config global show # 0.17+ Provider: keyring Profile: development ``` ### config global provider add [Section titled “config global provider add”](#config-global-provider-add) **Changed in version 0.17** The explicit `global` namespace replaces the earlier `config provider add` spelling, which remains a hidden alias. Adding aliases was introduced in 0.14, and `--credential` followed in 0.15. Add a provider alias to your user-level configuration (`~/.config/secretspec/config.toml`). To share aliases with your team, declare them in a top-level `[providers]` table in `secretspec.toml` instead — they take precedence over user-level aliases on name conflict. ```bash $ secretspec config global provider add [--credential NAME=PROVIDER]... # 0.17+ ``` **Arguments:** * `` - Short name for the provider (e.g., `prod_vault`, `shared`) * `` - Provider URI (e.g., `onepassword://Production`, `env://`) **Options:** * `--credential ` - Declare a [provider credential](/reference/provider-credentials/) and its source. `NAME` is semantic and provider-specific, such as `access_token` or `role_id`. Repeatable. Only the bare-string source form is expressible on the command line; add a `ref` by editing the config. **Example:** ```bash $ secretspec config global provider add prod_vault "onepassword://Production" # 0.17+ ✓ Provider alias 'prod_vault' added: 'onepassword://Production' $ secretspec config global provider add bws "bws://project-uuid" --credential access_token=keyring # 0.17+ ✓ Provider alias 'bws' added: 'bws://project-uuid' credentials: access_token=keyring run 'secretspec config provider login bws' to store the credentials ``` ### config global provider list [Section titled “config global provider list”](#config-global-provider-list) List all configured user-level provider aliases. Project-level aliases declared in `secretspec.toml` are not shown by this command. ```bash $ secretspec config global provider list # 0.17+ ``` **Example:** ```bash $ secretspec config global provider list # 0.17+ prod_vault → onepassword://Production shared → onepassword://Shared env → env:// ``` ### config global provider remove [Section titled “config global provider remove”](#config-global-provider-remove) Remove a provider alias from your user-level configuration. To remove a project-level alias, edit the `[providers]` table in `secretspec.toml` directly. ```bash $ secretspec config global provider remove # 0.17+ ``` **Arguments:** * `` - Name of the alias to remove **Example:** ```bash $ secretspec config global provider remove prod_vault # 0.17+ ✓ Provider alias 'prod_vault' removed ``` ### config provider login [Section titled “config provider login”](#config-provider-login) **Changed in version 0.15** In SecretSpec 0.14, supply provider credentials through the provider’s existing environment variables. Store the [credentials](/reference/provider-credentials/) a provider alias declares. Prompts (hidden input) for each credential and writes it to its source provider at the exact location resolution reads it back from. Runs in a project, like `set` and `check`. ```bash $ secretspec config provider login ``` **Arguments:** * `` - Name of the alias whose credentials to store **Example:** ```bash $ secretspec config provider login bws Enter access_token for provider 'bws' (source: keyring): **** ✓ stored access_token in keyring at myproject/default/access_token Run 'secretspec check --provider bws' to verify authentication. ``` A read-only source provider is rejected. An alias that declares no credentials reports that there is nothing to store. ### docker configure [Section titled “docker configure”](#docker-configure-020) **New in version 0.20** Configure Docker to retrieve credentials for one registry through SecretSpec. ```bash $ secretspec docker configure --registry --username [OPTIONS] ``` **Options:** * `--registry ` - Registry hostname, optionally including a port; Docker Hub aliases are normalized to Docker’s canonical registry key * `--username ` - Non-secret registry username; required for the embedded store, or as an alternative to `--username-secret` with `--file` * `--token-secret ` - Custom manifest key containing the password or access token; requires `--file` * `--username-secret ` - Custom manifest key containing the username; requires `--file` and conflicts with `--username` * `-P, --profile ` - Custom manifest profile; requires `--file` * `-p, --provider ` - Provider override the helper should use * `-y, --yes` - Confirm the Docker configuration change non-interactively Without `--file`, the command configures the embedded registry-isolated store and prints the corresponding `secretspec docker login` command. With `--file`, `--token-secret` and either username option are required. The command adds a registry-specific `credHelpers` entry to Docker’s `config.json`, prompts with a default of **No**, and refuses to replace an existing helper. ### docker login [Section titled “docker login”](#docker-login-020) **New in version 0.20** Store a password or token in the embedded Docker credential store: ```bash $ secretspec docker login [--provider ] ``` The registry is normalized exactly as it is for `configure`. Each registry and physical Docker configuration pair uses a separate SecretSpec project identity. This command rejects `--file`; use `secretspec set` for custom-manifest credentials. ### docker logout [Section titled “docker logout”](#docker-logout-020) **New in version 0.20** Remove a password or token from the embedded Docker credential store: ```bash $ secretspec docker logout [--provider ] ``` Use the same provider override supplied to `login`. This does not remove the Docker helper registration; use `unconfigure` for that. ### docker unconfigure [Section titled “docker unconfigure”](#docker-unconfigure-020) **New in version 0.20** Remove one or all Docker credentials configured by SecretSpec in the active Docker configuration. ```bash $ secretspec docker unconfigure --registry $ secretspec docker unconfigure --all ``` Use `--yes` to confirm the change non-interactively. `--all` removes only entries SecretSpec owns; it preserves the default credential store, other registry helpers, stored authentication entries, and unrelated Docker options. See [Docker credentials](/integrations/docker/) for complete setup, custom manifest, and ownership details. ### git configure [Section titled “git configure”](#git-configure-020) **New in version 0.20** Configure Git to retrieve an HTTP(S) or SMTP password or token through SecretSpec. Repository-local configuration is the default. ```bash $ secretspec git configure --url [OPTIONS] ``` **Options:** * `--url ` - HTTP(S) or SMTP URL this credential may authenticate; an HTTP(S) path limits it to that part of the host, while SMTP requires an explicit port * `--username ` - Non-secret username to keep in the managed Git configuration; required for SMTP and must match `sendemail.smtpUser` * `-p, --provider ` - Provider override the helper should use * `--global` - Configure the current user’s global Git settings instead * `-y, --yes` - Confirm a global change non-interactively; requires `--global` Without `--file`, the command uses the embedded Git manifest with required `PASSWORD` and optional `USERNAME` declarations. It records no manifest path and isolates storage by the canonical protocol, host, and configured path. With `--file`, `--token-secret ` is required; `--username-secret ` and `-P, --profile ` select custom manifest declarations and conflict with the embedded defaults. `--username-secret` conflicts with `--username`. Global changes prompt with a default of **No**. Existing helpers and unrelated Git configuration are not replaced. See [Git credentials](/integrations/git/) for setup examples and the ownership model. ### git login [Section titled “git login”](#git-login-020) **New in version 0.20** Store an embedded Git password or token, prompting securely on a terminal or reading it from piped standard input. ```bash $ secretspec git login [--username ] [--provider ] ``` `--username` also stores the optional embedded username. The URL must match the one passed to `configure`, including a path scope. For SMTP, the username is read from managed Git configuration unless passed explicitly. `git login` rejects `--file`; use `secretspec set` for custom manifest declarations. ### git logout [Section titled “git logout”](#git-logout-020) **New in version 0.20** Remove the embedded username and password or token for one exact target without removing its Git helper configuration. ```bash $ secretspec git logout [--username ] [--provider ] ``` For SMTP, the username is read from managed Git configuration unless passed explicitly. `git logout` rejects `--file`; use `secretspec delete` for custom manifest declarations. ### git unconfigure [Section titled “git unconfigure”](#git-unconfigure-020) **New in version 0.20** Remove one or all Git credentials configured by SecretSpec in the selected scope. ```bash $ secretspec git unconfigure --url $ secretspec git unconfigure --all $ secretspec git unconfigure --all --global ``` Use `--global` to select global configuration and `--yes` to confirm that global change non-interactively. `--all` removes only entries SecretSpec owns; it does not remove existing helpers, usernames, or unrelated includes. ### claude configure [Section titled “claude configure”](#claude-configure-021) **New in version 0.21** Configure Claude Code’s `apiKeyHelper` to retrieve an API or gateway credential through SecretSpec. Personal project settings in the Git repository’s main checkout `.claude/settings.local.json` are the default; outside Git, the command uses the current directory. ```bash $ secretspec claude configure [OPTIONS] ``` **Options:** * `--token-secret ` - Custom manifest key containing the credential; requires `--file` * `-P, --profile ` - Custom manifest profile; requires `--file` * `-p, --provider ` - Provider override the helper should use * `--resource ` - Non-secret API host recorded in audit caller context; defaults to `api.anthropic.com` * `--global` - Configure `$CLAUDE_CONFIG_DIR/settings.json`, or the current user’s `~/.claude/settings.json` when the variable is unset * `-y, --yes` - Confirm a user-level change non-interactively; requires `--global` Without `--file`, the command creates an embedded credential identity isolated by settings scope and audit resource, then prints the corresponding `secretspec claude login` command. Changing the resource selects a new embedded credential without deleting the previous one, so log out before reconfiguring when the old credential should be removed. With `--file`, `--token-secret` is required. The command preserves unrelated Claude settings and refuses to replace an `apiKeyHelper` it does not manage. User-level changes prompt with a default of **No**. See [Claude Code](/integrations/claude-code/) for API, gateway, custom-manifest, and authentication-precedence details. ### claude login [Section titled “claude login”](#claude-login-021) **New in version 0.21** Store an API or gateway credential in the embedded Claude Code credential store, prompting securely on a terminal or reading it from piped standard input. ```bash $ secretspec claude login [--global] [--provider ] ``` The command selects the current project’s managed configuration, or the user configuration with `--global`, and automatically uses its provider and audit resource. An explicit provider overrides the recorded provider for this operation. `claude login` rejects `--file`; use `secretspec set` for a custom manifest. ### claude logout [Section titled “claude logout”](#claude-logout-021) **New in version 0.21** Remove the embedded Claude Code credential without removing `apiKeyHelper`: ```bash $ secretspec claude logout [--global] [--provider ] ``` The command uses the same scope, provider, and audit resource selection as `login`, and remains available after `unconfigure`. `claude logout` rejects `--file`; use `secretspec delete` for a custom manifest. ### claude unconfigure [Section titled “claude unconfigure”](#claude-unconfigure-021) **New in version 0.21** Remove the SecretSpec-managed `apiKeyHelper` from the selected Claude Code settings file. ```bash $ secretspec claude unconfigure $ secretspec claude unconfigure --global ``` Use `--yes` to confirm a user-level change non-interactively. The command preserves the stored credential and unrelated Claude settings. It refuses to remove an `apiKeyHelper` that changed outside SecretSpec. ### check [Section titled “check”](#check) Check if all required secrets are available, with interactive prompting for missing secrets. ```bash $ secretspec check [OPTIONS] ``` **Options:** * `-p, --provider ` - Provider backend to use * `-P, --profile ` - Profile to use * `-S, --scope ` - Resolve only a `[scopes]` subset of the profile (SecretSpec 0.17+) * `-n, --no-prompt` - Don’t prompt for missing secrets (exit with error if any are missing) * `--json` - Print a value-free resolution report as JSON instead of prompting * `--explain` - Print a value-free, human-readable resolution trace instead of prompting **Example:** ```bash $ secretspec check --profile production ✓ DATABASE_URL - Database connection string ✗ API_KEY - API key for external service (required) # SecretSpec 0.19+: the exact write destination is shown before prompting. Writing secret 'API_KEY' to keyring (profile: production) target: item=secretspec/my-app/production/API_KEY [1/1] Enter value for API_KEY: **** ✓ Secret 'API_KEY' saved to keyring (profile: production) ``` #### Resolution report (`--json` / `--explain`) [Section titled “Resolution report (--json / --explain)”](#resolution-report---json----explain) `--json` and `--explain` report how every declared secret resolved for the active profile without prompting and without ever printing a secret value. Both exit non-zero when a required secret is missing, so they work as a CI gate. `--explain` prints a human-readable trace: ```bash $ secretspec check --profile development --explain profile: development provider: keyring:// DATABASE_URL ok source keyring:// DEV_SESSION_SECRET ok default value JWT_SECRET ok will generate SENTRY_DSN missing optional STRIPE_KEY MISSING required ``` Both surfaces resolve without minting anything, so a `generate` secret that no provider holds yet reads as `will generate` rather than as an existing value. Since SecretSpec 0.20, a **required** `generate` secret is reported as `MISSING required` while no provider holds it, and both surfaces exit non-zero. The value does not exist until a pass writes it, so a preflight that called it resolved would pass while the store is still empty. Run `secretspec check` (or `secretspec run`) once to mint and store it; afterwards the preflight reports it as resolved from its provider. `will generate` is reserved for the cases where nothing has to be provisioned: an optional `generate` secret, or a provider such as [`null`](/providers/null/) that never retains a generated value and therefore mints a fresh one every resolution. `--json` emits a versioned, machine-readable object for tooling and CI. Each entry reports the `status` (`resolved`, `missing_required`, `missing_optional`), whether the value came from a provider (`source_provider`, credential-free), a generator (`generated`), or a committed default (`default_applied`), and whether it is exposed `as_path`. No secret values appear. The canonical JSON Schema is committed at `schema/resolution-report.schema.json`. ```bash $ secretspec check --profile production --json { "schema_version": 1, "provider": "keyring://", "profile": "production", "secrets": [ { "name": "DATABASE_URL", "status": "resolved", "required": true, "source_provider": "keyring://", "default_applied": false, "generated": false, "as_path": false }, { "name": "STRIPE_KEY", "status": "missing_required", "required": true, "default_applied": false, "generated": false, "as_path": false } ] } ``` ### get [Section titled “get”](#get) Get a secret value. ```bash $ secretspec get [OPTIONS] ``` **Options:** * `-p, --provider ` - Provider backend to use * `-P, --profile ` - Profile to use **Example:** ```bash $ secretspec get DATABASE_URL --profile production postgresql://prod.example.com/mydb ``` For a composed secret, `get` resolves its transitive dependencies and prints the derived value. Available since SecretSpec 0.16. ### schema [Section titled “schema”](#schema) Emit a single-root JSON Schema for the manifest’s typed shape: by default the union `SecretSpec` (safe for any profile); with `--profile`, that profile’s exact fields. Value-free: reads only the manifest, never a provider. ```bash $ secretspec schema [OPTIONS] ``` **Options:** * `-P, --profile ` - Emit the schema for this profile’s fields instead of the union * `-o, --output ` - Write to this file instead of stdout Rather than ship a typed-accessor generator per language, feed this schema to [quicktype](https://quicktype.io), which generates an idiomatic type **and** deserializer for any language. Name the type with `--top-level`. At runtime, hand the generated deserializer the flat `{SECRET_NAME: value}` map from the SDK’s `fields()` helper: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang python -o secrets_gen.py ``` ```python from secretspec import SecretSpec from secrets_gen import SecretSpec as Secrets # quicktype-generated, typed resolved = SecretSpec.builder().with_reason("boot").load() s = Secrets.from_dict(resolved.fields()) print(s.database_url) # typed str ``` The same pattern works in every SDK: Go `UnmarshalSecretSpec(resolved.FieldsJSON())`, TypeScript `Convert.toSecretSpec(resolved.fieldsJson())`, Ruby `SecretSpec.from_dynamic!(resolved.fields)`. ### add [Section titled “add”](#add-018) **New in version 0.18** Add a secret declaration to an existing `secretspec.toml`. This edits only the selected profile and preserves the manifest’s comments, formatting, and unrelated tables. The new declaration follows the profile’s defaults; without a `required` profile default, it is required like any other declaration. ```bash $ secretspec add [--description ] [--profile ] # 0.18+ ``` **Arguments and options:** * `` - Secret name. It must be a valid identifier: letters, numbers, and underscores, without a leading number. * `-d, --description ` - Human-readable description. When omitted, SecretSpec prompts for it. * `-P, --profile ` - Profile to edit. When omitted, SecretSpec uses the normal active-profile resolution, including `SECRETSPEC_PROFILE` and the user-global default. ```bash $ secretspec add API_KEY --description "API access token" # 0.18+ ✓ Added secret 'API_KEY' to profile 'development' in secretspec.toml Set its value with: secretspec set API_KEY --profile development ``` `add` changes only the declaration; it never asks for or stores the secret value. Use `secretspec set` afterward to store the value. It rejects names that are already available in the selected profile, including declarations inherited from `default` or an extended manifest. ### set [Section titled “set”](#set) Set a secret value. ```bash $ secretspec set [OPTIONS] [VALUE] ``` **Options:** * `-p, --provider ` - Provider backend to use * `-P, --profile ` - Profile to use **Example:** ```bash $ secretspec set API_KEY sk-1234567890 --profile production --provider sops://secrets.enc.yaml # SecretSpec 0.19+: Writing secret 'API_KEY' to sops://secrets.enc.yaml?format=yaml (profile: production) target: /work/my-app/secrets.enc.yaml ["my-app"]["production"]["API_KEY"] ✓ Secret 'API_KEY' saved to sops (profile: production) ``` In SecretSpec 0.19+, `set` shows the resolved provider, profile, and native write target before reading a piped value or opening the password prompt. For SOPS this includes the exact encrypted file and `sops set` selector, making a missing `--profile` visible before the write. `set` rejects composed secrets because their values are derived and read-only. Available since SecretSpec 0.16. ### delete [Section titled “delete”](#delete-018) **New in version 0.18** Delete stored provider values without changing their declarations in `secretspec.toml`. ```bash $ secretspec delete ... [--provider ] [--profile ] $ secretspec delete --all [--yes] [--provider ] [--profile ] ``` **Arguments and options:** * `...` - One or more declared secrets to delete. * `--all` - Delete every provider-backed secret declared in the active profile. It cannot be combined with a name. * `-y, --yes` - Skip the interactive confirmation for `--all`. Non-interactive use of `--all` requires this option. * `-p, --provider ` - Delete from this provider instead of the manifest’s primary write provider. * `-P, --profile ` - Profile whose values are addressed. ```bash # Delete one value from its primary write provider $ secretspec delete API_KEY Deleted 'API_KEY' Deleted 1 secret value; 0 already absent # Delete selected values from an old dotenv provider $ secretspec delete API_KEY DATABASE_URL --provider dotenv://.env.old # Explicitly delete every stored value in production $ secretspec delete --all --profile production --yes ``` Deletion is idempotent: an already-absent value is reported as such and does not fail the command. Without `--provider`, routing mirrors `set`: only the primary write provider is changed, never every provider in a fallback chain. Any cache entry declared for the secret is invalidated so it cannot continue to serve the deleted value. The providers that support deletion in 0.18 are keyring, dotenv, pass, gopass, Vault, OpenBao, and Keeper Secrets Manager; age supports it starting with 0.20. Other providers return an explicit unsupported-operation error. Vault, OpenBao, and Keeper refuse to delete native `ref` entries because their backends would have to destroy a whole externally managed path or record rather than only the referenced field. ### run [Section titled “run”](#run) Run a command with secrets injected as environment variables. ```bash $ secretspec run [OPTIONS] -- ``` **Options:** * `-p, --provider ` - Provider backend to use * `-P, --profile ` - Profile to use * `-S, --scope ` - Inject only a `[scopes]` subset of the profile (SecretSpec 0.17+) **Examples:** ```bash # Run npm with secrets available as environment variables $ secretspec run --profile production -- npm run deploy # Verify secrets are injected $ secretspec run -- env | grep DATABASE_URL DATABASE_URL=postgresql://localhost/mydb # Inject only the `api` scope's secrets (SecretSpec 0.17+); secrets the # scope excludes are removed from the child even if the parent exported them $ secretspec run --scope api -- ./api-server ``` SecretSpec 0.19+ can securely request a declared missing value before the child starts. The selected provider normally saves the answer; choose `null` when it must be ephemeral: secretspec.toml ```toml [profiles.default] DEPLOY_PASSWORD = { description = "One-time deployment password", required = true, prompt = true, providers = ["null"] } ``` ```bash $ secretspec run -- ./deploy ? Enter value for DEPLOY_PASSWORD (profile: default): ``` The hidden prompt reads from the controlling terminal, leaving the child’s stdin unchanged even when it is piped or redirected. The answer is injected only for that invocation when the provider is `null`; writable providers save it and make the prompt a first-use provisioning step. If no controlling terminal exists, `run` fails before starting the child. Only declarations with `prompt = true` opt into this behavior; ordinary missing secrets still fail without a prompt. On Unix, SecretSpec 0.20+ forwards `SIGTERM`, `SIGINT`, and `SIGHUP` to the started command. This lets applications run their graceful-shutdown handlers when `secretspec run` is a container entrypoint, including when SecretSpec is PID 1. If the command is terminated by a signal, `run` exits with the conventional `128 + signal` status (for example, 143 for `SIGTERM`). The `--provider` override applies to every secret, including those with a [`ref`](/reference/configuration/#secret-references) field: refs are redirected to the overriding provider just like convention secrets. This makes it easy to point refs at fixtures during tests without editing the manifest: ```bash # Resolve every secret, refs included, from a fixtures file $ secretspec run --provider dotenv:.env.fixtures -- cargo test ``` Shell Variable Expansion Variables like `$DATABASE_URL` in the command line are expanded by your **shell before** secretspec runs. To use injected secrets in the command itself, wrap it in a subshell: ```bash # This won't work - $DATABASE_URL is expanded before secretspec runs $ secretspec run -- echo $DATABASE_URL # Output: (empty, because DATABASE_URL isn't set in current shell) # This works - variable expansion happens in the subprocess $ secretspec run -- sh -c 'echo $DATABASE_URL' # Output: postgresql://localhost/mydb ``` For most use cases, simply run your application and it will read secrets from its environment: ```bash $ secretspec run -- node app.js # app.js reads process.env.DATABASE_URL ``` ### export [Section titled “export”](#export) Resolve every secret for the active profile and write it to stdout in a chosen format, without running a command. Unlike `run`, it never prompts and exits non-zero when a required secret is missing, so CI can gate on it. ```bash $ secretspec export [OPTIONS] ``` Options are `-p, --provider `, `-P, --profile `, `-S, --scope ` (a `[scopes]` subset of the profile, SecretSpec 0.17+), and `--format ` (default `shell`). Unlike [`run --scope`](#run), `export --scope` only emits the scoped subset; it unsets nothing, because no output format can express an unset. A shell that already holds a wider set keeps those values after a scoped `export`, so use `run --scope` when the point is to narrow an existing environment. | Format | Output | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `shell` | `export KEY='value'` lines, ready for `eval "$(secretspec export)"` | | `dotenv` | `KEY=value` lines in dotenv syntax. In 0.20+, values are unquoted when they already round-trip and otherwise double-quoted and escaped; `$` remains literal. | | `json` | a single compact JSON object mapping each secret name to its value | | `gha` | appends `KEY=value` to the file named by `$GITHUB_ENV` and prints an `::add-mask::` command per value to stdout, so later workflow steps and third-party actions see the secrets | ```bash # Load secrets into the current shell $ eval "$(secretspec export --profile production)" # Emit JSON for another tool to consume $ secretspec export --profile production --format json {"DATABASE_URL":"postgresql://prod.example.com/mydb"} ``` The `gha` format targets a `secretspec export --format gha` step in a GitHub or Forgejo Actions job: it masks the values in the runner log and persists them to the job environment for the steps that follow. ### import [Section titled “import”](#import) Import secrets from one provider to another. ```bash $ secretspec import [--delete-source] ``` The destination provider and profile are determined from your configuration. Secrets that already exist in the destination provider will not be overwritten. In SecretSpec 0.19+, the source and destination resolve their addresses independently. A source alias can use its own [provider `ref` template or per-secret scoped ref](/concepts/references/#different-coordinates-per-provider-019), while the destination uses its selected alias’s mapping. Also in SecretSpec 0.19+, a literal source remains convention-addressed, but `import` warns when it shares a storage container with a defined alias whose template or active scoped refs resolve any imported secret to a different entry. The warning is informational: keep the literal to migrate convention-named entries, or select the alias when its alias-specific coordinates describe the intended source. Import output retains the selected alias name alongside its resolved, credential-free provider URI. **Arguments:** * `` - Provider to import from (e.g., `env`, `dotenv:/path/to/.env`) * `--delete-source` - After copying, delete a source value only when the destination is verified to contain the same value. Available in SecretSpec 0.18+. **Example:** ```bash # Import from environment variables to your default provider $ secretspec import env Importing secrets from env to keyring (profile: development)... ✓ DATABASE_URL - Database connection string ○ API_KEY - API key for external service (already exists in target) ✗ REDIS_URL - Redis connection URL (not found in source) Summary: 1 imported, 1 already exists, 1 not found in source # Import from a specific .env file $ secretspec import dotenv:/home/user/old-project/.env # Move values out of an old provider (SecretSpec 0.18+) $ secretspec import dotenv:/home/user/old-project/.env --delete-source ``` **Use Cases:** * Migrate from .env files to a secure provider like keyring or 1Password * Copy secrets between different profiles or projects * Import existing environment variables into SecretSpec management `import` skips composed secrets because they have no stored value to copy; their component secrets are imported normally. Available since SecretSpec 0.16. With `--delete-source`, source and destination must resolve to different physical entries. In SecretSpec 0.19+, distinct scoped refs in the same store are allowed. SecretSpec preflights every source and destination, performs all writes, reads back and validates every copied value, and only then begins source cleanup. If a destination already contains an identical value, the source is also safe to delete; if it differs, SecretSpec retains the source and reports the conflict. A source provider that does not support deletion fails explicitly instead of pretending the migration completed. Source deletion was introduced in SecretSpec 0.18; independent endpoint refs and operation-wide preflight are available in 0.19+. ### cache clear [Section titled “cache clear”](#cache-clear-017) **New in version 0.17** Delete cached provider values for one secret, or for every cached secret in the active profile. Authoritative fallback providers are not modified. ```bash $ secretspec cache clear [NAME] [--profile ] ``` **Arguments and options:** * `[NAME]` - Cached secret to clear. Omit it to clear all cached secrets in the profile. * `-P, --profile ` - Profile whose logical cache entries are cleared. The reported count is the number of entries that were actually removed, so a profile with nothing cached reports `Cleared 0 cache entries`. `--provider` and `SECRETSPEC_PROVIDER` are ignored: clearing always addresses the cache of the route the manifest declares. When one cache store cannot be cleared, the remaining secrets are still cleared and the command then reports what failed. ```bash # Force the next API_KEY read through its authoritative fallback route $ secretspec cache clear API_KEY Cleared 1 cache entry # Clear every cached secret in production $ secretspec cache clear --profile production Cleared 4 cache entries ``` See [Provider caching](/concepts/providers/caching/) for configuration and resolution behavior. ### audit [Section titled “audit”](#audit) Show the local [audit log](/concepts/audit/) of secret access. ```bash $ secretspec audit [--project ] [--action ] [-n ] [--json] ``` **Options:** * `--project ` - Only show entries for this project * `--action ` - Only show entries for this action (`get`, `set`, `check`, `run`, `import`, `export`, `cache_clear` and `cache_refresh` in 0.17+, or `delete` in 0.18+) * `-n, --tail ` - Show only the last N entries * `--json` - Output raw JSON Lines instead of the formatted summary The log location is read from your user-global config (`[audit]` in `~/.config/secretspec/config.toml`), defaulting to the per-user state directory. **Example:** ```bash $ secretspec audit --action get -n 5 2026-06-04T18:06:29Z get found GITHUB_TOKEN (my-app/production) reason: push release tag caller: git@2.51.0/credential_get github.com # Pipe raw entries to jq $ secretspec audit --json | jq 'select(.outcome == "missing")' ``` ### completions [Section titled “completions”](#completions-020) **New in version 0.20** Generate a completion script that asks the same command definition used by `secretspec --help` for suggestions. Completion results include every command, option, possible value, and description supported by the target shell. They also provide contextual suggestions for profile, scope, secret, provider, and provider-alias names. File arguments complete paths, while `secretspec run` completes executables and command-argument paths. When you press Tab, the completion script invokes `secretspec` to calculate the current suggestions. SecretSpec reads the nearest `secretspec.toml` (or the manifest selected by `--file` or `SECRETSPEC_FILE`) and user configuration to discover names and descriptions. It does not contact providers or read secret values. ```bash $ secretspec completions ``` Supported shells are `bash`, `elvish`, `fish`, `nushell`, `powershell`, and `zsh`. Load completions for the current session with the command for your shell: * Bash: `source <(secretspec completions bash)` * Elvish: `eval (secretspec completions elvish | slurp)` * Fish: `secretspec completions fish | source` * PowerShell: `secretspec completions powershell | Out-String | Invoke-Expression` * Zsh: `autoload -U compinit && compinit && source <(secretspec completions zsh)` For persistent Bash, Elvish, Fish, PowerShell, or Zsh completions, put the corresponding command in your shell’s startup file. Generating the script at startup keeps it synchronized after a SecretSpec upgrade. Nushell loads completion modules from a file: ```nu secretspec completions nushell | save -f ~/.config/nushell/completions-secretspec.nu use ~/.config/nushell/completions-secretspec.nu * ``` Regenerate that file after upgrading SecretSpec. ## Environment Variables [Section titled “Environment Variables”](#environment-variables) | Variable | Description | | --------------------- | ------------------------------------------------- | | `SECRETSPEC_PROFILE` | Default profile to use | | `SECRETSPEC_PROVIDER` | Default provider to use | | `SECRETSPEC_FILE` | Path to `secretspec.toml` (same as `--file`) | | `SECRETSPEC_REASON` | Reason for accessing secrets (same as `--reason`) | ## Quick Start Workflow [Section titled “Quick Start Workflow”](#quick-start-workflow) ```bash # Initialize from existing .env $ secretspec init --from .env # Set up user-global defaults (0.17+) $ secretspec config global init # Import existing secrets (optional) $ secretspec import env # or: secretspec import dotenv:.env.old # Check and set missing secrets $ secretspec check # Run your application $ secretspec run -- npm start ``` # secretspec.toml Reference > Complete reference for secretspec.toml configuration options ## secretspec.toml Reference [Section titled “secretspec.toml Reference”](#secretspectoml-reference) The `secretspec.toml` file defines project-specific secret requirements. This file should be checked into version control. ### \[project] Section [Section titled “\[project\] Section”](#project-section) ```toml [project] name = "my-app" # Project name (required) revision = "1.0" # Format version (required, must be "1.0") extends = ["../shared"] # Paths to parent configs for inheritance (optional) require_reason = "agents" # When to require a reason for secret access (optional) ``` | Field | Type | Required | Description | | ---------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `name` | string | Yes | Project identifier | | `revision` | string | Yes | Format version (must be “1.0”) | | `extends` | array\[string] | No | Paths to parent configuration files | | `require_reason` | `"agents"` \| boolean | No | When secret access must supply a reason (via `--reason`, `SECRETSPEC_REASON`, or the SDK’s `with_reason()`). Defaults to `"agents"`. | The `1.0` revision is backward compatible: newer SecretSpec versions continue to support existing `revision = "1.0"` configurations, although they may add features to the revision before SecretSpec 1.0 is released. With the SecretSpec 1.0 release, revision `1.0` will be finalized. Later configuration format changes may be introduced under new revision numbers. #### Requiring a reason for secret access [Section titled “Requiring a reason for secret access”](#requiring-a-reason-for-secret-access) `require_reason` controls when secretspec demands a reason for accessing secrets. It accepts three values: | Value | Behavior | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `"agents"` (default) | Require a reason only when SecretSpec heuristically classifies the current process as an AI agent. Sessions not classified as agents are unaffected. | | `true` | Require a reason from every caller using SecretSpec (humans, CI, and agents). | | `false` | Never require a reason. | The policy is enforced at SecretSpec’s secret-access entry points and travels with the checked-in `secretspec.toml`. With `true`, every caller using the manifest must supply a reason before SecretSpec proceeds: ```bash # In a session SecretSpec detects as an agent, with the default "agents" policy: $ secretspec run -- ./deploy.sh Error: Accessing secrets requires a reason. Provide one with --reason "" ... $ secretspec run --reason "Deploy web frontend" -- ./deploy.sh # ok ``` Agent detection is heuristic The default `"agents"` policy depends on detection. It can miss an unknown or changed agent and can classify a session incorrectly. Use `require_reason = true` when every SecretSpec caller must supply a reason. **Agent detection.** secretspec delegates heuristic detection of known agents to the [`detect-coding-agent`](https://crates.io/crates/detect-coding-agent) crate, which maintains the per-tool signal list (Claude Code, Cursor, Codex, Gemini CLI, Copilot, and more). It treats **autonomous and hybrid** environments as agents but not human-driven interactive editors. In addition, secretspec checks its own `SECRETSPEC_AGENT` environment variable as an explicit opt-in: ```bash # Mark any harness the detector does not recognize as an agent: $ export SECRETSPEC_AGENT=1 ``` Cooperative harnesses that are not auto-detected can set `SECRETSPEC_AGENT=1`. Do not rely on a caller to identify itself when a reason is mandatory; use `require_reason = true` instead. The reason is recorded in secretspec’s own [audit log](/concepts/audit/) and is also forwarded to providers that support auditing (e.g. the [Proton Pass](/providers/protonpass/) provider records it in the agent audit log). ### \[defaults] Section (0.21+) [Section titled “\[defaults\] Section (0.21+)”](#defaults-section-021) **New in version 0.21** Set one project-wide provider chain for provider-backed secrets that do not choose providers themselves or through their active profile: secretspec.toml ```toml [defaults] providers = ["developer"] ``` | Field | Type | Required | Description | | ----------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `providers` | array\[string] | Yes | Non-empty default provider chain for every profile. Entries may be aliases, provider names, or provider URIs. | Project defaults deliberately accept only `providers`: a literal fallback value or requiredness policy rarely makes sense for every secret in every profile. The chain may name an alias defined in the project `[providers]` table or only in the current user’s `[defaults.providers]` table. A secret-level chain wins, followed by `[profiles..defaults].providers`, this project default, and finally the user-global default provider. ### \[profiles.\*] Section [Section titled “\[profiles.\*\] Section”](#profiles-section) Defines secret variables for different environments. At least one profile is required. A `default` profile is optional; when present, other profiles inherit from it unless they opt out in SecretSpec 0.19+. ```toml [profiles.default] # Optional shared base profile DATABASE_URL = { description = "PostgreSQL connection", required = true } API_KEY = { description = "External API key", required = true } REDIS_URL = { description = "Redis cache", required = false, default = "redis://localhost:6379" } [profiles.production] # Additional profile (optional) DATABASE_URL = { required = true } # description inherited from default ``` #### Profile defaults [Section titled “Profile defaults”](#profile-defaults) `[profiles..defaults]` supplies settings for secrets declared in that profile: | Field | Type | Required | Description | | ----------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `inherit` (0.19+) | boolean | No | For a non-default profile, whether to inherit declarations and omitted fields from `[profiles.default]` (default: true) | | `required` | boolean | No | Default requiredness for secrets declared in this profile | | `default` | string | No | Default value for secrets declared in this profile | | `providers` | array\[string] | No | Default provider chain for secrets declared in this profile. Overrides project `[defaults].providers` in 0.21+. | In SecretSpec 0.19+, set `inherit = false` for a standalone profile: ```toml [profiles.deployment.defaults] inherit = false [profiles.deployment] DEPLOY_TOKEN = { description = "Deployment credential", required = true } ``` This excludes every `[profiles.default]` declaration and prevents explicitly redeclared secrets from inheriting omitted fields. The setting has no effect on the `default` profile itself. A standalone profile must declare at least one secret. #### Cross-secret presence constraints [Section titled “Cross-secret presence constraints”](#cross-secret-presence-constraints-017) **New in version 0.17** A profile can require alternative credentials by assigning secrets to a named group: ```toml [profiles.default] PASSWORD = { description = "Account password", required = { at_least_one = "account_auth" } } ACCESS_TOKEN = { description = "Personal access token", required = { at_least_one = "account_auth" } } GITHUB_TOKEN = { description = "GitHub token", required = { exactly_one = "github_auth" } } GITHUB_APP_KEY = { description = "GitHub App private key", required = { exactly_one = "github_auth" } } ``` `at_least_one` requires one or more group members to resolve; `exactly_one` requires one. Each field also accepts an array of group names for overlapping groups. Groups must contain at least two secrets and cannot mix modes. Group members are individually optional. Under a [scope](#scopes-section), a group is judged over the members that scope exposes, so a scoped consumer never inherits a guarantee that rests on a secret it cannot see. #### Secret Variable Options [Section titled “Secret Variable Options”](#secret-variable-options) Each secret variable is defined as a table with the following fields: | Field | Type | Required | Description | | ------------------ | ------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `description` | string | Yes (see notes) | Human-readable description of the secret | | `required` | boolean or table | No | Whether absence is an error; the table form (0.17+) accepts `at_least_one`/`exactly_one` presence groups (defaults to true; false with `default` or a presence group) | | `default` | string | No | Default value if not provided | | `composed` (0.16+) | string | No | Derive a read-only value from other declared secrets using `${UPPERCASE_NAME}` references | | `providers` | array\[string] | No | List of provider aliases to use in fallback order | | `ref` | table | No | Coordinates naming an externally managed secret in the provider’s store (e.g. `ref = { item = "db", field = "password" }`) | | `refs` (0.19+) | table | No | Provider-alias-scoped coordinates, keyed by leaf alias (e.g. `refs = { source = { item = "old" }, target = { item = "new" } }`); mutually exclusive with `ref` | | `as_path` | boolean | No | Write secret to temp file and return file path (default: false) | | `encoding` (0.19+) | `"base64"`, `"base64url"`, or `"hex"` | No | Encode logical values before storage writes and decode stored values after reads | | `extract` (0.19+) | table | No | Select one logical value from stored JSON (0.19+) or INI (0.20+) data with a pointer | | `type` | string | No | Secret type for generation: `password`, `hex`, `base64`, `uuid`, `command`, `rsa_private_key`, `openpgp_private_key` (0.21+), `ssh_private_key` (0.21+) | | `generate` | boolean or table | No | Enable auto-generation when secret is missing | | `prompt` (0.19+) | boolean | No | Securely prompt for a missing value during `secretspec run`; the selected provider controls persistence | Field notes: * `description` is required on the effective secret. An inheriting profile may omit it when a matching default declaration supplies it. A standalone profile using `inherit = false` (0.19+) must supply its own description. * `required` defaults to false when `default` is provided. In 0.17+, its table form accepts `at_least_one` and `exactly_one` as a group name or array of names. * `default` is invalid with an explicit `required = true`. A defaulted secret is guaranteed to be present in successful resolution and generated types, even though the provider does not have to supply it. * `type` is required when `generate` is enabled. * `generate` and `default` cannot both be set. * `prompt = true` (0.19+) is for individually required secrets and cannot be combined with `default`, enabled `generate`, `extract`, or `composed`. * `extract` (0.19+) is read-only and cannot be combined with enabled `generate`. #### Composed Secrets [Section titled “Composed Secrets”](#composed-secrets) **New in version 0.16** A composed secret derives a value from other secrets in the effective profile. See [Composed Secrets](/concepts/composed-secrets/) for the dependency model, CLI behavior, profile inheritance, and the differences from dotenv expansion: ```toml [profiles.default] DB_USER = { description = "Database user" } DB_PASSWORD = { description = "Database password" } DB_HOST = { description = "Database host" } DATABASE_URL = { description = "PostgreSQL DSN", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" } ``` References form a static dependency graph. Declaration order does not matter, and composed secrets may reference other composed secrets. SecretSpec rejects unknown references, cycles, malformed references, and source conflicts while loading the manifest. A composed secret is read-only and cannot also set `default`, `providers`, `ref`, `refs` (0.19+), `type`, enabled `generate`, `encoding` (0.19+), or `extract` (0.19+). Composition intentionally does **not** implement dotenv or shell expansion: * only `${UPPERCASE_NAME}` is a reference, and the name must match `[A-Z][A-Z0-9_]*` and identify a declared secret; * ambient environment variables are never consulted; * fallback operators such as `${NAME:-fallback}`, commands, and recursive expansion are unsupported; * inserted values are opaque and are never scanned again; * `$$` produces a literal `$` (`$${NAME}` renders `${NAME}`), while ordinary braces are literal; * a missing dependency makes a required composition missing, while a `required = false` composition is omitted; * empty values remain empty and are distinct from missing values. If a dependency uses `as_path = true`, its exported temporary-file path is the text inserted into the composed value. Applying `as_path = true` to the composed secret materializes the final combined value. Composition is raw string concatenation. SecretSpec cannot know whether a component occupies a URL username, password, host, path, query, or structured document position, so it does not URL-encode or JSON-encode components. Store components in the form required by the target format; use `secretspec export --format json` when exporting the resolved secret map as JSON. ### \[scopes] Section [Section titled “\[scopes\] Section”](#scopes-section) **New in version 0.17** See [Scopes](/concepts/scopes/) for the conceptual model and a focused guide to narrowing services and tasks. This section specifies the complete configuration and resolution behavior. Scopes name membership-only subsets of a profile’s secrets, so a single service or task resolves only what it declares instead of the entire profile. They are **orthogonal to profiles**: a profile decides how each secret resolves (`required`, `default`, providers, references, generation, prompts (0.19+), `as_path`, `encoding` (0.19+), `extract` (0.19+), and the storage namespace); a scope only decides *which* secrets take part in a given resolution. ```toml [profiles.default] DATABASE_URL = { description = "Database", required = true } API_KEY = { description = "API key", required = true } QUEUE_TOKEN = { description = "Queue token", required = true } [scopes.api] secrets = ["DATABASE_URL", "API_KEY"] [scopes.worker] secrets = ["DATABASE_URL", "QUEUE_TOKEN"] ``` ```bash $ secretspec run --scope api -- ./api # sees DATABASE_URL, API_KEY $ secretspec run --scope worker -- ./worker # sees DATABASE_URL, QUEUE_TOKEN $ secretspec check --scope api $ secretspec export --scope worker --format dotenv ``` Behavior: * **No scope** resolves the complete profile, exactly as before scopes existed. * Selecting a scope resolves the **intersection** of the merged profile and the scope’s `secrets` list — the *visible* set. A secret the profile does not declare is simply absent from that resolution rather than an error, so a scope can be reused across profiles that declare different subsets. * A required secret **excluded** by the active scope does not block resolution — it is not part of the scoped set. * **Composed secrets resolve their inputs without exposing them.** When a visible [composed secret](/concepts/composed-secrets/) references secrets the scope leaves out (for example `DATABASE_URL` built from `DB_USER` and `DB_PASSWORD`), those dependencies are fetched to build the composition and then dropped from the output — the child sees `DATABASE_URL`, never `DB_USER`/`DB_PASSWORD`. A secret that is neither visible nor a dependency of a visible secret is never fetched, so no provider is contacted for it. * A scope does not change a secret’s storage address (`{project}/{profile}/{key}`); it only narrows the set. * **Presence groups are judged over the visible members.** A `required = { at_least_one = … }` or `{ exactly_one = … }` group (see [Cross-secret presence constraints](#cross-secret-presence-constraints-017)) is evaluated against the members the scope actually exposes. A group with no visible member is not that consumer’s concern and is not enforced. A group with some visible members is enforced over those alone, so a scope never inherits a guarantee that rests on a secret it hides — if `at_least_one = "cloud"` is satisfied profile-wide by `GCP_KEY`, a scope showing only `AWS_KEY` still fails when `AWS_KEY` is absent. `exactly_one` remains enforced whenever two visible members are both present: scoping narrows what is judged, never whether it is judged. A secret fetched only as a hidden composition input does not count as present, and a violation message names only visible members. The reverse case cannot be detected, because a secret the scope hides is never fetched: if `exactly_one = "token"` is violated profile-wide by both `PRIMARY` and `FALLBACK` being present, a scope showing only `PRIMARY` reports success. A scoped check validates the scoped consumer, not the profile; run an unscoped `secretspec check` to validate the profile as a whole. * `run --scope` removes **every** manifest-declared secret the scope does not admit from the launched command’s environment, across *all* profiles rather than only the selected one, **even if the parent shell already exported them**, so a value inherited from another profile cannot leak into the child. Membership decides this, so a secret the scope lists survives even when the selected profile does not declare it (see the admitted rule below). This is secret minimization, not an authorization boundary: a process that still holds provider credentials could resolve another scope itself. * `export --scope` **emits** the visible set but unsets nothing, since its output formats have no way to express an unset. Narrowing an environment that already holds a wider set therefore needs `run --scope`: after `eval "$(secretspec export)"`, a later `eval "$(secretspec export --scope api)"` leaves the previously exported values live in the shell. * An **empty** scope (or a scope whose intersection with the profile is empty) resolves to nothing and contacts no provider. * **Diagnostics do not name what the scope hides.** A provider warning about a hidden composition input calls it `a hidden composition input` rather than naming it, matching the way prompting is filtered, so a failing provider cannot disclose the very name the output filter removed. A visible secret is still named. This covers secretspec’s own messages; a provider’s error text is written by that provider and may still mention the address it searched. * [Audit](#audit-logging) records what was **read**, not what was exposed: a scoped `check` logs the accessed set, including a composition input the scope hides, since the point of the log is to capture provider access. A `run` event logs what it injected — the visible set. Scoped `check`, `run`, and `export` events also carry the selected `scope` name (SecretSpec 0.17+). * An `as_path` secret’s resolved value is its temp-file path, so a visible composition built from a hidden `as_path` input embeds that path. The file stays alive for the duration of the command rather than being cleaned up with the hidden secret, so the path resolves. The hidden input is still absent from the environment; only its content, in the form the composition derived, is reachable — the same contract as a composed DSN that embeds a password. * A secret the scope **admits** is never scrubbed from `run`, whether it fails to resolve (an optional secret with no stored value) or the selected profile does not declare it at all. A value the parent exported is inherited exactly as it would be without a scope; scoping changes which secrets are in play, never the semantics of one it admits. This is what lets a single scope be reused across profiles that declare different subsets. * Under project `extends`, a child `[scopes.]` **replaces** the parent scope of the same name outright — the two `secrets` lists are not unioned (see [Configuration Inheritance](/concepts/inheritance/)). * Selecting an undefined scope, or a scope that lists a secret no profile declares, is a configuration error. * A scope’s `secrets` list must name at least one secret, with no blank or repeated entries. An empty scope is rejected rather than treated as “resolves to nothing”: it would contact no provider, so `check --scope` would report a clean `0 found, 0 missing` while `run --scope` started the command with every manifest secret scrubbed and none injected. An empty *intersection* between a valid scope and the selected profile is still fine, since a scope is meant to be reused across profiles that declare different subsets. The `--scope` flag (and the `SECRETSPEC_SCOPE` environment variable) apply to `check`, `run`, and `export`. Scopes are a resolution-time feature of these untyped paths. The write and copy commands are unaffected: `set` and `import` ignore an ambient `SECRETSPEC_SCOPE` entirely, so a scope neither restricts what they may write nor narrows the secrets they list. The untyped language SDK builders also accept an explicit scope and return its name in resolve/report results, and they honor `SECRETSPEC_SCOPE` when given none. The typed SDK loaders generated by `secretspec-derive` always resolve the **full** profile and deliberately **ignore** an ambient `SECRETSPEC_SCOPE`, since a generated struct expects every declared field. A **blank** `--scope` clears an inherited scope rather than being ignored: `SECRETSPEC_SCOPE=api secretspec run --scope "" -- ./job` resolves the whole profile and scrubs nothing. A blank `SECRETSPEC_SCOPE` with no flag means the same, so a CI template that materializes an unset variable as an empty string cannot silently narrow a job. ## Complete Example [Section titled “Complete Example”](#complete-example) secretspec.toml ```toml [project] name = "web-api" revision = "1.0" extends = ["../shared/secretspec.toml"] # Optional inheritance # Provider aliases used by profile provider chains [providers] prod_vault = "onepassword://Production" shared_vault = "onepassword://Shared" keyring = "keyring://" env = "env://" # Default profile - always loaded first [profiles.default] APP_NAME = { description = "Application name", required = false, default = "MyApp" } SESSION_SECRET = { description = "Session signing secret", required = true, providers = ["shared_vault"] } GITHUB_TOKEN = { description = "GitHub token", required = true, providers = ["env"] } # Development profile - extends default [profiles.development] DATABASE_URL = { description = "Database connection", required = false, default = "sqlite://./dev.db" } API_URL = { description = "API endpoint", required = false, default = "http://localhost:3000" } DEBUG = { description = "Debug mode", required = false, default = "true" } # Production profile - extends default [profiles.production] DATABASE_URL = { description = "PostgreSQL cluster connection", required = true, providers = ["prod_vault", "keyring"] } API_URL = { description = "Production API endpoint", required = true } SENTRY_DSN = { description = "Error tracking service", required = true, providers = ["shared_vault"] } REDIS_URL = { description = "Redis cache connection", required = true } ``` ### Provider Aliases [Section titled “Provider Aliases”](#provider-aliases) **Changed in version 0.15** SecretSpec 0.14 accepts only bare URI strings; when using 0.14, configure provider credentials through the provider’s existing environment variables, such as `BWS_ACCESS_TOKEN`. Provider alias `ref` templates are available starting with SecretSpec 0.19. Cached alias tables with `fallback` and `cache` are available since SecretSpec 0.17. Provider aliases may be declared in two places: 1. **In `secretspec.toml`** — a top-level `[providers]` table. Check this into version control so every team member and CI runner sees the same mapping out of the box. 2. **In `~/.config/secretspec/config.toml`** — a per-user `[defaults.providers]` table for personal overrides. On conflict the project-level alias wins, so a stale local config cannot silently shadow the team’s mapping. secretspec.toml ```toml [providers] prod_vault = "onepassword://Production" shared_vault = "onepassword://Shared" keyring = "keyring://" env = "env://" [profiles.production] DATABASE_URL = { description = "Production DB", providers = ["prod_vault", "keyring"] } ``` \~/.config/secretspec/config.toml ```toml [defaults] provider = "keyring" [defaults.providers] prod_vault = "onepassword://Production" shared_vault = "onepassword://Shared" keyring = "keyring://" env = "env://" ``` Manage user-level aliases via CLI: ```bash # SecretSpec 0.17+: add a provider alias to your user config $ secretspec config global provider add prod_vault "onepassword://Production" # SecretSpec 0.17+: list all aliases known to your user config $ secretspec config global provider list # SecretSpec 0.17+: remove an alias from your user config $ secretspec config global provider remove prod_vault ``` These explicitly scoped CLI commands operate on the user-global config only — edit `secretspec.toml` by hand to change project-level aliases. #### Credential-aware alias values [Section titled “Credential-aware alias values”](#secretspec-015-alias-values) **Changed in version 0.15** Provider aliases now also accept a table with a `uri` and provider credential sources; releases through 0.14 accept only a bare provider URI string. In SecretSpec 0.15 and later, an alias value is either a bare provider URI string or a table that also declares the credentials the provider needs. Both forms are accepted in the project `[providers]` and user `[defaults.providers]` tables. | Field | Type | Required | Description | | ------------- | ------ | ---------------- | ------------------------------------------------------------------------------------------------------------------ | | `uri` | string | Yes (table form) | The provider URI. A bare-string alias is shorthand for `{ uri = "..." }`. | | `credentials` | table | No | Maps a semantic [provider credential](/reference/provider-credentials/) name to its source. | | `ref` (0.19+) | table | No | Native-address template for this leaf alias. Coordinate strings may contain `{project}`, `{profile}`, and `{key}`. | Each `credentials` value is either a bare provider spec — read at the convention path for the active project and profile — or a table `{ provider = "...", ref = { ... } }` that pins the exact location with the same `ref` coordinates a secret uses. secretspec.toml ```toml [providers] keyring = "keyring://" # bare string: read access_token from keyring at the convention path bws = { uri = "bws://project-uuid", credentials = { access_token = "keyring" } } [providers.vault_prod] uri = "vault://secret/myapp?auth=approle" credentials = { role_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "role_id" } }, secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "secret_id" } } } ``` Configured credentials take precedence over provider environment fallbacks, credential chains are limited to one hop, and a fetched credential is never written to the environment. Store the credentials with [`secretspec config provider login`](/reference/cli/#config-provider-login). See [Provider credentials](/concepts/providers/#provider-credentials) for the full behavior. Starting with SecretSpec 0.19, a leaf alias may also compile logical secret names into that provider’s native coordinates. Templates expand each placeholder once; text inserted from a project, profile, or key is never interpreted as another placeholder. secretspec.toml ```toml [providers] remote = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } local = { uri = "dotenv://.env", ref = { item = "{key}" } } [profiles.production] API_KEY = { description = "API key", providers = ["remote", "local"] } ``` Templates belong on the leaf aliases in a cached route, not on the cached alias itself. Bare provider names and literal URIs have no alias identity, so they use provider convention naming unless the secret declares legacy `ref`. #### Inline provider cache [Section titled “Inline provider cache”](#secretspec-019-inline-provider-cache) **Changed in version 0.19** Single-provider caches now use `uri` and `cache` on the same alias; versions 0.17 and 0.18 require a cached fallback alias. Use `uri` and `cache` when one provider is authoritative. `credentials` remains optional and configures that same provider: | Field | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------- | | `uri` | string | Yes | Authoritative provider URI. | | `credentials` | table | No | Provider-specific credential sources for `uri`. | | `cache` | table | Yes | Local cache policy containing `provider` and `max_age`. | | `cache.provider` | string | Yes | Leaf provider spec used to store cache entries. Must support deletion and address a different store from `uri`. | | `cache.max_age` | string | Yes | Positive duration with `s`, `m`, `h`, `d`, or `w` units, such as `30m`, `8h`, or `1d`. | secretspec.toml ```toml [providers] local = "keyring://secretspec/cache/{project}/{profile}/{key}" azure = { uri = "akv://team-vault", credentials = { client_secret = "keyring" }, cache = { provider = "local", max_age = "8h" } } [profiles.development.defaults] providers = ["azure"] ``` The alias remains both the selected cached route and the build key for its authoritative provider, so its configured credentials apply normally. #### Cached fallback alias values [Section titled “Cached fallback alias values”](#secretspec-017-cached-fallback-alias-values) **New in version 0.17** A cached fallback alias uses `fallback` and `cache` when more than one provider can answer: | Field | Type | Required | Description | | ---------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fallback` | array\[string] | Yes | Non-empty authoritative provider route. Reads try entries in order; writes use the first entry. | | `cache` | table | Yes | Local cache policy containing `provider` and `max_age`. | | `cache.provider` | string | Yes | Leaf provider spec used to store cache entries. Must support deletion (keyring, pass, gopass, dotenv, age (0.20+), Azure App Configuration (0.20+), or Vault/OpenBao KV v2) and be a different store from every `fallback` entry. | | `cache.max_age` | string | Yes | Positive duration with `s`, `m`, `h`, `d`, or `w` units, such as `30m`, `8h`, or `1d`. | secretspec.toml ```toml [providers] azure = { uri = "akv://team-vault", credentials = { client_secret = "keyring" } } env = "env://" local = "keyring://secretspec/cache/{project}/{profile}/{key}" myprovider = { fallback = ["azure", "env"], cache = { provider = "local", max_age = "8h" } } [profiles.development.defaults] providers = ["myprovider"] ``` Every cached alias is a complete route and must be the only entry when selected through `providers`, in any position. Fallback entries and the cache provider accept aliases, provider names, and URIs, but must resolve to leaf providers; cached aliases cannot be nested, and the cache must resolve to a different store than the route’s own authoritative providers, since it holds its entries at the same logical address. The cache provider must also be one SecretSpec can delete from — keyring, pass, gopass, dotenv, age (0.20+), Azure App Configuration (0.20+), or a Vault/OpenBao KV v2 mount — since every form of invalidation is a delete. Put credentials on leaf aliases rather than the cached fallback alias. See [Provider caching](/concepts/providers/caching/) for freshness, failure, invalidation, and clearing behavior. #### Legacy bare-URI alias values [Section titled “Legacy bare-URI alias values”](#secretspec-014-alias-values) **Changed in version 0.15** Provider aliases gained table-form values in 0.15. Releases through 0.14 require every alias value to be a bare provider URI string. In SecretSpec 0.14, every alias value must be a provider URI string: secretspec.toml ```toml [providers] bws = "bws://project-uuid" ``` For example, authenticate the 0.14 BWS provider by setting its environment variable before running SecretSpec: ```bash $ export BWS_ACCESS_TOKEN="0.your-access-token..." $ secretspec check ``` ### Audit Logging [Section titled “Audit Logging”](#audit-logging) secretspec records every secret access to a local [audit log](/concepts/audit/). Auditing is a per-machine/operator concern — where the log lives and whether it is on — so it is configured in the **user-global config**, not the project’s `secretspec.toml`. A cloned repository therefore cannot redirect or silence your audit log. Auditing is **on by default**; configure it under the top-level `[audit]` table: \~/.config/secretspec/config.toml ```toml [audit] enabled = true # set false to turn auditing off path = "~/.local/state/secretspec/audit.log" # default: per-user XDG state dir max_size_bytes = 1048576 # default: 1 MiB ``` | Field | Type | Default | Description | | ---------------- | ------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | boolean | `true` | Whether to record secret access. | | `path` | string | per-user state dir | Where to write the JSON Lines log. Must be absolute (a leading `~` is expanded); a relative path is rejected and auditing is disabled. | | `max_size_bytes` | integer | `1048576` (1 MiB) | Hard size cap. At the cap the file is truncated and restarted; no rotated backups are kept. | Secret values are never written to the log, and credentials embedded in provider URIs are redacted. Audit failures never block secret access. See [Audit Logging](/concepts/audit/) for the record format and full details. ### as\_path Option [Section titled “as\_path Option”](#as_path-option) When `as_path = true`, the secret value is written to a temporary file and the file path is returned instead of the value: ```toml [profiles.default] TLS_CERT = { description = "TLS certificate", as_path = true } GOOGLE_APPLICATION_CREDENTIALS = { description = "GCP service account", as_path = true } ``` When combined with `encoding` (0.19+), the file contains the decoded bytes rather than the stored textual representation. When combined with `extract` (0.19+), it contains only the selected logical value. | Context | Behavior | | --------------------------- | --------------------------------------------------------------------------------------- | | CLI (`get`, `check`, `run`) | Files are persisted (not deleted after command exits) | | Rust SDK | Files cleaned up when `ValidatedSecrets` is dropped; use `keep_temp_files()` to persist | | Rust SDK types | `PathBuf` or `Option` instead of `String` | ### Secret Encoding [Section titled “Secret Encoding”](#secret-encoding-019) **New in version 0.19** `encoding` (0.19+) defines the textual representation stored by providers and the cache. It is independent of `as_path`: decoded UTF-8 remains an ordinary environment or SDK value, while arbitrary decoded bytes can be materialized to a file. ```toml [profiles.default] # encoding is available in SecretSpec 0.19+ TEXT_CONFIG = { description = "Encoded text", encoding = "base64" } KEYSTORE = { description = "Binary mTLS keystore", encoding = "base64", as_path = true } URL_SAFE_KEY = { description = "URL-safe encoded key", encoding = "base64url", as_path = true } HEX_KEY = { description = "Hex-encoded key", encoding = "hex", as_path = true } ``` | Encoding (0.19+) | Written representation | Accepted stored representation | | ---------------- | ---------------------------------------- | ------------------------------------------ | | `base64` | RFC 4648 standard Base64 with padding | Padded or unpadded standard Base64 | | `base64url` | RFC 4648 URL-safe Base64 without padding | Padded or unpadded URL-safe Base64 | | `hex` | Lowercase RFC 4648 Base16 | Uppercase, lowercase, or mixed-case Base16 | Exactly one trailing LF or CRLF is accepted so command-captured values work without preprocessing. Other whitespace and non-alphabet characters are rejected. Without `as_path = true`, decoded bytes must be valid UTF-8. `secretspec set`, interactive prompts, and generated secrets provide logical text; SecretSpec encodes it before writing to a provider or cache. Defaults and composed results are already logical and are not transformed. The `secretspec import` command copies the stored representation verbatim, avoiding double encoding. ### Structured Extraction [Section titled “Structured Extraction”](#structured-extraction-019) **New in version 0.19** **Changed in version 0.20** Structured extraction now supports INI documents with `format = "ini"`. `extract` (0.19+) selects one logical secret from structured text read from a provider or cache. It supports JSON (0.19+) and INI (0.20+). JSON `pointer` values are [RFC 6901 JSON Pointers](https://www.rfc-editor.org/rfc/rfc6901): ```toml [providers] documents = "file:./secrets" [profiles.default] # extract is available in SecretSpec 0.19+ DB_USER = { description = "Database user", providers = ["documents"], ref = { item = "application.json" }, extract = { format = "json", pointer = "/database/user" } } DB_PASSWORD = { description = "Database password", providers = ["documents"], ref = { item = "application.json" }, extract = { format = "json", pointer = "/database/password" } } ``` Both declarations read the same document. `/database/password` walks nested objects, `/hosts/0` selects an array element, and `/a~1b/~0key` selects the key `~key` beneath an `a/b` object. The empty pointer selects the complete document. JSON strings become their unquoted contents. Numbers, booleans, and `null` use their JSON spelling; objects and arrays become compact JSON. Invalid JSON or a pointer that does not match is a decoding error. Once a provider returns a document, extraction failure is not treated as a provider miss and does not continue along a fallback chain. INI extraction (0.20+) uses the same RFC 6901 escaping for pointer segments but accepts only value selectors. `/key` selects an unsectioned key, while `/section/key` selects a key in a named section: ```toml [profiles.default] # format = "ini" requires SecretSpec 0.20+ DB_PASSWORD = { description = "Database password", providers = ["documents"], ref = { item = "application.ini" }, extract = { format = "ini", pointer = "/database/password" } } ``` For example, that pointer reads `password` from `[database]`. An explicit `[DEFAULT]` section is selected as `/DEFAULT/key`; it is distinct from an unsectioned `/key`. Section and key matching is case-sensitive. `~1` selects a literal `/` and `~0` selects a literal `~`, just as in JSON Pointer. INI values always remain strings, and literal backslashes are preserved. Empty pointers, pointers deeper than `/section/key`, malformed INI, and unmatched pointers are decoding errors. Stored-value transforms run in this order: ```text provider or cache → encoding decode → structured extraction → as_path ``` This makes a Base64-encoded JSON document valid input when a declaration sets both `encoding = "base64"` (0.19+) and `extract` (0.19+). A provider-native `ref.field` is also resolved first, so a field whose contents are JSON can be selected further. Defaults and composed values are already logical and are not extracted. Extracted secrets are read-only. `set`, `delete`, interactive prompting, generation, and `import` reject them rather than replacing or removing the containing document and its sibling values. Update the document through its owning system instead. ### Secret References [Section titled “Secret References”](#secret-references) The `ref` field names one externally managed secret by the store’s own coordinates, instead of SecretSpec’s `{project}/{profile}/{key}` convention. See [Secret References](/concepts/references/) for the concept, model, and examples; this section is the specification. ```toml [profiles.production] DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["prod_vault"] } INFRA_TOKEN = { description = "Infra token", ref = { vault = "Production", item = "infra", field = "token" } } GITHUB_TOKEN = { description = "GitHub token", ref = { item = "GITHUB_PAT" }, providers = ["env"] } ``` `ref` is a table of provider-independent coordinates. Unknown keys are rejected at parse time. Only `item` is universal; it is the secret’s complete name in the store and replaces the whole convention path, including any `folder_prefix` or format string the provider is configured with (nothing is prepended). A coordinate a store has no equivalent for is rejected with an error naming it, never silently ignored. | Coordinate | Required | Meaning | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `item` | Yes | The store’s complete name for the secret. Replaces the whole convention path | | `field` | No | A named component inside the item. Rejected by stores whose secrets hold a single value | | `vault` | No | The container holding the item. 1Password only; other stores take their container from the provider URI | | `section` | No | A named group of fields inside the item. 1Password only; requires `field` | | `version` | No | Which revision of the secret to read. Supported by versioned stores such as Google Secret Manager, AWS Parameter Store (0.18+), and Azure Key Vault (0.20+); defaults to the latest | Stores fall into two groups for `field`: | Store | Shape of one secret | `field` | | ---------------------------------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------- | | dotenv, file (0.19+), env, pass, LastPass, Proton Pass, Bitwarden, AWS Parameter Store (0.18+) | a single value | Rejected: there is nothing to select | | 1Password, Keeper (0.18+), Passbolt (0.19+), Vault KV, AWS Secrets Manager, keyring | a record of named parts | Selects the part: field label, map key, JSON key, account | `vault` is the only container coordinate. For every store except 1Password the container is part of the provider URI, not the ref: ```toml # The mount `kv2` comes from the URI; the ref names the path inside it. DB = { description = "DB", ref = { item = "myapp/config", field = "pw" }, providers = ["vault://vault.example.com:8200/kv2"] } # 1Password: `vault` on the ref overrides the URI's default vault. TOKEN = { description = "Token", ref = { vault = "Production", item = "infra", field = "token" }, providers = ["onepassword://Private"] } ``` Which provider resolves a `ref` follows the ordinary [provider resolution order](/concepts/providers/fallback/); a `ref` composes with the `providers` fallback chain, and each provider is asked for the same coordinates. #### Provider-scoped references [Section titled “Provider-scoped references”](#provider-scoped-references-019) **New in version 0.19** Use `refs` when one logical secret already has different native coordinates in different providers. Keys are leaf provider aliases; they are identity, not a URI lookup, so aliases that happen to resolve to the same URI remain distinct. An entry may name an import-only source alias that is absent from the secret’s ordinary `providers` route. ```toml [providers] old = "onepassword://Legacy" new = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } } local = "keyring://" [profiles.production] API_KEY = { description = "API key", providers = ["new", "local"], refs = { old = { item = "legacy-api", field = "token" } } } ``` For each selected endpoint, address resolution is: 1. Legacy route-wide `ref`, when present (for compatibility). 2. The matching `refs.` entry. 3. The matching alias’s `ref` template. 4. The provider’s ordinary `{project}/{profile}/{key}` convention. `ref` and `refs` cannot be combined on one effective secret. Every `refs` key must name a defined leaf alias; cached route aliases cannot own templates or be used as scoped-ref keys. A literal URI or bare provider name has no alias key, so only legacy `ref` or convention naming applies to it. During profile inheritance, `ref` and `refs` (0.19+) form one setting rather than two independently inherited fields. The most specific profile entry that declares either form supplies the whole setting: an explicit `refs` replaces an inherited `ref`, and an explicit `ref` replaces inherited `refs`. If the profile entry declares neither, it inherits whichever form `[profiles.default]` uses. #### How providers interpret the coordinates [Section titled “How providers interpret the coordinates”](#how-providers-interpret-the-coordinates) | Provider | `item` | `field` | Without `field` | Writes via ref | | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | [1Password](/providers/onepassword/#use-existing-secrets) | Item title or UUID | Field label; `vault` and `section` also apply | Reads the item like a convention secret (its value or password field); writes edit the `value` field | ✅ via `op item edit` (adds a missing field, never creates items) | | [Keeper (0.18+)](/providers/keeper/#use-existing-records) | Record UID or exact title | Standard field type/label or custom field label | Reads `password` | ✅ for existing records and fields | | [keyring](/providers/keyring/#use-existing-secrets) | Service | Account (defaults to the current system username) | Current user’s entry | ✅ | | [dotenv](/providers/dotenv/#use-existing-secrets) | `.env` key | Rejected | Reads the key | ✅ | | [file (0.19+)](/providers/file/#use-existing-files) | Relative file path beneath the configured root | Rejected | Reads the complete UTF-8 file | ✅ | | [env](/providers/env/#use-existing-secrets) | Variable name | Rejected | Reads the variable | — (read-only) | | [EJSON (0.20+)](/providers/ejson/#use-existing-secrets) | RFC 6901 JSON Pointer | Rejected | Reads the selected JSON string | — (read-only) | | [systemd credentials (0.17+)](/providers/systemd-credential/#use-an-existing-credential-name) | Credential filename | Rejected | Reads the credential | — (read-only) | | [Fly.io secrets (0.20+)](/providers/fly/#use-existing-secrets) | Fly app secret name | Rejected | Error: Fly.io does not expose plaintext values | ✅ write-only via `flyctl secrets set` | | [Cloudflare Secrets Store (0.20+)](/providers/cloudflare/#use-existing-secrets-020) | Account-secret name in the selected store | Rejected | Error: Cloudflare’s management API does not expose plaintext values | ✅ write-only via the Cloudflare API | | [pass](/providers/pass/#use-existing-secrets) | Entry path | Rejected | Reads the entry | ✅ | | [Gopass (0.15+)](/providers/gopass/#use-existing-secrets) | Entry path, including any mount-point prefix | Rejected | Reads the entry | ✅ | | [LastPass](/providers/lastpass/#use-existing-secrets) | Item name | Rejected | Reads the item | ✅ | | [Dashlane (0.18+)](/providers/dashlane/#use-existing-secrets) | Item title or identifier | Field name on the item | Reads the type’s default field (`content`, or `password` for a login) | — (read-only) | | [Proton Pass](/providers/protonpass/#use-existing-secrets) | Item title | Rejected | Reads the note | ✅ | | [Passbolt (0.19+)](/providers/passbolt/#use-existing-resources) | Resource UUID or exact name | `password`, `username`, `uri`, or `description` | Reads `password` | ✅ for existing resources; never creates through `ref` | | [Vault](/providers/vault/#use-existing-secrets) | KV path relative to the mount | Required (KV entries are maps) | Error | — (read-only) | | [OpenBao](/providers/openbao/#use-existing-secrets) (0.17+) | KV path relative to the mount | Required (KV entries are maps) | Error | — (read-only) | | [AWS Secrets Manager](/providers/awssm/#use-existing-secrets) | Secret name or ARN | JSON key | Whole secret string | — (read-only) | | [AWS Parameter Store (0.18+)](/providers/awsps/#use-existing-parameters) | Parameter name or ARN; `version` selects a version or label | Rejected | Reads the decrypted value | ✅ by unversioned parameter name; version, label, and ARN refs are read-only | | [GCSM](/providers/gcsm/#use-existing-secrets) | Secret id; `version` also applies | Rejected | Reads latest or the pinned version | — (read-only) | | [Bitwarden (bws)](/providers/bws/#use-existing-secrets) | BWS key name | Rejected | Reads the key | ✅ | | [Azure Key Vault (0.15+)](/providers/akv/#use-existing-secrets) | Secret name; `version` pins a version (0.20+) | Rejected | Reads latest or the pinned version (0.20+) | — (read-only) | | [Azure App Configuration (0.20+)](/providers/aac/#use-existing-key-values) | App Configuration key | Rejected | Reads the direct value or resolves its canonical Key Vault reference | — (read-only) | | [Infisical (0.16+)](/providers/infisical/#use-existing-secrets) | Folder and key; `version` also applies | Rejected | Reads the latest version | ✅ unless a version is pinned | | [Kubernetes (0.20+)](/providers/kubernetes/#use-existing-secrets) | Secret key | Rejected | Reads entry | ✅ | A provider rejects coordinates it has no equivalent for, with an error naming the coordinate (for example, `field` on the env provider). #### Writing through a ref [Section titled “Writing through a ref”](#writing-through-a-ref) Writes are symmetric with reads: `secretspec set` and interactive `check` prompting write through the coordinates in place wherever the table above says writes are supported. Read-only stores fail with a clear error instead. #### No string refs [Section titled “No string refs”](#no-string-refs) `ref` is always a table. String and URI forms (`ref = "op://vault/item/field"`, `ref = "env://VAR"`, query-parameter URIs, and similar) are rejected, and the error spells out the exact table translation. For example, a pasted 1Password reference `op://Production/infra/token` translates to: ```toml INFRA_TOKEN = { description = "Infra token", ref = { vault = "Production", item = "infra", field = "token" }, providers = ["onepassword://Production"] } ``` Provider URIs stay store addresses only: `onepassword://Production` names a vault, and item paths on provider URIs are errors. #### Deduplication, auditing, and reporting [Section titled “Deduplication, auditing, and reporting”](#deduplication-auditing-and-reporting) * Secrets sharing identical coordinates and store are fetched once. * [Audit log](/concepts/audit/) events carry a `ref` field with the coordinates. * `check --explain` and `check --json` attribute ref secrets to the store URI they resolved from. ### Prompt on missing during run [Section titled “Prompt on missing during run”](#prompt-on-missing-during-run-019) **New in version 0.19** Use `prompt = true` when `secretspec run` should ask the operator after every configured provider has returned missing. Prompting is the value source; persistence remains a property of the selected provider. With a writable provider, the answer is saved and reused by later runs. The write destination and writability are checked before the hidden prompt opens, just as they are for `secretspec set`. Use the `null` provider when the answer must exist only for one child invocation: ```toml [profiles.default] DEPLOY_PASSWORD = { description = "One-time deployment password", required = true, prompt = true, providers = ["null"] } ``` Here `null` makes the operator the only possible value source and explicitly declines persistence, so the answer is injected into the child environment and discarded after it exits. It is not written to a provider or cache. The prompt uses the controlling terminal rather than the command’s stdin, so a pipe or redirected file remains available to the child: ```bash $ printf 'deployment input\n' | secretspec run -- ./deploy ? Enter value for DEPLOY_PASSWORD (profile: default): ``` Only `run` interprets `prompt = true` as a missing-value policy. `get`, `export`, SDK resolution, and value-free reports do not prompt. Interactive `check` retains its existing setup behavior instead: it offers to store any missing required secret, independently of `prompt`, and therefore cannot satisfy a `null`-backed declaration. A `run` without a controlling terminal fails before starting the child. Explicit `set` and import operations remain governed by the provider, not by `prompt`. `prompt = true` is limited to individually required secrets and cannot be combined with `default`, enabled `generate`, `extract`, or `composed`. Profile overrides may set `prompt = false` to return to ordinary missing-value behavior. ### Secret Generation [Section titled “Secret Generation”](#secret-generation) **New in version 0.7** When `type` and `generate` are set, missing secrets are automatically generated during `check` or `run` and stored via the configured provider: ```toml [profiles.default] # Simple: generate with type defaults DB_PASSWORD = { description = "Database password", type = "password", generate = true } REQUEST_ID = { description = "Request ID prefix", type = "uuid", generate = true } # Custom options API_TOKEN = { description = "API token", type = "hex", generate = { bytes = 32 } } SESSION_KEY = { description = "Session key", type = "base64", generate = { bytes = 64 } } # Shell command MONGO_KEY = { description = "MongoDB keyfile", type = "command", generate = { command = "openssl rand -base64 765" } } # RSA private key (PKCS1 PEM) JWT_SIGNING_KEY = { description = "JWT signing key", type = "rsa_private_key", generate = true } # OpenPGP signing key (0.21+) RELEASE_KEY = { description = "Release signing key", type = "openpgp_private_key", generate = { user_id = "Release Bot ", capabilities = ["sign"] } } # OpenSSH Ed25519 private key (0.21+) DEPLOY_KEY = { description = "Deployment SSH key", type = "ssh_private_key", generate = true } # Type without generate: informational only, no auto-generation MANUAL_SECRET = { description = "Manually managed", type = "password" } ``` #### Generation Types [Section titled “Generation Types”](#generation-types) | Type | Default Output | Options | | ----------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `password` | 32 alphanumeric chars | `length` (int), `charset` (`"alphanumeric"` or `"ascii"`) | | `hex` | 64 hex chars (32 bytes) | `bytes` (int) | | `base64` | 44 chars (32 bytes) | `bytes` (int) | | `uuid` | UUID v4 (36 chars) | none | | `command` | stdout of command | `command` (string, required) | | `rsa_private_key` | 2048-bit RSA private key (PKCS1 PEM) | `bits` (int) | | `openpgp_private_key` (0.21+) | ASCII-armored OpenPGP v4 transferable secret key | `user_id` (required), `algorithm` (`"ed25519"` or `"rsa"`), `bits` (RSA only), `capabilities` (`["sign"]`, `["encrypt"]`, or both) | | `ssh_private_key` (0.21+) | Unencrypted OpenSSH Ed25519 private key | `algorithm` (`"ed25519"` or `"rsa"`), `bits` (RSA only), `comment` (string) | #### OpenPGP private-key generation [Section titled “OpenPGP private-key generation”](#openpgp-private-key-generation-021) **New in version 0.21** `openpgp_private_key` is generated entirely in Rust and does not invoke GnuPG. The default `algorithm = "ed25519"` creates an Ed25519 certification-only primary key plus separate Ed25519 signing and/or Curve25519 encryption subkeys. For compatibility with RSA-only consumers, `algorithm = "rsa"` uses RSA for the primary key and all requested subkeys. RSA defaults to 3072 bits; `bits` accepts 2048 through 8192 and is invalid with `"ed25519"`. Omitting `capabilities` selects both; the list must otherwise contain `"sign"`, `"encrypt"`, or both without duplicates. `generate = true` is invalid because every generated certificate requires an explicit `user_id`. The ASCII-armored transferable secret key has no OpenPGP passphrase and no expiration. Store it with an encrypted provider when it needs protection at rest. Its public certificate and fingerprint can be derived after import by OpenPGP tooling; SecretSpec stores the secret key as one logical value. #### SSH private-key generation [Section titled “SSH private-key generation”](#ssh-private-key-generation-021) **New in version 0.21** `ssh_private_key` is generated entirely in Rust. `generate = true` creates an unencrypted Ed25519 OpenSSH private key. Select `algorithm = "rsa"` for compatibility; RSA defaults to 3072 bits and accepts 2048 through 8192. `bits` is invalid with Ed25519. An optional `comment` is embedded in the key and must not contain control characters. #### Behavior [Section titled “Behavior”](#behavior) * Generation only triggers when a secret is **missing** — existing secrets are never overwritten * Generated values are stored via the secret’s configured provider (or the default provider) * With `providers = ["null"]` (0.19+), a fresh generated value is returned only for the current resolution and is not written to provider storage * Subsequent runs find the stored value and skip generation (idempotent) * `generate` and `default` cannot both be set on the same secret * `type = "command"` requires `generate = { command = "..." }` (not just `generate = true`) * `type = "openpgp_private_key"` (0.21+) requires `generate.user_id`; omitted `algorithm` and `capabilities` default to Ed25519/Curve25519 and both signing and encryption, respectively * `type = "ssh_private_key"` (0.21+) defaults to Ed25519; RSA generation is available with `generate = { algorithm = "rsa", bits = 4096 }` * The value-free preflights — [`check --json` / `check --explain`](/reference/cli/#resolution-report---json----explain) and the SDKs’ report/no-values resolutions — never mint a value. Since SecretSpec 0.20 a **required** generatable secret that no provider holds is reported as `missing_required` there (and exits non-zero) until a `check` or `run` provisions it; an optional one, or one stored in a provider that never retains generated values such as `null`, is reported as *will generate* ## Profile Inheritance [Section titled “Profile Inheritance”](#profile-inheritance) * Non-default profiles inherit from `[profiles.default]` when it exists; `profiles..defaults.inherit = false` makes a profile standalone in SecretSpec 0.19+ * Profile-specific values override default values * `ref` and `refs` (0.19+) are alternative forms of one setting: declaring either in a profile replaces the form inherited from `[profiles.default]`, while declaring neither inherits it * Use the `extends` field in `[project]` to inherit from other secretspec.toml files # Provider credentials > Semantic provider credential names and their environment fallbacks Provider credentials let one provider load the authentication material it needs from another SecretSpec provider. They are supported in SecretSpec 0.15 and later. The table below is the exhaustive reference for accepted semantic credential names. An explicitly configured provider credential takes precedence over its environment fallback. When more than one fallback is listed, SecretSpec checks them from left to right. | Provider | Credential | Environment fallback | Available since | | ---------------------------------------- | --------------------------- | ------------------------------------ | --------------- | | [`aac`](/providers/aac/) | `tenant_id` | `AZURE_TENANT_ID` | 0.20+ | | [`aac`](/providers/aac/) | `client_id` | `AZURE_CLIENT_ID` | 0.20+ | | [`aac`](/providers/aac/) | `client_secret` | `AZURE_CLIENT_SECRET` | 0.20+ | | [`aac`](/providers/aac/) | `connection_string` | `AZURE_APPCONFIG_CONNECTION_STRING` | 0.20+ | | [`age`](/providers/age/) | `identity` | `AGE_IDENTITY` | 0.17+ | | [`akv`](/providers/akv/) | `tenant_id` | `AZURE_TENANT_ID` | 0.15+ | | [`akv`](/providers/akv/) | `client_id` | `AZURE_CLIENT_ID` | 0.15+ | | [`akv`](/providers/akv/) | `client_secret` | `AZURE_CLIENT_SECRET` | 0.15+ | | [`bws`](/providers/bws/) | `access_token` | `BWS_ACCESS_TOKEN` | 0.15+ | | [`cloudflare`](/providers/cloudflare/) | `api_token` | `CLOUDFLARE_API_TOKEN` | 0.20+ | | [`dashlane`](/providers/dashlane/) | `service_device_keys` | `DASHLANE_SERVICE_DEVICE_KEYS` | 0.18+ | | [`ejson`](/providers/ejson/) | `private_key` | — | 0.20+ | | [`fly`](/providers/fly/) | `access_token` | `FLY_API_TOKEN` → `FLY_ACCESS_TOKEN` | 0.20+ | | [`infisical`](/providers/infisical/) | `client_id` | `INFISICAL_CLIENT_ID` | 0.16+ | | [`infisical`](/providers/infisical/) | `client_secret` | `INFISICAL_CLIENT_SECRET` | 0.16+ | | [`infisical`](/providers/infisical/) | `token` | `INFISICAL_TOKEN` | 0.16+ | | [`kdbx`](/providers/kdbx/) | `password` | `SECRETSPEC_KDBX_PASSWORD` | 0.17+ | | [`keeper`](/providers/keeper/) | `config` | `KSM_CONFIG` | 0.18+ | | [`keeper`](/providers/keeper/) | `token` | `KSM_TOKEN` | 0.18+ | | [`onepassword`](/providers/onepassword/) | `service_account_token` | `OP_SERVICE_ACCOUNT_TOKEN` | 0.15+ | | [`openbao`](/providers/openbao/) | `role_id` | `BAO_ROLE_ID` → `VAULT_ROLE_ID` | 0.17+ | | [`openbao`](/providers/openbao/) | `secret_id` | `BAO_SECRET_ID` → `VAULT_SECRET_ID` | 0.17+ | | [`openbao`](/providers/openbao/) | `token` | `BAO_TOKEN` → `VAULT_TOKEN` | 0.17+ | | [`passbolt`](/providers/passbolt/) | `private_key` | `SECRETSPEC_PASSBOLT_PRIVATE_KEY` | 0.19+ | | [`passbolt`](/providers/passbolt/) | `passphrase` | `SECRETSPEC_PASSBOLT_PASSPHRASE` | 0.19+ | | [`scaleway`](/providers/scaleway/) | `secret_key` | `SCW_SECRET_KEY` | 0.17+ | | [`sops`](/providers/sops/) | `age_key` | `SOPS_AGE_KEY` | 0.17+ | | [`sops`](/providers/sops/) | `aws_secret_access_key` | `AWS_SECRET_ACCESS_KEY` | 0.17+ | | [`sops`](/providers/sops/) | `azure_client_secret` | `AZURE_CLIENT_SECRET` | 0.17+ | | [`sops`](/providers/sops/) | `hc_vault_token` | `VAULT_TOKEN` | 0.17+ | | [`sops`](/providers/sops/) | `huawei_sdk_ak` | `HUAWEICLOUD_SDK_AK` | 0.17+ | | [`sops`](/providers/sops/) | `huawei_sdk_sk` | `HUAWEICLOUD_SDK_SK` | 0.17+ | | [`sops`](/providers/sops/) | `google_oauth_access_token` | `GOOGLE_OAUTH_ACCESS_TOKEN` | 0.17+ | | [`vault`](/providers/vault/) | `role_id` | `VAULT_ROLE_ID` | 0.15+ | | [`vault`](/providers/vault/) | `secret_id` | `VAULT_SECRET_ID` | 0.15+ | | [`vault`](/providers/vault/) | `token` | `VAULT_TOKEN` | 0.15+ | See [Provider credentials](/concepts/providers/#provider-credentials) for credential source addresses, storage commands, one-hop chaining, and runtime handling rules. # Providers Reference > Complete reference for SecretSpec storage providers and their URI configurations SecretSpec supports multiple storage backends for secrets. Each provider has its own URI format and configuration options. This page is a compact URI reference. For installation, authentication, copyable project configuration, storage behavior, and CI/CD guidance, follow the link for the individual provider. For the semantic authentication names accepted by those providers and their environment fallbacks, see the [provider credentials reference](/reference/provider-credentials/). ## Dotenv Provider [Section titled “Dotenv Provider”](#dotenv-provider) **URI**: `dotenv://[path]` - Stores secrets in `.env` files ```text dotenv:// # Uses default .env dotenv:///config/.env # Custom path dotenv://config/.env # Relative path dotenv://~/.config/app/.env # Home-relative path (0.18+) ``` **Features**: Read/write, profiles, human-readable, no encryption ## File Provider [Section titled “File Provider”](#file-provider-019) **New in version 0.19** **URI**: `file:ROOT` - Stores one plaintext UTF-8 file per secret beneath an explicitly configured local directory `ROOT` is required. The bare `file` provider name is rejected. ```text file:./.secrets # Relative to secretspec.toml file:///run/secrets # Absolute directory ``` **Features**: Read/write/delete, project and profile isolation, exact UTF-8 text, atomic writes, nested relative `ref.item` paths **Storage**: `ROOT/{project}/{profile}/{key}` by convention. A `ref.item` replaces the convention path with a relative path beneath `ROOT`. **Security**: No encryption. New files use mode `0600` on Unix; traversal and symbolic links inside the configured store are rejected. ## Environment Provider [Section titled “Environment Provider”](#environment-provider) **URI**: `env://` - Read-only access to system environment variables ```text env:// # Current process environment ``` **Features**: Read-only, no setup required, no persistence ## Null Provider [Section titled “Null Provider”](#null-provider-019) **New in version 0.19** **URI**: `null://` - Always reports a missing value so the declaration’s committed `default`, configured generator, or `prompt = true` run input (0.19+) supplies the value ```text null:// # No configuration or storage ``` **Features**: No I/O, no authentication, no persistence, ordinary writes rejected; generated and prompted values are returned only for the current resolution or child invocation **Use case**: Non-sensitive configuration from the version-controlled manifest, or ephemeral generated/operator-supplied values that should be fresh for every resolution or run ## systemd Credential Provider [Section titled “systemd Credential Provider”](#systemd-credential-provider-017) **New in version 0.17** **URI**: `systemd-credential://` - Reads credentials passed to the current service by systemd ```text systemd-credential:// # $CREDENTIALS_DIRECTORY ``` **Features**: Read-only, flat credential names, immutable service-lifetime values, provider-credential source support **Prerequisites**: A process started by systemd with `LoadCredential=`, `LoadCredentialEncrypted=`, `SetCredential=`, or `SetCredentialEncrypted=` **Storage**: One runtime file per credential under `$CREDENTIALS_DIRECTORY`; convention addresses use the SecretSpec key as the filename, and `ref.item` selects a different credential name ## Gopass Provider [Section titled “Gopass Provider”](#gopass-provider) Available starting with SecretSpec 0.15. **URI**: `gopass://[host][path]` - Uses `gopass`, a multi-user and multi-store abstraction layer over `pass`, with GPG encryption ```text gopass:// # Default folder prefix gopass://secretspec/shared/{profile}/{key} # Custom folder prefix with placeholders ``` **Features**: Read/write, GPG encryption, git-backed sync, profiles, local storage **Prerequisites**: `gopass` CLI, initialized password store **Storage**: Path `secretspec/{project}/{profile}/{key}` by default; the URI host and path override the folder prefix and support `{project}`, `{profile}`, and `{key}` placeholders Gopass entries store a single line; multiline secrets are truncated to their first line when read. ## Keyring Provider [Section titled “Keyring Provider”](#keyring-provider) **URI**: `keyring://` - Uses system keychain/keyring for secure storage ```text keyring:// # System default keychain ``` **Features**: Read/write, secure encryption, profiles, cross-platform **Storage**: Service `secretspec/{project}/{profile}/{key}`, with the current operating-system username as the account ## KeePass KDBX Provider [Section titled “KeePass KDBX Provider”](#keepass-kdbx-provider-017) **New in version 0.17** **URI**: `kdbx:PATH[?keyfile=PATH][&prefix=TEMPLATE]` - Stores secrets in a KeePass-compatible encrypted database ```text kdbx:./secrets.kdbx kdbx:/var/lib/myapp/secrets.kdbx kdbx:./secrets.kdbx?keyfile=./secrets.key kdbx:./shared.kdbx?prefix=teams/{project}/{profile}/{key} ``` **Features**: KDBX 3 read, KDBX 4 read/write, password and key-file authentication, standard and custom entry fields, profiles **Prerequisites**: Master password, key file, or both; build with `--features kdbx` (0.17+) **Authentication**: [`password` provider credential](/providers/kdbx/#provider-credentials) from a bootstrap provider (recommended), or the discouraged `SECRETSPEC_KDBX_PASSWORD` fallback; optional `?keyfile=PATH` **Storage**: Entry path `secretspec/{project}/{profile}/{key}`, field `Password` by default. A secret `ref` uses `item` for the complete group path and entry title, and optional `field` for a standard or custom field. ## LastPass Provider [Section titled “LastPass Provider”](#lastpass-provider) **URI**: `lastpass://[item_template]` - Integrates with LastPass via `lpass` CLI ```text lastpass:// # Default layout lastpass://Work/SecretSpec/{project}/{profile}/{key} # Custom item template ``` **Features**: Read/write, cloud sync, profiles via folders, auto-sync **Prerequisites**: `lpass` CLI, authenticated with `lpass login` **Storage**: Item name `secretspec/{project}/{profile}/{key}` by default. A URI item template replaces the default and supports `{project}`, `{profile}`, and `{key}` placeholders. ## Dashlane Provider [Section titled “Dashlane Provider”](#dashlane-provider-018) **New in version 0.18** **URI**: `dashlane://[item_type]` - Integrates with Dashlane via the `dcli` CLI ```text dashlane:// # Search secrets, then logins, then notes dashlane://note # Secure notes only dashlane://secret # Dashlane Secrets only (Business plans) dashlane://password # Logins only ``` **Features**: Read-only, reads a locally synced vault, profiles via item titles **Prerequisites**: `dcli` CLI, device registered with `dcli sync`, or `DASHLANE_SERVICE_DEVICE_KEYS` for a non-interactive device **Storage**: Item titled `secretspec/{project}/{profile}/{key}`. The value is the item’s default field: `content` for a secret or note, `password` for a login. `dcli` cannot create or edit vault items, so `secretspec set` fails; author items in a Dashlane app and run `dcli sync`. With `DASHLANE_SERVICE_DEVICE_KEYS` set, `dcli` runs against a private, owner-only state directory per credential, since it otherwise prefers an already-registered device and reads that identity’s vault instead. That state is separate from your own, so `dcli configure disable-auto-sync true` does not apply to it and those reads sync hourly. ## 1Password Provider [Section titled “1Password Provider”](#1password-provider) **URI**: `onepassword://[account@]vault` or `onepassword+token://vault` ```text onepassword://MyVault # Default account onepassword://work@CompanyVault # Specific account onepassword+token://SecureVault # Service account ``` The `onepassword+token://` scheme selects service account authentication; the token comes from the `service_account_token` provider credential or `OP_SERVICE_ACCOUNT_TOKEN`. Putting the token in the URI (`onepassword+token://token@vault`) is rejected from 0.19 on, since a URI ends up in committed manifests, shell history, and CI logs. **Features**: Read/write, cloud sync, profiles via vaults, service accounts **Prerequisites**: `op` CLI, authenticated through desktop app integration, a service account token, or a legacy `op signin` shell session **Storage**: Secure Note named `secretspec/{project}/{profile}/{key}`, with tags `automated` and `{project}` The URI names a vault only; item paths on the URI are rejected. To read and write an existing item’s field in place, name it with the `ref` field (`SECRET = { description = "…", ref = { item = "…", field = "…" } }`); see [Secret References](/reference/configuration/#secret-references). ## Keeper Secrets Manager Provider [Section titled “Keeper Secrets Manager Provider”](#keeper-secrets-manager-provider-018) **New in version 0.18** **URI**: `keeper://FOLDER_UID[?config_file=PATH]` - Stores records in Keeper Secrets Manager through Keeper’s official Rust SDK ```text keeper://SHARED_FOLDER_UID keeper://SHARED_FOLDER_UID?config_file=.keeper/client-config.json ``` **Features**: Read/write/delete, end-to-end encryption, profile-aware record titles, standard and custom field references, batched retrieval **Prerequisites**: A Keeper Secrets Manager application with access to the selected folder; build with `--features keeper` (0.18+) **Authentication**: [`config` or `token` provider credentials](/providers/keeper/#provider-credentials), with `KSM_CONFIG` and `KSM_TOKEN` fallbacks; alternatively a bound `config_file`. **Storage**: Login record titled `secretspec/{project}/{profile}/{key}`, field `password`. A `ref` selects an existing record by UID or exact title and an optional standard/custom `field`. ## Pass Provider [Section titled “Pass Provider”](#pass-provider) **URI**: `pass://` - Uses Unix password manager with GPG encryption ```text pass:// # Default password store ``` **Features**: Read/write, GPG encryption, profiles, local storage **Prerequisites**: `pass` CLI, initialized with `pass init ` **Storage**: Path `secretspec/{project}/{profile}/{key}` ## Proton Pass Provider [Section titled “Proton Pass Provider”](#proton-pass-provider) **URI**: `protonpass://[vault[/title-template]]` - Stores secrets in Proton Pass via the official `pass-cli` ```text protonpass:// # Default vault ("secretspec") protonpass://Work # Specific vault protonpass://Work/{project}/{profile}/{key} # Custom vault and title template ``` **Features**: Read/write, end-to-end encryption, cloud sync, vault organisation, PAT-based CI auth **Prerequisites**: `pass-cli`, authenticated with `pass-cli login` (or `pass-cli login --pat $PAT` for CI) **Storage**: Note item titled `{project}/{profile}/{key}` inside the configured vault `pass-cli` ships backward incompatible changes in patch releases without advance notice, so a CLI upgrade can break secret resolution on its own. `pass-cli` 2.2.4 removed `pass-cli test`, which SecretSpec 0.18.0 and earlier use to check the session before every read and write; those releases need `pass-cli` 2.2.3 or earlier, while SecretSpec 0.19+ tries `pass-cli info` and falls back to `pass-cli test`, working with either. Pin a tested build and select it with `SECRETSPEC_PROTONPASS_CLI_PATH`. See [`pass-cli` compatibility](/providers/protonpass/#pass-cli-compatibility). ## Passbolt provider [Section titled “Passbolt provider”](#passbolt-provider-019) **New in version 0.19** **Availability**: Added in SecretSpec 0.19. **URI**: `passbolt://[?server=URL][&folder=ID][&template=PATTERN]` - Reads and writes resources in a self-hosted Passbolt server through `go-passbolt-cli` ```text passbolt:// # Default resource template passbolt://?server=https://pass.example.com # Select a server passbolt://?folder= # Scope lookup and creation passbolt://?template=teams/{project}/{profile}/{key} # Replace the convention template ``` **Features**: Read/write, self-hosted, provider credentials, `init --from`, and `ref` by resource UUID or exact name (standard fields: `password`/`username`/`uri`/`description`; custom resource-type fields are not addressable) **Prerequisites**: `go-passbolt-cli`, an OpenPGP private key, and its passphrase. Use the `private_key` and `passphrase` provider credentials, their `SECRETSPEC_PASSBOLT_*` environment fallbacks, or the CLI configuration. For MFA, the CLI supports TOTP only. **Storage**: Resource `secretspec/{project}/{profile}/{key}`, field `password`. Missing resources named through `ref` are never created. **Write limitation**: `go-passbolt-cli` accepts created/updated values only as flags, so a value being written is visible in the child process argv until it exits. See the [Passbolt provider security notes](/providers/passbolt/#security-considerations-and-limitations). ## Fly.io secrets provider [Section titled “Fly.io secrets provider”](#flyio-secrets-provider-020) **New in version 0.20** **Availability**: Added in SecretSpec 0.20. **URI**: `fly://APP[?stage=true][&detach=true]` - Publishes application secrets through `flyctl secrets` ```text fly://my-app # Update Machines and monitor the rollout fly://my-app?stage=true # Register changes without deploying them fly://my-app?detach=true # Start the rollout without monitoring it ``` **Features (0.20+)**: Write, delete, provider credentials, and name-only discovery through `init --from`; secret values are sent to `flyctl` over stdin instead of process arguments **Prerequisites (0.20+)**: `flyctl`, an authenticated login or an `access_token` provider credential (`FLY_API_TOKEN` and `FLY_ACCESS_TOKEN` are fallbacks), and permission to manage the app named in the URI **Storage (0.20+)**: Fly app secret `{key}`. The app URI, rather than the SecretSpec project or profile name, supplies isolation. **Read limitation**: Fly.io exposes secret names and digests but never plaintext values. `get`, `check`, `run`, fallback reads, generation-on-miss, and prompting-on-miss cannot use this write-only provider. See the [Fly.io provider guide](/providers/fly/). **Write limitation (0.20+)**: `flyctl` trims values read from stdin. SecretSpec rejects leading or trailing whitespace rather than silently publishing a different value. ## Cloudflare Secrets Store provider [Section titled “Cloudflare Secrets Store provider”](#cloudflare-secrets-store-provider-020) **New in version 0.20** **Availability**: Added in SecretSpec 0.20 and included in default builds; use the `cloudflare` feature for a custom minimal build. **URI**: `cloudflare://STORE_ID[?account_id=ACCOUNT_ID][&scopes=LIST][&auth=MODE][&wrangler_profile=NAME]` * Publishes account-level secrets through Cloudflare’s REST API ```text cloudflare://STORE_ID?account_id=ACCOUNT_ID cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=token cloudflare://STORE_ID?account_id=ACCOUNT_ID&auth=wrangler&wrangler_profile=production cloudflare://STORE_ID?account_id=ACCOUNT_ID&scopes=workers,containers ``` **Features (0.20+)**: Write, replace, delete, provider credentials, and name-only discovery through `init --from`; values are sent only in HTTPS request bodies **Prerequisites (0.20+)**: A Cloudflare account and Secrets Store, account **Secrets Store Write** permission, the account and store IDs, and either an `api_token` provider credential, `CLOUDFLARE_API_TOKEN`, or credentials from `wrangler auth token --json`. `CLOUDFLARE_ACCOUNT_ID` is the fallback when the URI omits `account_id`. **Storage (0.20+)**: Account secret `{key}` in the selected store. The store URI, rather than the SecretSpec project or profile name, supplies isolation. New and replaced entries receive the configured scopes, defaulting to `workers`. **Read limitation (0.20+)**: Cloudflare’s management API exposes metadata but never plaintext secret values. `get`, `check`, `run`, fallback reads, generation-on-miss, and prompting-on-miss cannot use this write-only provider. Plaintext is available only inside a bound Cloudflare service. See the [Cloudflare provider guide](/providers/cloudflare/). ## Google Cloud Secret Manager Provider [Section titled “Google Cloud Secret Manager Provider”](#google-cloud-secret-manager-provider) **URI**: `gcsm://PROJECT_ID` - Stores secrets in Google Cloud Secret Manager ```text gcsm://my-gcp-project # GCP project ID ``` **Features**: Read/write, cloud sync, profiles, service account support **Prerequisites**: `gcloud` CLI, authenticated, Secret Manager API enabled, build with `--features gcsm` **Storage (0.20+)**: Secret name `secretspec2--{project}--{profile}--{key}` with validated, non-overlapping `--` boundaries. Releases through 0.19 used `secretspec-{project}-{profile}-{key}`. When the new id holds no value, reads fall back to the 0.19 id and warn; the fallback writes nothing, so no new permissions are needed. Writes always use the new id, so `secretspec set` is what moves a secret, and the 0.19 secret is left in place. Names accepted through 0.19 that the new layout cannot represent, such as a project containing `--`, keep reading their 0.19 secret and must be renamed before they can be written. Explicit `ref` addresses are unaffected. ## AWS Secrets Manager Provider [Section titled “AWS Secrets Manager Provider”](#aws-secrets-manager-provider) **URI**: `awssm://[profile@]REGION` - Stores secrets in AWS Secrets Manager ```text awssm://us-east-1 # Specific AWS region awssm://production@us-east-1 # Specific AWS profile and region awssm:// # SDK default region and credentials ``` **Features**: Read/write, cloud sync, profiles, IAM/SSO authentication **Prerequisites**: AWS credentials configured, build with `--features awssm` **Storage**: Secret name `secretspec/{project}/{profile}/{key}` ## AWS Systems Manager Parameter Store Provider [Section titled “AWS Systems Manager Parameter Store Provider”](#aws-systems-manager-parameter-store-provider-018) **New in version 0.18** **URI (0.18+)**: `awsps://[profile@]REGION[?prefix=PATH&template=TEMPLATE&kms_key_id=KEY&tier=TIER]` * Stores secrets as encrypted AWS Systems Manager Parameter Store values; `prefix` and `template` are mutually exclusive. ```text awsps://us-east-1 # Specific AWS region awsps://production@us-east-1 # AWS profile and region awsps://us-east-1?prefix=/team # Additional hierarchy awsps://us-east-1?template=/{profile}/{project}/{key} # Replace the hierarchy awsps://us-east-1?kms_key_id=alias/key&tier=advanced awsps:// # SDK defaults ``` **Features (0.18+)**: Read/write, `SecureString` encryption, cloud sync, profiles, IAM/SSO authentication, batched reads, version- or label-pinned read-only refs, writable unversioned parameter-name refs; ARN refs are read-only **Prerequisites (0.18+)**: AWS credentials configured, build with `--features awsps` **Storage (0.18+)**: Parameter `[/prefix]/secretspec/{project}/{profile}/{key}`. `template` replaces the complete layout and must end in `/{key}`; `kms_key_id` selects a customer-managed key, while `tier` accepts `standard`, `advanced`, or `intelligent-tiering` **Discovery (0.18+)**: Bounded declaration discovery through `init --from` ## Scaleway Secret Manager Provider [Section titled “Scaleway Secret Manager Provider”](#scaleway-secret-manager-provider-017) **New in version 0.17** **URI**: `scaleway://[REGION][?project_id=UUID&path=/folder]` - Stores secrets in Scaleway Secret Manager ```text scaleway://fr-par # Region, project from SCW_DEFAULT_PROJECT_ID scaleway://nl-ams?project_id=PROJECT_UUID # Region and project scaleway://fr-par?project_id=PROJECT_UUID&path=/team # Nest under a folder scaleway:// # Region from SCW_DEFAULT_REGION, else fr-par ``` **Features**: Read/write, cloud sync, profiles via folders, version-pinned refs, JSON-key refs **Prerequisites**: Scaleway API secret key (`secret_key` credential or `SCW_SECRET_KEY`), build with `--features scaleway` **Storage**: Folder `[{base}/]secretspec/{project}/{profile}`, secret name `{key}` ## Vault Provider [Section titled “Vault Provider”](#vault-provider) **URI**: `vault://[namespace@]host[:port][/mount][?options]` - Stores secrets in HashiCorp Vault’s KV engine ```text vault://vault.example.com:8200/secret # KV v2 at "secret" mount vault://vault.example.com:8200 # Default "secret" mount vault://ns1@vault.example.com:8200/secret # With namespace vault://vault.example.com:8200/secret?auth=approle # SecretSpec 0.17+ vault://vault.example.com:8200/secret?auth=jwt&role=ci # SecretSpec 0.18+ vault://vault.example.com:8200/secret?auth=approle&auth_mount=platform-approle # SecretSpec 0.18+, with default_role configured on the JWT auth mount vault://vault.example.com:8200/secret?auth=jwt vault://127.0.0.1:8200/secret?kv=1 # KV v1 engine vault://127.0.0.1:8200/secret?tls=false # Disable TLS (dev mode) ``` **Features**: Read/write, KV v1 and v2, namespaces; token and AppRole authentication, including AppRoles without SecretID binding (0.18+); JWT/OIDC authentication (0.17+); custom AppRole/JWT mounts and server-default JWT roles (0.18+) **Prerequisites**: Vault server, authentication credentials, build with `--features vault` **Storage**: KV path `secretspec/{project}/{profile}/{key}` with a `value` field ## OpenBao Provider [Section titled “OpenBao Provider”](#openbao-provider-017) **New in version 0.17** **URI**: `openbao://[namespace@]host[:port][/mount][?options]` - Stores secrets in OpenBao’s KV engine ```text openbao://bao.example.com:8200/secret openbao://team-a@bao.example.com:8200/secret openbao://bao.example.com:8200/secret?auth=approle openbao://bao.example.com:8200/secret?auth=jwt&role=ci # SecretSpec 0.18+ openbao://bao.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=ci # SecretSpec 0.18+, with default_role configured on the JWT auth mount openbao://bao.example.com:8200/secret?auth=jwt openbao://127.0.0.1:8200/secret?kv=1&tls=false ``` **Features**: Read/write, KV v1 and v2, namespaces; token, AppRole, and JWT/OIDC authentication; AppRoles without SecretID binding, custom AppRole/JWT mounts, and server-default JWT roles (0.18+); documented OpenBao CLI variables plus SecretSpec-defined `BAO_*` AppRole/JWT inputs, all with `VAULT_*` compatibility fallbacks **Prerequisites**: OpenBao server, authentication credentials, build with `--features openbao` (0.17+) **Storage**: KV path `secretspec/{project}/{profile}/{key}` with a `value` field ## Bitwarden Password Manager Provider [Section titled “Bitwarden Password Manager Provider”](#bitwarden-password-manager-provider-018) **New in version 0.18** **URI**: `bw://[COLLECTION]` - Stores secrets in a Bitwarden Password Manager vault via the `bw` CLI ```text bw:// # Personal vault bw://dev-secrets # Collection, by name or ID bw://myorg@dev-secrets # Organization and collection bw://?server=https://vault.company.com # Expected self-hosted server (guard) bw://?type=login&field=username # Default item type and field bw://?folder=team/{project}/{profile} # Convention title prefix (0.20+) ``` Organizations and collections may be named or given as IDs; SecretSpec resolves a name to the ID the CLI requires, matching case-insensitively. The organization scopes and validates the collection rather than filtering alongside it: it selects which `dev-secrets` is meant when more than one exists, and must match the collection’s actual organization. Naming it is optional when the collection name is unambiguous. Addresses that resolve to nothing fail with the organizations or collections that do exist. Item names match the same way — **in full and case-insensitively** (0.18+), so `API_KEY` never resolves `API_KEY_OLD`. Names are not unique in Bitwarden, and a name matching several items is refused with their ids rather than resolved to an arbitrary one; address a single item by using its id as the `item`. `?type=` narrows both reads and writes to that item type, keeping a Card and a same-named Login separately addressable. An unsupported `?type=`, or an unknown query parameter, is rejected when the address is parsed rather than ignored. SecretSpec 0.20+ convention items use the title `secretspec/{project}/{profile}/{key}`. `?folder=` replaces the prefix before the key; it is an item-title namespace, not a Bitwarden folder. Explicit `ref.item` values remain complete, unprefixed item titles. Releases through 0.19 wrote bare convention titles, which must be renamed to the 0.20 layout or kept with an explicit `ref = { item = "OLD_TITLE" }`; there is no automatic bare-name fallback because a bare item carries no project/profile ownership. `?server=` does not configure the CLI. The `bw` CLI takes its server only from `bw config server`, which must be run while logged out, so self-hosted users configure the CLI themselves and SecretSpec verifies the setting matches before each operation. See the [provider guide](/providers/bw/#self-hosted-servers). **Features**: Read/write, all vault item types (logins, cards, identities, SSH keys, secure notes), organization/collection addressing by name or ID, field selection, `ref = { item, field }` mapping in `secretspec.toml`, declaration discovery through `init --from` (0.18+) **Prerequisites**: Bitwarden CLI (`bw`), signed in and unlocked (`BW_SESSION` env var), self-hosted servers set with `bw config server` before login, build with `--features bw` **Storage**: One vault item per secret; convention title `secretspec/{project}/{profile}/{key}` (0.20+, customizable with `?folder=`), with per-type default fields unless `?field=` or a `ref` mapping selects one ## Bitwarden Secrets Manager Provider [Section titled “Bitwarden Secrets Manager Provider”](#bitwarden-secrets-manager-provider) **URI**: `bws://[SERVER_BASE@]PROJECT_UUID` - Stores secrets in Bitwarden Secrets Manager ```text bws://a9230ec4-5507-4870-b8b5-b3f500587e4c # US cloud (default) bws://vault.bitwarden.eu@a9230ec4-5507-4870-b8b5-b3f500587e4c # EU cloud bws://bw.example.com@a9230ec4-5507-4870-b8b5-b3f500587e4c # Self hosted ``` `SERVER_BASE` is the bare hostname of the Bitwarden instance. SecretSpec 0.17+ passes `https://SERVER_BASE` to `bws --server-url`; SecretSpec 0.16 and earlier derive the `https://SERVER_BASE/identity` and `https://SERVER_BASE/api` endpoints through the SDK. Omit it to use the `bitwarden.com` US cloud. **Features**: Read/write, cloud sync, project-scoped, end-to-end encryption **Prerequisites**: BWS subscription, machine account access token, build with `--features bws` **Storage**: Flat key names in the specified BWS project SecretSpec 0.17 and later require the official `bws` CLI 0.3.0 or later on `PATH` and invoke it for all reads and writes; set `SECRETSPEC_BWS_CLI_PATH` to use another executable path. The access token is supplied through the child process environment. Secret values passed to the CLI for creation or editing may briefly be visible to same-user process-inspection tools. ## Azure Key Vault Provider [Section titled “Azure Key Vault Provider”](#azure-key-vault-provider) **URI**: `akv://VAULT_NAME[?auth=env|cli|managed_identity|workload_identity][&suffix=DNS_SUFFIX]` - Stores secrets in Azure Key Vault ```text akv://myvault # Service principal env vars, falling back to `az login` akv://myvault?auth=managed_identity # VM / App Service / AKS system-assigned managed identity akv://myvault?auth=workload_identity # AKS workload identity federation akv://myvault.vault.azure.cn # Sovereign cloud (full DNS name) akv://myvault?suffix=vault.azure.cn # Sovereign cloud (explicit suffix, bare vault name) ``` **Features**: Read/write, cloud sync, profiles, service principal/managed identity/workload identity auth, version-pinned refs (0.20+) **Prerequisites**: An Azure Key Vault instance, authenticated via one of the methods above, build with `--features akv` **Storage**: Secret name `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}` (lowercase, unpadded Base32 preserves case and punctuation distinctions within Azure’s case-insensitive secret-name namespace) ## Azure App Configuration Provider [Section titled “Azure App Configuration Provider”](#azure-app-configuration-provider-020) **New in version 0.20** **URI**: `aac://STORE[?auth=METHOD][&label=LABEL][&prefix=PREFIX][&tag=NAME=VALUE]...` * Reads and manages Azure App Configuration key-values and resolves canonical Azure Key Vault references ```bash aac://payments-production aac://shared?label=production&prefix=payments: aac://shared?tag=app=payments&tag=stage=production aac://shared?auth=connection_string&key_vault_auth=managed_identity ``` **Features (0.20+)**: Read/write/delete, project and profile namespacing, declaration discovery, exact label and tag selection, sovereign-cloud endpoint configuration, Entra or connection-string authentication, and Key Vault reference resolution **Prerequisites (0.20+)**: An Azure App Configuration store and matching data-plane permissions. Official and default builds include AAC; custom minimal builds use `--features aac`. Key Vault references also require an Entra identity with secret-read access. **Authentication (0.20+)**: `env`, `cli`, `managed_identity`, `workload_identity`, or `connection_string`. Prefer Entra authentication so workloads use Azure RBAC without distributing App Configuration access keys; reserve connection strings for environments where Entra is unavailable. See the [provider guide](/providers/aac/#authentication) for App Configuration and Key Vault identity separation. **Storage (0.20+)**: `{prefix}secretspec:{project}:{profile}:{key}` under one exact label; omission selects the null label ## Infisical Provider [Section titled “Infisical Provider”](#infisical-provider) Available since SecretSpec 0.16. **URI**: `infisical://[HOST]/PROJECT_ID[?env=SLUG][&path=/PREFIX][&tls=false]` - Stores secrets in Infisical ```text infisical://app.infisical.com/7e2f1a4c-... # Infisical Cloud (US) infisical://eu.infisical.com/7e2f1a4c-... # Infisical Cloud (EU) infisical://vault.example.com/7e2f1a4c-...?env=prod # Read every profile from one environment infisical://localhost:8080/7e2f1a4c-...?tls=false # Self-hosted over plain HTTP ``` The project is Infisical’s project **UUID** (Project Settings → Project ID); its API does not accept the project slug. Without a host, the provider reads `INFISICAL_DOMAIN`, then Infisical’s legacy `INFISICAL_API_URL`, then defaults to Infisical Cloud. **Features**: Read/write, cloud sync, profiles, machine-identity (Universal Auth) or token auth, secret references, version-pinned refs **Prerequisites**: An Infisical project, a machine identity with access to it, build with `--features infisical` **Authentication**: `INFISICAL_CLIENT_ID` + `INFISICAL_CLIENT_SECRET` (Universal Auth), or a ready-made `INFISICAL_TOKEN`. Service tokens are not supported; Infisical deprecated them in favour of machine identities. **Storage**: Secret `{key}` in folder `/secretspec/{project}/{profile}`, in the environment named by the profile (or by `?env=`). Keys are stored verbatim. By default the SecretSpec profile names the Infisical environment, so a `production` profile reads the `production` environment. This covers refs as well as convention naming (0.20+). Projects whose environments do not correspond to profiles pin one with `?env=`; the profile still names the folder, so profiles never share a secret. Infisical uses the same 404 for a missing secret, folder, environment, or project. In SecretSpec 0.20+, an all-missing read checks the environment root once and reports a missing environment or project, including whether the profile or `?env=` selected the environment. Ordinary missing secrets and folders remain unset so provider fallback continues. Values are read with Infisical’s secret references expanded, matching its own CLI, so a value of `postgres://${DB_USER}@host` arrives resolved. ## EJSON Provider [Section titled “EJSON Provider”](#ejson-provider-020) **New in version 0.20** **URI**: `ejson:PATH` - Reads string values from one EJSON encrypted file ```text ejson:secrets.ejson ejson:config/secrets.production.ejson ejson:///var/run/application/secrets.ejson ``` **Features (0.20+)**: Read-only, RFC 6901 JSON Pointer references, project/profile convention pointers, exact provider-credential key sources, and one decryption per batch **Prerequisites (0.20+)**: EJSON 1.1.0 or later, an encrypted EJSON file, and its matching private key; build with `--features ejson` **Authentication (0.20+)**: The `private_key` provider credential. It has no environment or URI fallback; source it from an exact provider reference such as a Google Cloud Secret Manager secret and version. **Storage (0.20+)**: Convention reads use `/{project}/{profile}/{key}` in the decrypted JSON document. A native `ref.item` is any RFC 6901 JSON Pointer selecting a string. The provider decrypts the complete EJSON document in memory through the current official CLI, then returns only requested values. Encrypted input and decrypted output are each limited to 16 MiB. It writes no decrypted value to a file itself. On Unix, SecretSpec opens the configured path with no-follow and nonblocking semantics, copies ciphertext into an anonymous inherited descriptor, and stops the CLI process group after a 30-second timeout. Other platforms use a private named ciphertext snapshot. Parent-directory resolution still requires a trusted path. The provider rejects writes, non-string selected values, URI credentials, ports, query options, fragments, and native coordinates other than `item`. EJSON properties beginning with `_` are plaintext metadata and must not contain secrets. See the [EJSON provider guide](/providers/ejson/) for exact Google Cloud Secret Manager credential configuration and security limitations. ## age Provider [Section titled “age Provider”](#age-provider-017) **New in version 0.17** **URI**: `age://PATH[?identity=FILE][&recipients-file=FILE][&armor=false]` - Stores secrets in a single age-encrypted file committed alongside code ```text age://secrets.age # Encrypt to your own identity age://secrets.age?identity=/home/alice/.config/age/plugin-identity.txt age://secrets.age?recipients-file=secrets.age.recipients # Share with a roster ``` **Features**: Read/write, delete (0.20+), committed-file storage, X25519 and SSH keys, native tagged recipients, and non-interactive `age-plugin-*` recipients and identities **Prerequisites**: An age identity; hybrid ML-KEM-768 + X25519 keys from `age-keygen -pq` are recommended for new setups and currently require the non-interactive `age-plugin-pq` compatibility plugin. Build with `--features age`. **Authentication**: The `identity` credential, `AGE_IDENTITY`, or `?identity=`; recipients from `?recipients-file=` or derived from the identity **Storage**: One `KEY=value` entry per secret inside the encrypted blob at PATH ## SOPS Provider [Section titled “SOPS Provider”](#sops-provider-017) **New in version 0.17** **URI**: `sops://PATH[?format=yaml|json|dotenv|ini]` - Stores secrets in a SOPS-encrypted file or a templated set of files ```text sops://secrets.enc.yaml sops://secrets/{project}/{profile}.enc.json sops://secrets/{project}/.env.{profile}.enc?format=dotenv ``` **Features**: Read/write, YAML, JSON, dotenv, and INI files, SOPS key-service support, and profile-aware templated paths **Prerequisites**: The `sops` CLI and the required SOPS key configuration; build with `--features sops` (0.17+) **Authentication**: SOPS environment variables or the [supported provider credentials](/providers/sops/#provider-credentials) **Storage**: Single-file YAML and JSON convention writes use `[project][profile][key]`; single-file INI uses `[profile][key]`, and dotenv is flat. Templated paths use a root key (or `[DEFAULT][key]` for INI) in one file per project/profile. A single-file provider supports `ref = { item = "..." }` as a root key (or a key in `[DEFAULT]` for INI); extra coordinates and refs through templated paths are rejected. ## Kubernetes Provider [Section titled “Kubernetes Provider”](#kubernetes-provider-020) **New in version 0.20** **URI**: `k8s+KIND://NAME[@NAMESPACE]` - Stores secrets in a Kubernetes ConfigMap or Secret ```text k8s+configmap://db-config@db-postgres k8s+configmap://db-config k8s+secret://db-credentials@db-postgres ``` **Features**: Read/write Kubernetes ConfigMaps and Secrets **Prerequisites**: A Kubernetes configuration in `$KUBECONFIG` or `$HOME/.kube/config`; build with `--features kubernetes` (0.20+) **Authentication**: Configured in Kubernetes configuration **Storage**: `secretspec--{project}--{profile}--{key}` key under `.data` in the Kubernetes object ## Provider Selection [Section titled “Provider Selection”](#provider-selection) ### Command Line [Section titled “Command Line”](#command-line) ```bash # Simple provider names $ secretspec get API_KEY --provider keyring $ secretspec get API_KEY --provider dotenv $ secretspec get API_KEY --provider env # URIs with configuration $ secretspec get API_KEY --provider dotenv:/path/to/.env $ secretspec get API_KEY --provider onepassword://vault $ secretspec get API_KEY --provider "onepassword://account@vault" ``` ### Environment Variables [Section titled “Environment Variables”](#environment-variables) ```bash $ export SECRETSPEC_PROVIDER=keyring $ export SECRETSPEC_PROVIDER="dotenv:///config/.env" ``` ## Security Considerations [Section titled “Security Considerations”](#security-considerations) | Provider | Encryption | Storage Location | Network Access | | -------------------------------- | ---------------------------------- | ---------------------------------------------------------- | --------------------------------- | | Dotenv | ❌ Plain text | Local filesystem | ❌ No | | File (0.19+) | ❌ Plain text | Local filesystem | ❌ No | | Environment | ❌ Plain text | Process memory | ❌ No | | Null (0.19+) | N/A — no stored value | None | ❌ No | | systemd Credential (0.17+) | Depends on unit source | systemd-managed runtime memory | ❌ No | | Keyring | ✅ System encryption | System keychain | ❌ No | | KeePass KDBX (0.17+) | ✅ KDBX encryption | Local filesystem | ❌ No | | Pass | ✅ GPG encryption | Local filesystem | ❌ No | | Gopass | ✅ GPG encryption | Local filesystem | ❌ No | | Proton Pass | ✅ End-to-end | Cloud (Proton) | ✅ Yes | | Passbolt (0.19+) | ✅ End-to-end | Self-hosted (Passbolt server) | ✅ Yes | | Fly.io secrets (0.20+) | ✅ Fly.io-managed | Cloud (Fly.io app vault) | ✅ Yes | | Cloudflare Secrets Store (0.20+) | ✅ Cloudflare-managed | Cloud (account-level store) | ✅ Yes | | LastPass | ✅ End-to-end | Cloud (LastPass) | ✅ Yes | | Dashlane (0.18+) | ✅ End-to-end | Cloud (Dashlane), synced locally | Yes — `dcli` auto-syncs hourly | | 1Password | ✅ End-to-end | Cloud (1Password) | ✅ Yes | | Keeper (0.18+) | ✅ End-to-end | Cloud (Keeper) | ✅ Yes | | GCSM | ✅ Google-managed | Cloud (GCP) | ✅ Yes | | AWSSM | ✅ AWS KMS | Cloud (AWS) | ✅ Yes | | AWS Parameter Store (0.18+) | ✅ AWS KMS (`SecureString`) | Cloud (AWS) | ✅ Yes | | Scaleway (0.17+) | ✅ Scaleway-managed | Cloud (Scaleway) | ✅ Yes | | Vault | ✅ Vault encryption | Vault server | ✅ Yes | | OpenBao (0.17+) | ✅ OpenBao encryption | OpenBao server | ✅ Yes | | BW (0.18+) | ✅ End-to-end | Cloud (Bitwarden) or self-hosted | ✅ Yes | | BWS | ✅ End-to-end | Cloud (Bitwarden) | ✅ Yes | | AKV | ✅ Azure-managed | Cloud (Azure) | ✅ Yes | | Azure App Configuration (0.20+) | ✅ Azure-managed | Cloud (Azure) | ✅ Yes | | Infisical | ✅ Infisical-managed | Cloud (Infisical) or self-hosted | ✅ Yes | | EJSON (0.20+) | ✅ EJSON encryption | Local encrypted file; private key from configured provider | Depends on private-key provider | | age (0.17+) | ✅ age encryption | Local filesystem | ❌ No | | SOPS (0.17+) | ✅ Configured SOPS encryption | Local filesystem | Depends on configured key service | | Kubernetes (0.20+) | ❌ ConfigMap ✅ Secret if configured | Kubernetes server | ✅ Yes | # C# SDK > Resolve SecretSpec secrets from C# and .NET **Changed in version 0.16** The 0.15.0 NuGet package is an unsupported bootstrap artifact used to reserve the package ID; use version 0.16 or later for the API below. > **Native library name:** SecretSpec 0.20+ packages the embedded C ABI as `libsecretspec` (`libsecretspec.*`). The runtime loader still accepts the pre-0.20 `secretspec_ffi` filenames. The C# SDK (`Cachix.SecretSpec`) is a thin client over the same Rust resolver as the CLI. Every provider, fallback chain, profile, generator, reference, and `as_path` secret therefore works without C#-side resolution logic. ## Install [Section titled “Install”](#install-016) **New in version 0.16** ```bash $ dotnet add package Cachix.SecretSpec ``` The package targets .NET 8 and includes native resolvers for glibc and musl Linux x64/Arm64, macOS x64/Arm64, and Windows x64/Arm64. Windows assets statically include the C runtime. No separate SecretSpec CLI, native library, Visual C++ Redistributable, or system `libdbus` installation is needed. The managed client is safe to trim and supports NativeAOT publishing. A NativeAOT application still carries the matching SecretSpec native resolver beside its executable; `dotnet publish` selects and copies that runtime asset automatically. ```bash $ dotnet publish -c Release -r linux-x64 --self-contained \ -p:PublishAot=true ``` ## Quick start [Section titled “Quick start”](#quick-start) ``` using Cachix.SecretSpec; using var resolved = SecretSpec.Builder() .WithProvider("keyring://") .WithProfile("production") .WithReason("boot web app") .Load(); Console.WriteLine($"{resolved.Provider} {resolved.Profile}"); Console.WriteLine(resolved.Secrets["DATABASE_URL"].Get()); resolved.SetAsEnv(); ``` `Get()` returns the inline value, or the readable file path for an `as_path` secret. A missing required secret throws `MissingRequiredException`; its `Missing` property contains the secret names. Other failures throw `SecretSpecException`, whose `Kind` property is a stable error category. A one-shot form is also available: ``` using Cachix.SecretSpec; using var resolved = SecretSpec.Resolve( provider: "keyring://", profile: "production", reason: "boot web app"); ``` ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```csharp var builder = SecretSpec.Builder().WithCaller(new CallerContext { Name = "git", Version = "2.51.0", Operation = "credential_get", Resource = "github.com", }); ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `WithInlineSpec(spec, baseDir)` to resolve strict inline-spec v1 declarations from an object serialized by `System.Text.Json`. `baseDir` resolves relative provider paths, and an older native library reports a capability error. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `WithScope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available as `Resolved.Scope` and `ResolutionReport.Scope`: ``` using Cachix.SecretSpec; using var resolved = SecretSpec.Builder().WithScope("api").Load(); ``` ## ASP.NET Core [Section titled “ASP.NET Core”](#aspnet-core) Resolve and export secrets before creating the application builder, so normal environment-variable configuration sees them: ``` using Cachix.SecretSpec; using var secrets = SecretSpec.Builder() .WithProfile(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")) .WithReason("ASP.NET Core boot") .Load(); secrets.SetAsEnv(); var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Run(); ``` For longer-lived services, you can instead register `resolved` in dependency injection and read `ResolvedSecret` objects directly. Keep the result alive for as long as consumers need any `as_path` file. ## Value-free preflight [Section titled “Value-free preflight”](#value-free-preflight) `Report()` returns the inventory view exposed by `secretspec check --json`. It never carries values. Missing required secrets appear with `Status == "missing_required"` rather than throwing, so incomplete deployments can still be inspected. ``` using Cachix.SecretSpec; var report = SecretSpec.Builder() .WithProfile("production") .WithReason("deployment preflight") .Report(); foreach (var secret in report.Secrets) Console.WriteLine($"{secret.Name}: {secret.Status}"); ``` ## Typed access [Section titled “Typed access”](#typed-access) Generate an idiomatic C# model from the manifest schema: ```bash $ secretspec schema | \ quicktype -s schema --top-level AppSecrets --lang csharp -o AppSecrets.cs ``` Then deserialize the SDK’s flat field map: ``` using Cachix.SecretSpec; using var resolved = SecretSpec.Builder().Load(); var typed = AppSecrets.FromJson(resolved.FieldsJson()); Console.WriteLine(typed.DatabaseURL); ``` The schema models successful resolution: required, defaulted, and generated secrets are non-nullable, and profile-specific schemas include inherited default-profile fields. ## Files (`as_path`) [Section titled “Files (as\_path)”](#files-as_path) File-shaped secrets are materialized as mode-0400 temporary files. The returned path must remain valid after `Load()`, so the caller owns its lifetime. `Resolved` implements `IDisposable`; use a `using` declaration or call `Close()` to remove these files deterministically: ``` using Cachix.SecretSpec; using var resolved = SecretSpec.Builder().WithReason("TLS boot").Load(); var certificatePath = resolved.Secrets["TLS_CERT"].Get(); // Use the certificate before resolved is disposed. ``` ## Native loading [Section titled “Native loading”](#native-loading) The NuGet runtime asset is selected automatically. For local SDK development, `SECRETSPEC_FFI_LIB` can point to a particular `libsecretspec` build. From a SecretSpec source checkout, the SDK also searches an ancestor Cargo `target/debug` or `target/release` directory. # Go SDK > Resolve SecretSpec secrets from Go **Changed in version 0.20** `libsecretspec` was named `secretspec-ffi` through SecretSpec 0.19. The current Go loader prefers the new artifact name and still accepts pre-0.20 shared libraries. The Go SDK (`secretspec-go`) is a thin client over the `libsecretspec` C ABI, loaded via [purego](https://github.com/ebitengine/purego) (dlopen, no cgo). Resolution happens in the Rust core, so the SDK inherits every provider with no Go-side logic. ## Quick start [Section titled “Quick start”](#quick-start) ``` package main import ( "fmt" "log" secretspec "github.com/cachix/secretspec/secretspec-go" ) func main() { resolved, err := secretspec.New(). WithProvider("keyring://"). WithProfile("production"). WithReason("boot web app"). Load() if err != nil { log.Fatal(err) } fmt.Println(resolved.Provider, resolved.Profile) db := resolved.Secrets["DATABASE_URL"] fmt.Println(db.Get()) // the value, or the file path for as_path secrets resolved.SetAsEnv() // export everything into the process environment } ``` A missing required secret returns `*MissingRequiredError`; any other failure returns `*Error` (with a stable `.Kind`). ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```go builder := secretspec.New().WithCaller(secretspec.CallerContext{ Name: "git", Version: "2.51.0", Operation: "credential_get", Resource: "github.com", }) ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Applications that own their declarations in code can resolve a strict JSON inline specification without creating a temporary `secretspec.toml` file. Pass the wire document to `WithInlineSpec`; `baseDir` resolves relative provider paths just as a manifest’s directory would. ```go spec := map[string]any{ "project": map[string]any{"name": "my-app"}, "profiles": map[string]any{ "default": map[string]any{"secrets": map[string]any{ "API_TOKEN": map[string]any{"description": "API token"}, }}, }, } resolved, err := secretspec.New(). WithInlineSpec(spec, "/logical/project"). WithReason("application startup"). Load() ``` Inline specification v1 uses `project`, `profiles`, and each profile’s `secrets` object; optional `providers`, `scopes`, profile `defaults`, and the normal secret declaration fields are also supported. Unknown declaration fields are rejected. `project.extends` resolves parent manifests relative to `baseDir`. The SDK requires the native `secretspec_call` capability for inline specs; an older library returns a capability error rather than falling back to a filesystem search. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `WithScope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available as `Resolved.Scope` and `Report.Scope`: ``` package main import ( "log" secretspec "github.com/cachix/secretspec/secretspec-go" ) func main() { resolved, err := secretspec.New().WithScope("api").Load() if err != nil { log.Fatal(err) } defer resolved.Close() } ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate typed structs with `secretspec schema` plus [quicktype](https://quicktype.io), then unmarshal `resolved.FieldsJSON()`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang go -o secrets_gen.go ``` ``` package main import ( "fmt" "log" secretspec "github.com/cachix/secretspec/secretspec-go" ) func main() { resolved, err := secretspec.New().Load() if err != nil { log.Fatal(err) } defer resolved.Close() data, _ := resolved.FieldsJSON() typed, _ := UnmarshalSecretSpec(data) // typed, generated fmt.Println(typed.DatabaseURL) } ``` ## Library discovery [Section titled “Library discovery”](#library-discovery) The native `libsecretspec` cdylib is resolved at runtime, in order: 1. The `SECRETSPEC_FFI_LIB` environment variable (an explicit path). 2. A library embedded at build time with `-tags embed_lib`. 3. A Cargo `target` directory found by searching up from the working directory (the development path). The SDK uses [purego](https://github.com/ebitengine/purego), so the cdylib is loaded at runtime, not linked. Either install/build `libsecretspec` and set `SECRETSPEC_FFI_LIB`, or stage the per-platform library into `lib/` and build with `-tags embed_lib` for a self-contained binary. The embedded library is extracted to a per-user, owner-only cache directory at first use, and is not distributed through the Go module proxy. ## Static linking [Section titled “Static linking”](#static-linking) For a self-contained binary with no runtime library to locate, build with `-tags static` instead. This uses cgo and links `libsecretspec.a` directly into the Go binary. In a development checkout: ```bash $ bash scripts/stage-staticlib.sh $ CGO_ENABLED=1 go build -tags static ./... ``` ## Linking with pkg-config [Section titled “Linking with pkg-config”](#linking-with-pkg-config-019) **New in version 0.19** Install one library type with [cargo-c](https://github.com/lu-zero/cargo-c): ```bash # Use "static" (the default) or "shared"; use separate prefixes for both. $ bash libsecretspec/scripts/cinstall.sh "$PREFIX" static ``` Then use the same build command for either type: ```bash $ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" CGO_ENABLED=1 go build -tags pkgconfig ./... ``` Unlike staging, this also works for a `go get` dependency. A shared install in a non-system prefix also requires `PREFIX/lib` in the platform’s runtime library search path. # Haskell SDK > Resolve SecretSpec secrets from Haskell **Changed in version 0.20** `libsecretspec` was named `secretspec-ffi` through SecretSpec 0.19. New static builds and pkg-config metadata use `libsecretspec.a` and `libsecretspec.pc`. The Haskell SDK (`secretspec-hs`) is a thin client over the `libsecretspec` C ABI, linked at build time via the Haskell FFI. Resolution happens in the Rust core, so the SDK inherits every provider with no Haskell-side logic. ## Quick start [Section titled “Quick start”](#quick-start) ``` {-# LANGUAGE OverloadedStrings #-} import qualified Data.Map.Strict as Map import Data.Function ((&)) import qualified SecretSpec as S main :: IO () main = do resolved <- S.load ( S.builder & S.withProvider "keyring://" & S.withProfile "production" & S.withReason "boot web app" ) print (S.resolvedProvider resolved, S.resolvedProfile resolved) case Map.lookup "DATABASE_URL" (S.resolvedSecrets resolved) of Just db -> print (S.get db) -- the value, or the file path for as_path secrets Nothing -> pure () S.setAsEnv resolved -- export everything into the process environment S.close resolved ``` A missing required secret throws `MissingRequiredError`; any other failure throws `SecretSpecError` (with a stable `errorKind`). `as_path` secrets are materialized to temp files that outlive the call; call `S.close resolved` when done so they do not accumulate in the temp dir. ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```haskell let caller = S.CallerContext "git" (Just "2.51.0") (Just "credential_get") (Just "github.com") configured = S.builder & S.withCaller caller ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `withInlineSpec spec baseDir` with an Aeson JSON value to resolve strict inline-spec v1 declarations. `baseDir` resolves relative provider paths; an older static archive fails to link the versioned call symbol. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `withScope "api"` to resolve only a named `[scopes.api]` subset. The selected name is available through `resolvedScope` and `reportScope`: ``` {-# LANGUAGE OverloadedStrings #-} import Data.Function ((&)) import qualified SecretSpec as S main :: IO () main = do resolved <- S.load (S.builder & S.withScope "api") S.close resolved ``` ## Value-free report [Section titled “Value-free report”](#value-free-report) `S.report` returns the inventory/preflight view: per-secret status and provenance, never a value. Unlike `load`, it does not throw when a required secret is missing — that secret appears as a `SecretReport` with `srStatus` `"missing_required"`. ``` {-# LANGUAGE OverloadedStrings #-} import Data.Function ((&)) import qualified SecretSpec as S main :: IO () main = do rep <- S.report (S.builder & S.withProfile "production") mapM_ (\s -> print (S.srName s, S.srStatus s, S.srRequired s)) (S.reportSecrets rep) ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate a typed record with `secretspec schema` plus [quicktype](https://quicktype.io), then decode `S.fieldsJson resolved`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang haskell -o Secrets.hs ``` ## Building [Section titled “Building”](#building) The build links the `libsecretspec` archive statically. Stage the `.a` in a directory of its own (so the linker picks the archive, not the co-located `.so`) and pass its native dependencies to the linker: ```bash $ cargo build -p libsecretspec $ TARGET="$(cargo metadata --no-deps --format-version 1 \ | grep -o '"target_directory":"[^"]*"' | head -1 | sed 's/.*:"\(.*\)"/\1/')" # Stage the staticlib alone, and capture its native-static-libs for the linker. $ LIBDIR="$(mktemp -d)" $ cp "$TARGET/debug/libsecretspec.a" "$LIBDIR/" $ NATIVE_LIBS="$(cargo rustc -q -p libsecretspec --crate-type staticlib -- \ --print native-static-libs 2>&1 | sed -n 's/^note: native-static-libs: //p' | tail -1)" $ cabal build --extra-lib-dirs="$LIBDIR" --ghc-options="-optl${NATIVE_LIBS// / -optl}" $ cabal test --extra-lib-dirs="$LIBDIR" --ghc-options="-optl${NATIVE_LIBS// / -optl}" ``` ### Linking with pkg-config [Section titled “Linking with pkg-config”](#linking-with-pkg-config-019) **New in version 0.19** Install one library type with [cargo-c](https://github.com/lu-zero/cargo-c): ```bash # Use "static" (the default) or "shared"; use separate prefixes for both. $ bash libsecretspec/scripts/cinstall.sh "$PREFIX" static ``` Then use the same Cabal flag for either type: ```bash $ cd secretspec-hs $ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" cabal build -f use-pkg-config $ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" cabal test -f use-pkg-config ``` A shared install in a non-system prefix also requires `PREFIX/lib` in the platform’s runtime library search path. # JVM SDK > Resolve SecretSpec secrets from Java Virtual Machine languages **New in version 0.20** The JVM SDK (`org.cachix.secretspec-jvm`) is a thin client over the same Rust resolver as the CLI. Every provider, fallback chain, profile, generator, reference, and `as_path` secret therefore works without JVM-side resolution logic. The 0.20+ JVM package carries native assets under the `libsecretspec` embedded C ABI name. ## Install [Section titled “Install”](#install-020) **New in version 0.20** ### Using gradle [Section titled “Using gradle”](#using-gradle) In your `build.gradle.kts` dependencies: ```kotlin implementation("org.cachix:secretspec-jvm:$secretspecVersion") ``` ### Using Maven [Section titled “Using Maven”](#using-maven) In your `pom.xml` dependencies: ```xml org.cachix secretspec-jvm ${secretspec.version} ``` ### Native libraries [Section titled “Native libraries”](#native-libraries) The package targets JDK 11 and includes native resolvers for glibc and musl Linux x64/Arm64, macOS x64/Arm64, and Windows x64/Arm64. Windows assets statically include the C runtime. No separate SecretSpec CLI, native library, Visual C++ Redistributable, or system `libdbus` installation is needed. ## Quick start [Section titled “Quick start”](#quick-start) ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; public class QuickStartExample { public static void main(String[] args) { try (var resolved = SecretSpec.builder() .withProvider("keyring://") .withProfile("production") .withReason("boot web app") .load() ) { System.out.println(resolved.provider() + " (" + resolved.profile() + ")"); System.out.println(resolved.secret("DATABASE_URL").get()); resolved.setAsSystemProperties(); } } } ``` `get()` returns the inline value, or the readable file path for an `as_path` secret. A missing required secret throws `MissingRequiredException`; its `missing()` method returns the secret names. Other failures throw `SecretSpecException`, whose `kind()` method returns a stable error category. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-021) **New in version 0.21** Use `withInlineSpec(spec, baseDir)` to resolve strict inline-spec v1 declarations. `baseDir` resolves relative provider paths. Can be useful if you want to embed the specification inside your application. ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; public class InlineSpecExample { public static void main(String[] args) { try (var resolved = SecretSpec.builder() .withInlineSpec( "{\n" + " \"project\": { \"name\": \"java-inline\" },\n" + " \"providers\": { \"env\": \"dotenv://inline.env\" },\n" + " \"profiles\": { \"default\": { \"secrets\": {\n" + " \"DATABASE_URL\": { \"description\": \"Database URL\", \"providers\": [\"env\"] }\n" + " } } }\n" + "}", System.getProperty("user.dir") ) .withReason("boot web app") .load() ) { System.out.println(resolved.provider() + " (" + resolved.profile() + ")"); System.out.println(resolved.secret("DATABASE_URL").get()); resolved.setAsSystemProperties(); } } } ``` ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `withScope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available as `Resolved.scope()` and `ResolutionReport.scope()`: ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; public class ScopeExample { public static void main(String[] args) { try (var resolved = SecretSpec.builder().withScope("api").load()) { resolved.setAsSystemProperties(); } } } ``` ## Value-free preflight [Section titled “Value-free preflight”](#value-free-preflight) `report()` returns the inventory view exposed by `secretspec check --json`. It never carries values. Missing required secrets appear with `status().equals("missing_required")` rather than throwing, so incomplete deployments can still be inspected. ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; public class ReportExample { public static void main(String[] args) { var report = SecretSpec.builder() .withProfile("production") .withReason("deployment preflight") .report(); for (var secret : report.secrets()) System.out.println(secret.name() + ": " + secret.status()); } } ``` ## Typed access [Section titled “Typed access”](#typed-access) Generate an idiomatic language model from the manifest schema: ```bash secretspec schema | quicktype -s schema --top-level AppSecrets --lang java -o AppSecrets.java ``` Then deserialize the SDK’s flat field map: ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; import io.quicktype.AppSecrets; import io.quicktype.Converter; import java.io.IOException; public class TypedAccessExample { public static void main(String[] args) throws IOException { try (var resolved = SecretSpec.builder().load()) { AppSecrets typed = Converter.fromJsonString(resolved.fieldsJson()); System.out.println(typed.getDatabaseURL()); } } } ``` The schema models successful resolution: required, defaulted, and generated secrets are non-nullable, and profile-specific schemas include inherited default-profile fields. ## Files (`as_path`) [Section titled “Files (as\_path)”](#files-as_path) File-shaped secrets are materialized as mode-0400 temporary files. The returned path must remain valid after `load()`, so the caller owns its lifetime. `Resolved` implements `AutoCloseable`; use a `try-with-resources` declaration or call `close()` to remove these files deterministically: ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; public class AsPathExample { public static void main(String[] args) { try (var resolved = SecretSpec.builder().withReason("TLS boot").load()) { var secrets = resolved.secrets(); var certificatePath = secrets.get("TLS_CERT").get(); // Use the certificate before resolved is disposed. System.out.println(certificatePath); } } } ``` ## Caller [Section titled “Caller”](#caller-020) **New in version 0.20** Caller context answers *what* invoked SecretSpec (for example, `git`). It is deliberately separate from the user-supplied access reason, which answers *why* the access is happening and may be required by a project’s `require_reason` policy. Caller context never satisfies that policy. The context is caller-asserted metadata, not an authenticated identity. It is included in audit events and forwarded to providers that choose to consume it. Do not put credentials or secret values in any field. ``` package org.cachix.examples; import org.cachix.secretspec.SecretSpec; import org.cachix.secretspec.Caller; public class CallerExample { public static void main(String[] args) { try (var resolved = SecretSpec.builder() .withProvider("keyring://") .withProfile("production") .withCaller(Caller.named("caller name") .withVersion("optional caller version") .withOperation("optional operation") .withResource("optional resource") ) .withReason("boot web app") .load() ) { System.out.println(resolved.provider() + " (" + resolved.profile() + ")"); System.out.println(resolved.secret("DATABASE_URL").get()); } } } ``` ## Native loading [Section titled “Native loading”](#native-loading) The Jar runtime asset is selected automatically. For local SDK development, `SECRETSPEC_FFI_LIB` can point to a particular `libsecretspec` build. From a SecretSpec source checkout, the SDK also searches an ancestor Cargo `target/debug` or `target/release` directory. # Node.js SDK > Resolve SecretSpec secrets from Node.js and TypeScript The Node.js / TypeScript SDK (`secretspec`) is a thin wrapper over a [napi-rs](https://napi.rs/) native addon that embeds the resolver. Resolution happens in the Rust core, so the SDK inherits every provider with no JS-side logic. npm installs a prebuilt addon for the host platform: Linux x64 and arm64 (glibc, and musl for Alpine images in 0.20+), macOS on Apple silicon, and Windows x64. TypeScript declarations ship in `index.d.ts`. ## Quick start [Section titled “Quick start”](#quick-start) ``` const { SecretSpec } = require('secretspec'); const resolved = SecretSpec.builder() .withProvider('keyring://') .withProfile('production') .withReason('boot web app') .load(); console.log(resolved.provider, resolved.profile); const db = resolved.secrets.DATABASE_URL; console.log(db.get()); // the value, or the file path for as_path secrets resolved.setAsEnv(); // export everything into process.env ``` A missing required secret throws `MissingRequiredError`; any other failure throws `SecretSpecError` (with a stable `.kind`). ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```js const builder = SecretSpec.builder().withCaller({ name: 'git', version: '2.51.0', operation: 'credential_get', resource: 'github.com', }); ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `.withInlineSpec(spec, baseDir)` (or `loadAsync`/`reportAsync`) to resolve a strict inline-spec v1 object. `baseDir` resolves relative provider paths; the embedded addon submits the versioned native request directly. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `.withScope('api')` to resolve only a named `[scopes.api]` subset. The selected name is available as `resolved.scope` and `report.scope`: ``` const resolved = SecretSpec.builder().withScope('api').load(); ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate typed interfaces with `secretspec schema` plus [quicktype](https://quicktype.io), then convert `resolved.fieldsJson()`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang typescript -o secrets_gen.ts ``` ``` import { Convert } from './secrets_gen'; // typed, generated const typed = Convert.toSecretSpec(resolved.fieldsJson()); console.log(typed.DATABASE_URL); ``` # SDK Overview > How the SecretSpec language SDKs work SecretSpec ships SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, C# (0.16+), Swift (0.18+) and JVM languages (0.20+). They all resolve secrets from the same declarative `secretspec.toml`, and they all behave identically, because they share one resolver. > **C# compatibility:** Available since SecretSpec 0.16. The 0.15.0 NuGet package is an unsupported package-ID bootstrap; use version 0.16 or later for the API shown in the C# guide. > **Native library name:** Starting with SecretSpec 0.20, the embedded C ABI is `libsecretspec`. It was called `secretspec-ffi` through SecretSpec 0.19; current runtime SDKs retain shared-library filename compatibility. ## One resolver, thin clients [Section titled “One resolver, thin clients”](#one-resolver-thin-clients) Resolution (providers, fallback chains, profiles, generation, `as_path` materialization) lives in a single Rust core. Each SDK is a thin client over that core rather than a reimplementation: * **Rust** uses the library directly, with a compile-time derive macro for strongly-typed access. * **Ruby** (a native C extension) statically links the `libsecretspec` C ABI at build time; **Go** (purego) loads it at runtime with no cgo. Both exchange a small JSON request/response with the core. * **Haskell** links the same C ABI at build time via the GHC FFI. * **C# (0.16+)** loads the same C ABI with P/Invoke from a runtime-specific native asset in the NuGet package. * **Swift (0.18+)** imports the same C ABI as a Clang module from a macOS XCFramework and maps the shared JSON envelope to `Codable` value types. * **Python** uses a [pyo3](https://pyo3.rs/) native extension, and **Node.js/TypeScript** uses a [napi-rs](https://napi.rs/) native addon; both embed the same resolver directly and exchange the same JSON request/response shape as the C ABI. * **PHP** prefers an [ext-php-rs](https://github.com/davidcole1340/ext-php-rs) extension that embeds the resolver (working under FPM with no `ffi.enable`), and falls back to loading the same C ABI at runtime through `ext-ffi`. * **JVM (0.20+)** loads the same C ABI with JNA from a runtime-specific native asset in the Jar package. Because resolution happens in one place, every provider, chain, profile, and generator works the same in every language, and a new provider added to the core is immediately available everywhere with no per-SDK change. A cross-language conformance suite asserts that all the SDKs reduce the same inputs to the same result. ## The runtime API [Section titled “The runtime API”](#the-runtime-api) Each SDK mirrors the Rust derive crate’s vocabulary: a builder that takes a provider, profile, and an access reason, and a `load`/`resolve` that returns the resolved secrets plus the provider and profile used. A missing required secret is a typed error, distinct from a transport failure (which carries a stable `kind`). Secrets exposed `as_path` come back as a readable file path. ```python from secretspec import SecretSpec resolved = SecretSpec.builder().with_provider("keyring://").with_reason("boot").load() print(resolved.secrets["DATABASE_URL"].get) ``` See each language’s page for the idiomatic spelling: [Rust](/sdk/rust), [Python](/sdk/python), [Go](/sdk/go), [Ruby](/sdk/ruby), [Node.js](/sdk/nodejs), [Haskell](/sdk/haskell), [PHP](/sdk/php), [C# (0.16+)](/sdk/csharp), [Swift (0.18+)](/sdk/swift) and [JVM languages (0.20+)](/sdk/jvm). Every builder also takes a [scope (0.17+)](/concepts/scopes/), resolving only a named subset of the profile and returning the selected name on the result. The one exception is Rust’s typed loader, which always resolves the full profile because a generated struct has a field per declared secret; see [Rust](/sdk/rust#scopes-017). ## Typed access [Section titled “Typed access”](#typed-access) Beyond the Rust derive macro, typed accessors for the other languages are generated from the manifest. `secretspec schema` emits a JSON Schema for the secret shape; [quicktype](https://quicktype.io) turns it into an idiomatic type and deserializer for any language, which you build from the SDK’s `fields()` map: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang ``` This keeps the per-language surface tiny: the SDK only provides `fields()`, and quicktype owns the type generation. The schema models successful resolution: required, defaulted, and generated secrets are non-nullable. A profile schema includes fields inherited from the `default` profile and is exhaustive. ## Distribution [Section titled “Distribution”](#distribution) The resolver ships inside each package, so there is nothing extra to install and no runtime library path to set: * **Python** builds the resolver into a pyo3 extension shipped as a `cp39-abi3` wheel, and **Ruby** statically links the `libsecretspec` archive into a native C extension in the gem. * **Haskell** statically links the same archive at build time via the GHC FFI. * **C# (0.16+)** ships the `cdylib` as runtime-specific native assets in one NuGet package and loads the matching asset through P/Invoke. The managed client supports trimming and NativeAOT; glibc/musl Linux, Intel/Arm macOS, and x64/Arm64 Windows assets are included. * **Swift (0.18+)** ships Intel and Apple-silicon macOS cdylibs in one checksummed XCFramework binary target. SwiftPM selects and embeds the matching architecture. * **Go** embeds the `cdylib` in the module and loads it at runtime via purego (no cgo); an opt-in `-tags static` build links it statically instead. * **Node.js** builds the resolver into a napi-rs addon. * **PHP** ships as a normal PHP extension (provisioned like `ext-redis`), with an `ext-ffi` fallback that dlopens the bundled `cdylib`. * **JVM** ships the `cdylib` as runtime-specific native assets in one Jar package and loads the matching asset through JNA; glibc/musl Linux, Intel/Arm macOS, and x64/Arm64 Windows assets are included. Because the resolver is linked or embedded directly, the SDKs do not depend on a separately installed `cdylib` or an `LD_LIBRARY_PATH`/`SECRETSPEC_FFI_LIB` override at runtime — the one exception being PHP’s optional `ext-ffi` fallback, where `SECRETSPEC_FFI_LIB` can point at a specific library build. ## Platform support [Section titled “Platform support”](#platform-support) Prebuilt packages cover the following platforms. Windows support for the Python wheel, the Ruby gem, and the PHP extension binaries is added in SecretSpec 0.17. | SDK | Linux x64 | Linux arm64 | macOS Intel | macOS Apple silicon | Windows x64 | Windows arm64 | | ------------------- | --------- | ----------- | ----------- | ------------------- | ----------- | ------------- | | Rust (source crate) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Python | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | Node.js | ✓ | ✓ | — | ✓ | ✓ | — | | Go | ✓ | ✓ | — | ✓ | ✓ | — | | Ruby | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Swift (0.18+) | — | — | ✓ | ✓ | — | — | | PHP | ✓ | ✓ | — | ✓ | ✓ (0.17+) | — | | Haskell (source) | ✓ | — | — | — | ✓ (0.17+) | — | | JVM (0.20+) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Most Linux packages build against a manylinux\_2\_28 baseline (glibc 2.28 or newer); the C# and JVM packages additionally ship musl Linux assets. Hackage distributes the Haskell SDK as source, so its row records the platforms CI builds and tests. Contributors: the [SDK development](/development/sdks) page documents how these artifacts are built and how to add a platform. # PHP SDK > Resolve SecretSpec secrets from PHP, Laravel, and Symfony **Changed in version 0.20** `libsecretspec` was named `secretspec-ffi` through SecretSpec 0.19. The PHP loader prefers the new artifact name and still accepts pre-0.20 shared libraries. The PHP SDK (`cachix/secretspec`) is a thin client over the same Rust resolver every other SecretSpec SDK uses, so it inherits every provider, chain, profile, and generator with no PHP-side logic. It reaches the resolver through one of two native backends over an identical JSON contract: * **The `secretspec` PHP extension** (built with [ext-php-rs](https://github.com/davidcole1340/ext-php-rs)) embeds the resolver the way `pdo` or `redis` do. It needs no `ffi.enable` and works under PHP-FPM and the web SAPI out of the box — the recommended path for Laravel and Symfony. * **`ext-ffi`** dlopens the `libsecretspec` shared library at runtime. Nothing to compile, ideal for CLI tools and local development; requires the FFI extension enabled. The SDK prefers the extension whenever it is loaded and transparently falls back to `ext-ffi`, so your application code is the same either way. ## Install [Section titled “Install”](#install) ```bash $ composer require cachix/secretspec ``` That installs the pure-PHP client. Then provide the native resolver with **one** of the backends below. ### Option A — the PHP extension (recommended for web / FPM) [Section titled “Option A — the PHP extension (recommended for web / FPM)”](#option-a--the-php-extension-recommended-for-web--fpm) The `secretspec-php-native` extension embeds the resolver, so it works under PHP-FPM with no `ffi.enable` and nothing to locate at runtime — the same operational model as `ext-redis` or `ext-imagick` (the binary is provisioned at the image/host level, not by Composer). Install it one of three ways: * **Prebuilt binary** — download the `secretspec-php-native--nts-` shared object for your PHP version and platform from the [GitHub release](https://github.com/cachix/secretspec/releases), then enable it in `php.ini`: ```ini extension=/path/to/secretspec-php-native.so ``` In an official PHP Docker image, drop it into the extension dir and `docker-php-ext-enable secretspec-php-native`. * **Build from source** (needs the Rust toolchain, `php-config`, and libclang): ```bash cargo build --release -p secretspec-php-native # then point extension= at target/release/libsecretspec_php_native.so ``` Once loaded, `php -m` lists `secretspec-php-native` and the SDK uses it automatically. ### Option B — ext-ffi (quick start / CLI) [Section titled “Option B — ext-ffi (quick start / CLI)”](#option-b--ext-ffi-quick-start--cli) The FFI backend dlopens the `libsecretspec` library at runtime. Enable the bundled FFI extension — in CLI it is on by default; for the web SAPI set: ```ini extension=ffi ffi.enable=true ``` Then fetch the native library for your platform (a one-time step; Composer does not run it automatically): ```bash $ vendor/bin/secretspec-install-lib ``` That downloads the right `libsecretspec` library from the matching GitHub release into the package. Alternatively, point `SECRETSPEC_FFI_LIB` at a library you built or placed yourself. The SDK looks at `SECRETSPEC_FFI_LIB` first, then the downloaded copy, then a local Cargo `target/` directory. ## Quick start [Section titled “Quick start”](#quick-start) ``` withProvider('keyring://') ->withProfile('production') ->withReason('boot web app') ->load(); echo $resolved->provider, ' ', $resolved->profile, PHP_EOL; $db = $resolved->secrets['DATABASE_URL']; echo $db->get(); // the value, or the file path for as_path secrets $resolved->setAsEnv(); // export everything into getenv()/$_ENV/$_SERVER ``` A missing required secret throws `Secretspec\MissingRequiredException` (with a `->missing` list); any other failure throws `Secretspec\SecretSpecException` (with a stable `->kind`). There is also a one-shot form using named arguments: ``` withCaller(new CallerContext( name: 'git', version: '2.51.0', operation: 'credential_get', resource: 'github.com', )); ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `withInlineSpec($spec, $baseDir)` to resolve a strict inline-spec v1 PHP array. The embedded extension or FFI fallback uses the versioned native call; an older cdylib raises a capability error instead of searching for a manifest. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `withScope('api')` to resolve only a named `[scopes.api]` subset. The selected name is available as `$resolved->scope` and `$report->scope`: ``` withScope('api')->load(); ``` ## Laravel [Section titled “Laravel”](#laravel) Resolve your secrets early and export them so Laravel’s `env()` and config see them. A service provider is a natural home: ``` withProfile(app()->environment()) // "production", "local", ... ->withReason('laravel boot') ->load() ->setAsEnv(); } } ``` Register it first in `bootstrap/providers.php` so the secrets are present before other providers read configuration. Because `setAsEnv()` also populates `$_ENV` and `$_SERVER`, the `env()` helper and any `config/*.php` that calls `env(...)` resolve normally. > If you run `php artisan config:cache`, configuration is frozen at cache time and `env()` is not read per request. Either resolve before caching, or bind the `Resolved` into the container and read secrets from it directly where you need them. ## Symfony [Section titled “Symfony”](#symfony) Export the secrets in the front controller and `bin/console`, before the kernel boots, so `%env(DATABASE_URL)%` in your config resolves: ``` withProfile($_SERVER['APP_ENV'] ?? 'dev') ->withReason('symfony boot') ->load() ->setAsEnv(); ``` `setAsEnv()` sets `$_ENV`, `$_SERVER`, and `putenv()`, all three of which Symfony’s env-var processors read, so no bundle or extra configuration is needed. ## Plain PHP [Section titled “Plain PHP”](#plain-php) Point the builder at a specific manifest and provider and read the values back: ``` withPath(__DIR__.'/secretspec.toml') ->withProvider('dotenv://.env.production') ->withReason('cron job') ->load(); foreach ($resolved->secrets as $name => $secret) { // $secret->get() is the value, or a readable file path for as_path secrets. printf("%s=%s\n", $name, $secret->get()); } ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate a typed class with `secretspec schema` plus [quicktype](https://quicktype.io), then build it from `$resolved->fields()`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang php -o SecretSpecTyped.php ``` ``` fields() is a [SECRET_NAME => value] map; quicktype's `from` // wants an object, so cast it. $typed = SecretSpec::from((object) $resolved->fields()); echo $typed->getDatabaseURL(); ``` ## Files (`as_path`) [Section titled “Files (as\_path)”](#files-as_path) Secrets declared `as_path` are materialized to a temporary file and come back as a readable path; `$secret->get()` returns the path. The SDK persists the file (mode 0400) so the path stays valid after `load()` returns — you own its lifetime. Call `$resolved->close()` when done to remove those temp files: ``` withReason('tls')->load(); try { $certPath = $resolved->secrets['TLS_CERT']->get(); // ... use the file ... } finally { $resolved->close(); } ``` ## Native backends [Section titled “Native backends”](#native-backends) The SDK chooses a backend automatically: if the `secretspec-php-native` extension is loaded it is used directly (no `ffi.enable`, no library to locate); otherwise the SDK dlopens the `libsecretspec` library via `ext-ffi`, looking first at `SECRETSPEC_FFI_LIB`, then the copy `vendor/bin/secretspec-install-lib` places in the package, then a local Cargo `target/` directory. Both backends call the identical Rust `resolve_json`, so the result is the same — a cross-language conformance suite asserts it. # Python SDK > Resolve SecretSpec secrets from Python The Python SDK (`secretspec`) is a thin client over a pyo3 extension that calls `secretspec::resolve_json` directly. Resolution (providers, chains, profiles, generation, `as_path`) happens in the Rust core, so the SDK inherits every provider with no Python-side logic. ## Quick start [Section titled “Quick start”](#quick-start) ``` from secretspec import SecretSpec resolved = ( SecretSpec.builder() .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .load() ) print(resolved.provider, resolved.profile) db = resolved.secrets["DATABASE_URL"] print(db.get) # the value, or the file path for as_path secrets resolved.set_as_env() # export everything into os.environ ``` A missing required secret raises `MissingRequiredError`; any other failure raises `SecretSpecError` (with a stable `.kind`). ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```python from secretspec import CallerContext, SecretSpec builder = SecretSpec.builder().with_caller(CallerContext( name="git", version="2.51.0", operation="credential_get", resource="github.com", )) ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `.with_inline_spec(spec, base_dir)` to resolve a strict inline-spec v1 dictionary without a manifest file. `base_dir` resolves relative provider paths. An inline call uses the versioned native entry point, so it cannot fall back to a filesystem manifest on an older runtime. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `.with_scope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available as `resolved.scope` and `report.scope`: ``` resolved = SecretSpec.builder().with_scope("api").load() ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate typed classes with `secretspec schema` plus [quicktype](https://quicktype.io), then build them from `resolved.fields()`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang python -o secrets_gen.py ``` ``` from secrets_gen import SecretSpec as Secrets # typed typed = Secrets.from_dict(resolved.fields()) print(typed.database_url) # typed str ``` ## Native library [Section titled “Native library”](#native-library) The resolver is statically linked into a pyo3 extension (`secretspec._native`, built from the `secretspec-py-native` crate) using pyo3’s `abi3-py39` feature, so the published `cp39-abi3` wheel is self-contained — there is no separate `cdylib` to locate and no runtime dlopen. # Ruby SDK > Resolve SecretSpec secrets from Ruby **Changed in version 0.20** `libsecretspec` was named `secretspec-ffi` through SecretSpec 0.19. Upgrade the extension source and `libsecretspec.a` together because 0.20 adds the `secretspec_call` symbol. The Ruby SDK (`secretspec`) is a thin client over the `libsecretspec` C ABI, linked into a native C extension at build time. Resolution happens in the Rust core, so the SDK inherits every provider with no Ruby-side logic. ## Quick start [Section titled “Quick start”](#quick-start) ``` require "secretspec" resolved = Secretspec::SecretSpec.builder .with_provider("keyring://") .with_profile("production") .with_reason("boot web app") .load puts resolved.provider, resolved.profile db = resolved.secrets["DATABASE_URL"] puts db.get # the value, or the file path for as_path secrets resolved.set_as_env! # export everything into ENV ``` A missing required secret raises `Secretspec::MissingRequiredError`; any other failure raises `Secretspec::Error` (with a stable `#kind`). ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```ruby builder = Secretspec::SecretSpec.builder.with_caller( Secretspec::CallerContext.new( name: "git", version: "2.51.0", operation: "credential_get", resource: "github.com" ) ) ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `.with_inline_spec(spec, base_dir)` to resolve a strict inline-spec v1 hash at its logical provider-path base directory. The extension links the separate native call symbol, so an older archive cannot fall back to a manifest search. ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** Use `.with_scope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available as `resolved.scope` and `report.scope`: ``` resolved = Secretspec::SecretSpec.builder.with_scope("api").load ``` ## Typed access (codegen) [Section titled “Typed access (codegen)”](#typed-access-codegen) Generate typed classes with `secretspec schema` plus [quicktype](https://quicktype.io), then build them from `resolved.fields`: ```bash $ secretspec schema | quicktype -s schema --top-level SecretSpec --lang ruby -o secrets_gen.rb ``` ``` typed = SecretSpec.from_dynamic!(resolved.fields) # typed, generated puts typed.database_url ``` ## Native library [Section titled “Native library”](#native-library) The published platform gems bundle the `libsecretspec` archive and statically link it into the mkmf extension at install time. ### Linking with pkg-config [Section titled “Linking with pkg-config”](#linking-with-pkg-config-019) **New in version 0.19** Install one library type with [cargo-c](https://github.com/lu-zero/cargo-c): ```bash # Use "static" (the default) or "shared"; use separate prefixes for both. $ bash libsecretspec/scripts/cinstall.sh "$PREFIX" static ``` Then use the same extension flag for either type: ```bash $ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" gem install secretspec -- --enable-pkg-config ``` A shared install in a non-system prefix also requires `PREFIX/lib` in the platform’s runtime library search path. # Rust SDK > Type-safe Rust integration for SecretSpec SecretSpec provides a Rust library with type-safe access to secrets through a derive macro. The macro reads `secretspec.toml` at compile time and generates Rust types for its profiles and secrets. ## Quick start [Section titled “Quick start”](#quick-start) **Changed in version 0.20** On 0.19 and earlier, also depend on `secrecy = { version = "0.10", features = ["serde"] }` and `serde = { version = "1", features = ["derive"] }`. Add the runtime and derive macro from the command line: ```shell cargo add secretspec secretspec-derive ``` Alternatively, add both dependencies to your `Cargo.toml`: ```toml [dependencies] secretspec = "0.20" secretspec-derive = "0.20" ``` The examples on this page are compiled as Cargo examples in `secretspec-derive`. They generate their types from this manifest: ``` [project] name = "rust-sdk-example" revision = "1.0" [profiles.default] DATABASE_URL = { description = "PostgreSQL connection string", required = true } REDIS_URL = { description = "Redis connection string", required = false } TLS_CERT = { description = "TLS certificate", required = true, as_path = true } TLS_KEY = { description = "TLS private key", required = false, as_path = true } [profiles.development] DATABASE_URL = { default = "postgresql://localhost/development" } [profiles.production] DATABASE_URL = { required = true } API_KEY = { description = "Production API key", required = true } [scopes.api] secrets = ["DATABASE_URL"] ``` `declare_secrets!` generates `SecretSpec`, `Profile`, and `SecretSpecProfile`. The standard loader returns the union type that is safe to use with any declared profile: ``` secretspec_derive::declare_secrets!("secretspec.toml"); fn main() -> Result<(), Box> { let resolved = SecretSpec::builder() .with_provider("keyring://") .with_profile("development") .with_reason("start application") .load()?; println!("Database: {}", resolved.secrets.database_url); if let Some(redis_url) = &resolved.secrets.redis_url { println!("Redis: {redis_url}"); } resolved.secrets.set_as_env_vars(); println!("Profile: {}", resolved.profile); println!("Provider: {}", resolved.provider); Ok(()) } ``` Required and defaulted secrets are generated as `String`; secrets that may be absent are `Option`. Field names use Rust snake case, so `DATABASE_URL` becomes `database_url`. ## Describing secrets in Rust [Section titled “Describing secrets in Rust”](#describing-secrets-in-rust-020) **New in version 0.20** Starting with SecretSpec 0.20, `Spec` is the format-independent declaration API. Build one directly in Rust when an application owns its secret contract in code, then pass the validated specification to `Secrets::from_spec`: ``` use secretspec::{Profile, Secret, Secrets, Spec}; fn main() -> secretspec::Result<()> { let spec = Spec::builder("checkout") .provider("env", "env://") .secret( "DATABASE_URL", Secret::required("PostgreSQL connection URL").providers(["env"]), ) .secret( "SENTRY_DSN", Secret::optional("Sentry error-reporting endpoint"), ) .profile( "production", Profile::new().secret("SENTRY_DSN", Secret::required("Production Sentry endpoint")), ) .scope("web", ["DATABASE_URL", "SENTRY_DSN"]) .build()?; let mut secrets = Secrets::from_spec(spec)?; secrets.set_profile("production"); secrets.set_scope("web"); let resolved = secrets.resolve()?; println!("resolved profile: {}", resolved.profile); Ok(()) } ``` `Spec::from_toml` and `Spec::try_from(path)` produce the same type through the same validation and compilation path. Convert from a path for manifests with `extends`, because a TOML string has no directory from which to resolve relative paths. A spec loaded from a file retains that file’s directory for relative provider paths. A Rust-built declaration resolves them from the current working directory by default; `Secrets::from_spec_at` selects another logical base directory. Use `schema_json(None)` to emit the value-free JSON Schema for the union shape, or pass a profile name for that profile’s effective fields: ```rust let union_schema = spec.schema_json(None)?; let production_schema = spec.schema_json(Some("production"))?; ``` Schema generation reads declarations only. It does not resolve secret values or contact providers. `Spec` is immutable so its declarations and compiled view cannot drift apart. Use `to_builder()` to edit a copy, or `into_builder()` to consume the original, then rebuild to validate the result: ```rust let edited = spec .to_builder() .remove_secret("default", "LEGACY_TOKEN") .add_secret( "production", "DEPLOY_TOKEN", Secret::required("Production deployment token"), ) .build()?; ``` `remove_secret` removes the declaration from that profile. Removing an override can reveal the declaration inherited from `default`; removing it from `default` also removes it from profiles that only inherited it. `build()` rejects dangling scope membership, invalid compositions, empty profiles, and other semantic errors introduced by an edit. For a spec loaded from TOML, `secret`, `add_secret`, `replace_secret`, and `remove_secret` preserve comments, ordering, quoting, and unrelated syntax in the root document. Read the edited document with `edited.preserved_text()`. Adding and then removing the same declaration restores the original bytes, and inherited declarations are never inlined into a child manifest. Other builder operations are semantic edits and clear the retained text; use `to_toml()` when freshly formatted output is acceptable. A spec constructed with `Spec::builder()` has no original document to preserve. The Rust-first API complements `declare_secrets!`: `Spec` describes and resolves names dynamically, while the macro continues to generate statically typed fields from a manifest at compile time. ## Profile-specific types [Section titled “Profile-specific types”](#profile-specific-types) Use `load_profile()` when code should receive the exact shape of the selected profile. It returns a `SecretSpecProfile` enum whose variants contain that profile’s effective fields, including fields inherited from `[profiles.default]`: ``` secretspec_derive::declare_secrets!("secretspec.toml"); fn main() -> Result<(), Box> { let resolved = SecretSpec::builder() .with_provider("keyring://") .with_profile(Profile::Production) .with_reason("start production application") .load_profile()?; match resolved.secrets { SecretSpecProfile::Production { database_url, api_key, .. } => { println!("Database: {database_url}"); println!("API key loaded: {} bytes", api_key.len()); } _ => unreachable!("the production profile was selected"), } Ok(()) } ``` ## Scopes [Section titled “Scopes”](#scopes-017) **New in version 0.17** A [scope](/concepts/scopes/) resolves only a named subset of a profile. Scopes are available through the untyped `Secrets` API: ``` use secretspec::Secrets; fn main() -> Result<(), Box> { let mut spec = Secrets::load()?; spec.set_scope("api"); let resolved = spec.resolve()?; assert_eq!(resolved.scope.as_deref(), Some("api")); Ok(()) } ``` `resolve()` and `report()` both return the active scope. The untyped API also honors `SECRETSPEC_SCOPE` when no scope is selected explicitly. Typed loaders generated by `declare_secrets!` deliberately do not support scopes. A generated struct has a field for every declared secret, so hiding one would leave that field unfillable. `SecretSpec::builder()` therefore has no `with_scope`, and typed `load()` and `load_profile()` always resolve the full profile. Use a separate manifest or the untyped API when a component needs a narrowed set. ## Resolving one secret [Section titled “Resolving one secret”](#resolving-one-secret-019) **New in version 0.19** `resolve()` answers whether the whole profile can be satisfied, so a single missing required secret fails it and returns nothing. When a component needs one secret, `resolve_named()` reads only that secret and the inputs it composes from, and reports the outcomes separately: ``` use secretspec::{NamedResolution, Secrets}; fn main() -> Result<(), Box> { // Resolving one secret reads only that secret and its composition inputs, // so an unrelated missing required secret cannot fail the call. let spec = Secrets::load()?.with_default_reason("cache warmup"); match spec.resolve_named("REDIS_URL")? { NamedResolution::Resolved(secret) => { // Exactly one of `value` and `path` is set; `path` for `as_path`. println!("resolved from {:?}", secret.source); } // Declared, but nothing provided it. `required` says whether a // whole-profile resolve would treat that as an error. NamedResolution::Missing { required } => { println!("no value (required: {required})"); } // Not declared in this profile, or hidden by the active scope. NamedResolution::Undeclared => println!("not on this profile's surface"), } Ok(()) } ``` `NamedResolution::Undeclared` covers both a name the profile does not declare and one the active [scope](/concepts/scopes/) hides, since neither is on the surface this session resolves. Provider and configuration failures stay `Err` rather than turning into a missing value, and whole-profile presence constraints (`at_least_one`, `exactly_one`) are not evaluated for a single-secret read. `with_default_reason()` (also 0.19+) supplies a reason only when the caller has not already set one through `with_reason()` or `SECRETSPEC_REASON`, so a wrapper can describe itself without overwriting the more specific reason it was given. When a wrapper only needs to identify the software integration, use the separate caller context below; unlike a default reason, it cannot satisfy `require_reason`. ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** Software integrations can record what invoked SecretSpec without replacing the user-supplied access reason: ```rust use secretspec::{CallerContext, Secrets}; let spec = Secrets::load()?.with_caller( CallerContext::new("git") .with_version("2.51.0") .with_operation("credential_get") .with_resource("github.com"), ); ``` Generated builders expose the same `with_caller()` method. Caller context is caller-asserted audit metadata, not an authenticated identity, and never satisfies `require_reason`. Do not put credentials or secret values in it. ## Interactive prompting [Section titled “Interactive prompting”](#interactive-prompting-020) **New in version 0.20** `Secrets::ensure_secrets` prompts for and stores any missing required secret when stdin is a real terminal. Generated builders opt into the same behavior with `prompt_missing()`: ``` secretspec_derive::declare_secrets!("secretspec.toml"); fn main() -> Result<(), Box> { let resolved = SecretSpec::builder() .with_provider("keyring://") .with_profile("development") .with_reason("start application") .prompt_missing(true) .load()?; println!("Database: {}", resolved.secrets.database_url); Ok(()) } ``` Left unset (the default), a missing required secret still fails fast with `RequiredSecretMissing`, exactly as without `prompt_missing()`. ## Secrets as file paths [Section titled “Secrets as file paths”](#secrets-as-file-paths) Secrets declared with `as_path = true` are generated as `PathBuf` instead of `String`. Optional file-shaped secrets use `Option`: ``` secretspec_derive::declare_secrets!("secretspec.toml"); fn main() -> Result<(), Box> { let resolved = SecretSpec::builder() .with_provider("keyring://") .with_reason("configure TLS") .load()?; let certificate: &std::path::PathBuf = &resolved.secrets.tls_cert; println!("Certificate: {}", certificate.display()); if let Some(private_key) = &resolved.secrets.tls_key { println!("Private key: {}", private_key.display()); } // The materialized files remain valid until `resolved` is dropped. Ok(()) } ``` # Swift SDK > Resolve SecretSpec secrets from Swift on macOS **New in version 0.18** The Swift SDK is a thin `Codable` wrapper over the same Rust resolver and versioned C ABI as the other language SDKs. Every provider, fallback chain, profile, scope, generator, reference, and `as_path` secret therefore works without Swift-side resolution logic. ## Install [Section titled “Install”](#install-018) **New in version 0.18** In Xcode, choose **File → Add Package Dependencies** and enter: ```text https://github.com/cachix/secretspec ``` Or add the package to `Package.swift`: ```swift dependencies: [ .package( url: "https://github.com/cachix/secretspec", from: "0.18.0" ), ], targets: [ .target( name: "MyApp", dependencies: [ .product(name: "SecretSpec", package: "secretspec"), ] ), ] ``` The package supports macOS 12 or later on Intel and Apple silicon. Its checksummed XCFramework contains the native Rust resolver, so consumers need neither Rust nor a separately installed SecretSpec library. SecretSpec is a development-workflow secrets manager with filesystem, process, and desktop credential-store integrations; the SDK does not target iOS, watchOS, tvOS, or visionOS. ## Quick start [Section titled “Quick start”](#quick-start) ``` import SecretSpec func quickStart() throws { let resolved = try SecretSpec.builder() .withProvider("keyring://") .withProfile("production") .withReason("boot web app") .load() defer { try? resolved.close() } print(resolved.provider, resolved.profile) print(resolved.secrets["DATABASE_URL"]?.get() ?? "") try resolved.setAsEnvironment() } ``` `get()` returns the inline value, or the readable file path for an `as_path` secret. A missing required secret throws `MissingRequiredError`; its `missing` property contains the unresolved names. Other failures throw `SecretSpecError`, whose `kind` property is a stable error category: ``` import SecretSpec func handleErrors() { do { let resolved = try SecretSpec.builder().load() defer { try? resolved.close() } // Use resolved. } catch let error as MissingRequiredError { print("Missing:", error.missing.joined(separator: ", ")) } catch let error as SecretSpecError { print("\(error.kind): \(error.message)") } catch { print(error) } } ``` A one-shot form is also available: ``` import SecretSpec func oneShot() throws { let resolved = try SecretSpec.resolve( provider: "keyring://", profile: "production", reason: "boot web app" ) try resolved.close() } ``` ## Caller context [Section titled “Caller context”](#caller-context-020) **New in version 0.20** ```swift let builder = SecretSpec.builder().withCaller(CallerContext( name: "git", version: "2.51.0", operation: "credential_get", resource: "github.com" )) ``` Caller context identifies the invoking integration in audit records but never satisfies `require_reason`. Do not put credentials or secret values in it. ## Inline specifications [Section titled “Inline specifications”](#inline-specifications-020) **New in version 0.20** Use `try builder.withInlineSpec(spec, baseDir: ...)` with an `Encodable` declaration to resolve strict inline-spec v1 JSON. The base directory resolves relative provider paths; an older XCFramework fails to link the versioned call. ## Scopes [Section titled “Scopes”](#scopes) Use `withScope("api")` to resolve only a named `[scopes.api]` subset. The selected name is available through `resolved.scope` and `report.scope`: ``` import SecretSpec func scopes() throws { let resolved = try SecretSpec.builder().withScope("api").load() try resolved.close() } ``` ## Value-free preflight [Section titled “Value-free preflight”](#value-free-preflight) `report()` returns the inventory view exposed by `secretspec check --json`. It never carries values. A missing required secret appears with `status == "missing_required"` rather than throwing, so an incomplete deployment can still be inspected. ``` import SecretSpec func report() throws { let report = try SecretSpec.builder() .withProfile("production") .withReason("deployment preflight") .report() for secret in report.secrets { print("\(secret.name): \(secret.status)") } } ``` ## Typed access [Section titled “Typed access”](#typed-access) Generate an idiomatic Swift model from the manifest schema: ```bash $ secretspec schema | \ quicktype -s schema --top-level AppSecrets --lang swift -o AppSecrets.swift ``` Then decode the SDK’s flat field map: ``` import Foundation import SecretSpec private struct AppSecrets: Decodable { let databaseURL: String private enum CodingKeys: String, CodingKey { case databaseURL = "DATABASE_URL" } } func typedAccess(resolved: Resolved) throws { let typed = try JSONDecoder().decode( AppSecrets.self, from: resolved.fieldsJSON() ) print(typed.databaseURL) } ``` The schema models successful resolution: required, defaulted, and generated secrets are non-nullable, and profile schemas include inherited default-profile fields. ## Files (`as_path`) [Section titled “Files (as\_path)”](#files-as_path) File-shaped secrets are materialized as mode-0400 temporary files. Call `resolved.close()` after the last consumer finishes with those paths. `Resolved` also performs best-effort cleanup when it is deinitialized, but explicit cleanup gives deterministic lifetime and reports filesystem errors.