<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SecretSpec | Blog</title><description/><link>https://secretspec.dev/</link><language>en</language><item><title>Where .env Went Wrong</title><link>https://secretspec.dev/blog/where-env-went-wrong/</link><guid isPermaLink="true">https://secretspec.dev/blog/where-env-went-wrong/</guid><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; is one of software’s most successful accidents.&lt;/p&gt;
&lt;p&gt;It starts as a shortcut for three &lt;code dir=&quot;auto&quot;&gt;export&lt;/code&gt; commands. Then it becomes the
project’s configuration schema, secret store, environment model, onboarding
guide, CI interface, and deployment format.&lt;/p&gt;
&lt;p&gt;Environment variables do one job well: deliver strings to a process. &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt;
turned that delivery mechanism into a source of truth.&lt;/p&gt;
&lt;p&gt;A convenience became architecture. That is where &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; went wrong.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;environment-variables-only-deliver-values&quot;&gt;Environment variables only deliver values&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Environment variables solve a small problem: getting values into a running
process. The application can read &lt;code dir=&quot;auto&quot;&gt;DATABASE_URL&lt;/code&gt; without knowing whether a
developer, CI system, or secrets manager supplied it.&lt;/p&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; file makes those values easy to save and reload. That is useful. But
teams also use the file to describe what the application needs. &lt;code dir=&quot;auto&quot;&gt;KEY=value&lt;/code&gt;
cannot say whether a value is required, secret, safe to commit, available only
in production, or restricted to one service.&lt;/p&gt;
&lt;p&gt;Those requirements outlive any process and any developer laptop. They belong
in a durable project declaration. &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; stores values for delivery; it cannot
define the application’s secret model.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;SecretSpec separates the committed
&lt;a href=&quot;https://secretspec.dev/concepts/declarative/&quot;&gt;declaration&lt;/a&gt; from value
&lt;a href=&quot;https://secretspec.dev/concepts/providers/&quot;&gt;storage&lt;/a&gt; and delivery. The CLI and
&lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SDKs&lt;/a&gt; resolve the same declaration regardless of where values
live or how applications receive them.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;a-string-is-not-a-schema&quot;&gt;A string is not a schema&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Consider a typical example file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;REDIS_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;redis://localhost:6379&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;STRIPE_API_KEY&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DEBUG&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The file raises more questions than it answers. Does an empty value mean
required or optional? Is &lt;code dir=&quot;auto&quot;&gt;REDIS_URL&lt;/code&gt; a development default? Is
&lt;code dir=&quot;auto&quot;&gt;STRIPE_API_KEY&lt;/code&gt; production-only? Is &lt;code dir=&quot;auto&quot;&gt;DEBUG&lt;/code&gt; a boolean?&lt;/p&gt;
&lt;p&gt;Dotenv cannot encode those answers. Node.js documents that
&lt;a href=&quot;https://nodejs.org/api/environment_variables.html#variable-values&quot;&gt;every value becomes a
string&lt;/a&gt;.
A &lt;a href=&quot;https://github.com/motdotla/dotenv/issues/51&quot;&gt;dotenv issue about
booleans&lt;/a&gt;, opened in 2015, still
collects reactions from developers surprised that &lt;code dir=&quot;auto&quot;&gt;&quot;false&quot;&lt;/code&gt; is truthy.&lt;/p&gt;
&lt;p&gt;Teams put the missing information elsewhere: validation code, a README,
&lt;code dir=&quot;auto&quot;&gt;.env.example&lt;/code&gt;, or a teammate’s memory. These sources drift.&lt;/p&gt;
&lt;p&gt;The file also makes &lt;code dir=&quot;auto&quot;&gt;DEBUG&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;STRIPE_API_KEY&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Without an explicit declaration, missing values fail late: the application
discovers them only when code tries to use them.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;The &lt;a href=&quot;https://secretspec.dev/reference/configuration/&quot;&gt;SecretSpec declaration&lt;/a&gt; records names,
descriptions, required values, and safe defaults. &lt;code dir=&quot;auto&quot;&gt;secretspec check&lt;/code&gt; and
&lt;code dir=&quot;auto&quot;&gt;secretspec run&lt;/code&gt; validate those requirements before the application starts,
while ordinary settings such as &lt;code dir=&quot;auto&quot;&gt;DEBUG&lt;/code&gt; remain in application configuration.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;then-the-file-starts-to-multiply&quot;&gt;Then the file starts to multiply&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A new requirement usually creates another file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env.local&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env.development&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env.development.local&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env.test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.env.production&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The filenames become an environment model. Suffixes define scope, load order
defines inheritance, and copying a file becomes deployment.&lt;/p&gt;
&lt;p&gt;This reverses the
&lt;a href=&quot;https://12factor.net/config&quot;&gt;Twelve-Factor App’s guidance&lt;/a&gt;. Its point was that
environment variables should be independent controls because named
environments become brittle as deployments multiply. &lt;code dir=&quot;auto&quot;&gt;.env.production&lt;/code&gt;
recreates that grouping in a filename.&lt;/p&gt;
&lt;p&gt;Now every new value must be added to &lt;code dir=&quot;auto&quot;&gt;.env.example&lt;/code&gt;, documented in a README,
validated in code, and copied into the right real files. Miss one and the
environments drift.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;a href=&quot;https://secretspec.dev/concepts/profiles/&quot;&gt;Profiles&lt;/a&gt; express real requirement differences as sparse
overlays on &lt;code dir=&quot;auto&quot;&gt;profiles.default&lt;/code&gt;. Each deployment selects its values through
providers instead of maintaining a complete, copied secret file.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;there-is-no-env-spec&quot;&gt;There is no &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; spec&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; looks standardized, but every parser defines its own format.
&lt;a href=&quot;https://nodejs.org/api/environment_variables.html#env-files&quot;&gt;Node.js documents the lack of a formal
specification&lt;/a&gt;,
as does
&lt;a href=&quot;https://github.com/theskumar/python-dotenv#file-format&quot;&gt;python-dotenv&lt;/a&gt;. Each
loader makes its own choices.&lt;/p&gt;
&lt;p&gt;python-dotenv expands &lt;code dir=&quot;auto&quot;&gt;${NAME}&lt;/code&gt; but not &lt;code dir=&quot;auto&quot;&gt;$NAME&lt;/code&gt;. Node dotenv delegates &lt;a href=&quot;https://github.com/motdotla/dotenv#variable-expansion&quot;&gt;variable
expansion&lt;/a&gt; to another
tool. Docker Compose supports its own
&lt;a href=&quot;https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation&quot;&gt;shell-style operators&lt;/a&gt;.
Vite even supports references in reverse order, then
&lt;a href=&quot;https://main.vite.dev/guide/env-and-mode#expanding-variables-in-reverse-order&quot;&gt;warns&lt;/a&gt;
that the same expression will not work in a shell or Docker Compose.&lt;/p&gt;
&lt;p&gt;Comments and quotes differ too. Node dotenv changed the meaning of &lt;code dir=&quot;auto&quot;&gt;#&lt;/code&gt; in
unquoted values in version 15 as a &lt;a href=&quot;https://github.com/motdotla/dotenv#comments&quot;&gt;breaking
change&lt;/a&gt;. One devenv user found
that
&lt;a href=&quot;https://github.com/cachix/devenv/issues/1333&quot;&gt;quotes became part of an exported
key&lt;/a&gt;.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;SecretSpec defines one TOML declaration and one resolution model shared by its
CLI and &lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SDKs&lt;/a&gt;. Dotenv parsing is confined to the
&lt;a href=&quot;https://secretspec.dev/providers/dotenv/&quot;&gt;compatibility provider&lt;/a&gt;, so changing loaders or storage
backends does not change the application’s declaration.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;which-value-wins&quot;&gt;Which value wins?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Parsers also disagree about precedence.
&lt;a href=&quot;https://github.com/motdotla/dotenv#path&quot;&gt;Node dotenv&lt;/a&gt; normally lets the first
file win. &lt;a href=&quot;https://github.com/compose-spec/compose-spec/blob/main/spec.md#env_file&quot;&gt;Docker
Compose&lt;/a&gt;
lets the last &lt;code dir=&quot;auto&quot;&gt;env_file&lt;/code&gt; win, then lets the &lt;code dir=&quot;auto&quot;&gt;environment&lt;/code&gt; section override
that. &lt;a href=&quot;https://main.vite.dev/guide/env-and-mode#env-loading-priorities&quot;&gt;Vite&lt;/a&gt;
gives an existing process variable priority over its files.&lt;/p&gt;
&lt;p&gt;Docker Compose gives two similar names different behavior. &lt;code dir=&quot;auto&quot;&gt;env_file:&lt;/code&gt; supplies
variables to a container but does not use them to interpolate &lt;code dir=&quot;auto&quot;&gt;compose.yaml&lt;/code&gt;.
&lt;code dir=&quot;auto&quot;&gt;docker compose --env-file&lt;/code&gt; does affect interpolation. In &lt;a href=&quot;https://github.com/docker/compose/issues/9443&quot;&gt;an issue closed as
working as
designed&lt;/a&gt;, a maintainer described
the option’s name as unfortunately chosen.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;SecretSpec applies one deterministic &lt;a href=&quot;https://secretspec.dev/concepts/providers/fallback/#provider-selection-order&quot;&gt;provider resolution
order&lt;/a&gt;. Per-secret
routes and fallbacks are explicit in the declaration, and the same resolver
applies them across the CLI and SDKs.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;who-loaded-env-first&quot;&gt;Who loaded &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; first?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Precedence also depends on timing. Node dotenv’s &lt;a href=&quot;https://github.com/motdotla/dotenv/blob/94f6542d5c8b1ab211cab0dcd8f7aa907dd39124/README.md#L406-L435&quot;&gt;ES module
guidance&lt;/a&gt;
needs special handling when imported modules read the environment during
initialization. Vite warns that Bun’s automatic &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; loading can interfere
with &lt;a href=&quot;https://main.vite.dev/guide/env-and-mode#env-files&quot;&gt;Vite’s own loading
order&lt;/a&gt;. &lt;code dir=&quot;auto&quot;&gt;VITE_*&lt;/code&gt; values are
replaced at build time and become part of the
&lt;a href=&quot;https://main.vite.dev/guide/env-and-mode#env-variables&quot;&gt;client bundle&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;At that point &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; behaves like a small program, with control flow spread
across filenames, flags, working directories, parent processes, and library
versions.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;a href=&quot;https://secretspec.dev/reference/cli/#run&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec run&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;an-ignored-file-is-still-a-file&quot;&gt;An ignored file is still a file&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The dotenv project says &lt;a href=&quot;https://github.com/motdotla/dotenv#should-i-commit-my-env-file&quot;&gt;not to commit
&lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt;&lt;/a&gt;.
&lt;code dir=&quot;auto&quot;&gt;.gitignore&lt;/code&gt; prevents one accident. It does not add encryption, access control,
auditing, or revocation.&lt;/p&gt;
&lt;p&gt;The file can still end up in editor backups, chat messages, archives, support
bundles, container build contexts, and old laptops. A devenv integration was
&lt;a href=&quot;https://github.com/cachix/devenv/issues/1694&quot;&gt;reported to copy &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; contents into the Nix
store&lt;/a&gt;, where paths are not
confidential. When a developer leaves, there is no file access to revoke.
Each credential they received is a separate copy.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Environment-variable delivery has limits too. Docker mounts managed secrets as
files because environment variables can
&lt;a href=&quot;https://docs.docker.com/engine/swarm/secrets/#build-support-for-docker-secrets-into-your-images&quot;&gt;leak between
containers&lt;/a&gt;.
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.&lt;/p&gt;
&lt;aside aria-label=&quot;How does SecretSpec solve this?&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;How does SecretSpec solve this?&lt;/p&gt;&lt;div&gt;&lt;p&gt;The committed declaration contains no secret values. Providers supply storage,
encryption, identity, and access control, while the
&lt;a href=&quot;https://secretspec.dev/concepts/audit/&quot;&gt;metadata-only audit log&lt;/a&gt; records local access. &lt;a href=&quot;https://secretspec.dev/concepts/scopes/&quot;&gt;Scopes
(0.17+)&lt;/a&gt; let each service or command resolve only its
declared subset.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;let-env-become-small-again&quot;&gt;Let &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; become small again&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The useful part of &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; is the short path from “this application needs a
value” to “the application can run.”&lt;/p&gt;
&lt;p&gt;Keep it as an adapter for tools that expect &lt;code dir=&quot;auto&quot;&gt;KEY=value&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;A durable design separates three jobs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a committed declaration says which secrets the application needs;&lt;/li&gt;
&lt;li&gt;protected storage controls who can read their values;&lt;/li&gt;
&lt;li&gt;explicit delivery gives each process only the values it needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-secretspec-applies-this&quot;&gt;How SecretSpec applies this&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SecretSpec puts the declaration in a file that is safe to commit:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[project]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;payments&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;revision&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;1.0&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Postgres connection string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;REDIS_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Redis connection string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;STRIPE_API_KEY&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Stripe API key&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.development]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;REDIS_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;redis://localhost:6379&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This file records requirements, defaults, and descriptions without containing
secret values. &lt;a href=&quot;https://secretspec.dev/concepts/providers/&quot;&gt;Providers&lt;/a&gt; choose where values live, and
&lt;a href=&quot;https://secretspec.dev/concepts/profiles/&quot;&gt;profiles&lt;/a&gt; describe real differences in requirements.&lt;/p&gt;
&lt;p&gt;Existing programs can adopt SecretSpec without code changes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This command injects resolved secrets into the child process environment. It is
useful during migration, while the preferred integration is a
&lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SecretSpec SDK&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With an SDK, the application resolves its declaration directly. This removes
the environment-variable handoff used by &lt;code dir=&quot;auto&quot;&gt;secretspec run&lt;/code&gt;. Values stored in a
keyring, password manager, or Vault never enter the global process environment.
Applications that require a file can receive a
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#as_path-option&quot;&gt;temporary file&lt;/a&gt; instead. &lt;a href=&quot;https://secretspec.dev/concepts/scopes/&quot;&gt;Scopes
(0.17+)&lt;/a&gt; let each component resolve only the secrets it
declares.&lt;/p&gt;
&lt;p&gt;Migration can be gradual. SecretSpec initializes a declaration from an existing
file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dotenv:.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This copies names without copying values. The current file can remain a
provider during the transition:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dotenv:.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dotenv:.env&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Values can then move to a system keyring, password manager, Vault, or another
provider without changing the names the application reads.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SecretSpec aims to eliminate environment variables for secrets altogether.&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.17: Scopes, secrets caching, SOPS, age, and systemd credentials</title><link>https://secretspec.dev/blog/secretspec-0-17-scopes-secrets-caching-age-and-systemd-credentials/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-17-scopes-secrets-caching-age-and-systemd-credentials/</guid><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.17.0&quot; title=&quot;SecretSpec 0.17 release&quot;&gt;SecretSpec 0.17&lt;/a&gt;
ships:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/concepts/scopes/&quot;&gt;Scopes&lt;/a&gt;&lt;/strong&gt;: let each service or task resolve
only its declared subset of a profile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/concepts/providers/caching/&quot;&gt;Secrets caching&lt;/a&gt;&lt;/strong&gt;: cache a slow fallback
route in a local secret store with bounded freshness and explicit
invalidation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;#cross-secret-validation&quot;&gt;Cross-secret validation&lt;/a&gt;&lt;/strong&gt;:
declare alternative or mutually exclusive credentials instead of marking
every value independently required or optional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;#github-and-forgejo-actions&quot;&gt;GitHub and Forgejo Actions&lt;/a&gt;&lt;/strong&gt;: resolve a
profile in one workflow step and expose its secrets to the steps that follow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;#new-providers&quot;&gt;New providers&lt;/a&gt;&lt;/strong&gt;: now with 20 providers, use
&lt;a href=&quot;https://secretspec.dev/providers/sops/&quot;&gt;SOPS&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/providers/age/&quot;&gt;age&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/providers/kdbx/&quot;&gt;KeePass
KDBX&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/providers/openbao/&quot;&gt;OpenBao&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/providers/scaleway/&quot;&gt;Scaleway Secret
Manager&lt;/a&gt;, and &lt;a href=&quot;https://secretspec.dev/providers/systemd-credential/&quot;&gt;systemd
credentials&lt;/a&gt; through the same SecretSpec
interface, with JWT/OIDC authentication for &lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt; and
&lt;a href=&quot;https://secretspec.dev/providers/openbao/&quot;&gt;OpenBao&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;scopes&quot;&gt;Scopes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A profile describes how secrets resolve for an environment. A
&lt;a href=&quot;https://secretspec.dev/concepts/scopes/&quot;&gt;scope&lt;/a&gt; now describes which of those secrets one consumer
may receive:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Database&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;API_KEY&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;API key&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;QUEUE_TOKEN&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Queue token&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[scopes.api]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;API_KEY&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[scopes.worker]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;QUEUE_TOKEN&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--scope&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./api&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--scope&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;worker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./worker&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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 &lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;export&lt;/code&gt;, and the &lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SDK
builders&lt;/a&gt;. Scopes minimize secret delivery; they are not an
authorization boundary when the child itself holds provider credentials.&lt;/p&gt;
&lt;p&gt;Carrying the selected scope through resolver requests and results required a
breaking change to
&lt;a href=&quot;https://github.com/cachix/secretspec/tree/main/secretspec-ffi&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec-ffi&lt;/code&gt;&lt;/a&gt;.
All &lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SecretSpec SDKs&lt;/a&gt; have been updated for 0.17 to support
scopes, so applications should upgrade their SDK package and bundled native
resolver together.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;secrets-caching&quot;&gt;Secrets caching&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Many cloud providers take long enough to resolve a secret that their latency
becomes part of every development command.&lt;/p&gt;
&lt;p&gt;A single &lt;strong&gt;1Password&lt;/strong&gt; lookup can take &lt;strong&gt;roughly one second&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;SecretSpec providers implement &lt;code dir=&quot;auto&quot;&gt;get_many&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Waiting on a remote service or its CLI every time makes &lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt;, and
application startup feel slow, especially as a project grows.&lt;/p&gt;
&lt;p&gt;A provider alias can now combine its authoritative fallback route with a local
cache:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;vault&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vault://vault.example.com:8200/secret&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://secretspec/cache/{project}/{profile}/{key}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fast_vault&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;fallback&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vault&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;max_age&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;8h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default.defaults]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;providers&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;fast_vault&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Fresh entries avoid contacting the remote provider. A miss or expired entry
falls through to &lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;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.
&lt;code dir=&quot;auto&quot;&gt;secretspec cache clear [NAME]&lt;/code&gt; forces the next read back through the
authoritative route.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt; and &lt;a href=&quot;https://secretspec.dev/providers/openbao/&quot;&gt;OpenBao&lt;/a&gt; KV v2 caches are
the only providers that handle &lt;code dir=&quot;auto&quot;&gt;max_age&lt;/code&gt; as server-side expiry properly.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Our planned &lt;a href=&quot;https://github.com/domenkozar/factorseal&quot;&gt;FactorSeal&lt;/a&gt;
provider in &lt;a href=&quot;#future-work&quot;&gt;Future work&lt;/a&gt; is intended to close that gap with an
explicit API for credential eviction among the other goals.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;cross-secret-validation&quot;&gt;Cross-secret validation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Profiles can now express credential alternatives directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PASSWORD&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;at_least_one&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Token&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;at_least_one&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GitHub token&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;exactly_one&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;github_auth&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GITHUB_APP_KEY&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GitHub App private key&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;exactly_one&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;github_auth&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;auth&lt;/code&gt; group accepts a password, an access token, or both. The
&lt;code dir=&quot;auto&quot;&gt;github_auth&lt;/code&gt; group requires exactly one credential and rejects configurations
that provide both the token and the app key.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;new-providers&quot;&gt;New providers&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/sops/&quot;&gt;SOPS&lt;/a&gt;&lt;/strong&gt; brings the encrypted-file workflow from our recent
&lt;a href=&quot;https://secretspec.dev/blog/but-i-use-sops/&quot;&gt;SOPS comparison&lt;/a&gt; behind SecretSpec’s
provider-independent CLI and SDKs. SecretSpec delegates encryption and
decryption to the installed SOPS CLI, so existing SOPS key services and
&lt;code dir=&quot;auto&quot;&gt;.sops.yaml&lt;/code&gt; creation rules remain in control.&lt;/p&gt;
&lt;p&gt;The provider reads and writes YAML, JSON, dotenv, and INI files, supports a
single shared file or &lt;code dir=&quot;auto&quot;&gt;{project}&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;{profile}&lt;/code&gt; path templates, and can source
sensitive SOPS inputs such as age keys or cloud credentials through provider
credentials.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sops&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sops://secrets/{project}/{profile}.enc.yaml&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.production.defaults]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;providers&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sops&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/age/&quot;&gt;age&lt;/a&gt;&lt;/strong&gt; offers a smaller encrypted-file setup. It stores a
dotenv-style secret set for one or more age recipients, including hybrid
post-quantum recipients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/kdbx/&quot;&gt;KeePass KDBX&lt;/a&gt;&lt;/strong&gt; reads KDBX 3 and 4 databases and writes
KDBX 4, with master passwords sourced from another provider rather than
embedded in the URI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/openbao/&quot;&gt;OpenBao&lt;/a&gt;&lt;/strong&gt; gets its own &lt;code dir=&quot;auto&quot;&gt;openbao://&lt;/code&gt; identity and
&lt;code dir=&quot;auto&quot;&gt;BAO_*&lt;/code&gt; configuration while sharing compatible KV, token, AppRole, and JWT
mechanics with &lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt;. 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 &lt;code dir=&quot;auto&quot;&gt;id-token: write&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/scaleway/&quot;&gt;Scaleway Secret Manager&lt;/a&gt;&lt;/strong&gt; adds regional,
project-aware cloud storage and read-only references to existing secrets and
revisions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/systemd-credential/&quot;&gt;systemd credentials&lt;/a&gt;&lt;/strong&gt; is a read-only
provider that resolves values from the current service’s
&lt;code dir=&quot;auto&quot;&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt;, including credentials used to bootstrap another
provider.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;github-and-forgejo-actions&quot;&gt;GitHub and Forgejo Actions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Alongside 0.17, the new
&lt;a href=&quot;https://github.com/cachix/secretspec-action&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;cachix/secretspec-action&lt;/code&gt;&lt;/a&gt;
installs SecretSpec, resolves the selected profile, masks every value in the
runner log, and adds the secrets to the environment of later job steps:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;deploy&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;runs-on&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ubuntu-latest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;actions/checkout@v7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;cachix/secretspec-action@main&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;./deploy.sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A missing required secret fails the action, so the same step also checks that
the deployment environment is complete. See the &lt;a href=&quot;https://secretspec.dev/ci/github-actions/&quot;&gt;GitHub Actions
guide&lt;/a&gt; for provider selection and tokenless
&lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt; or &lt;a href=&quot;https://secretspec.dev/providers/openbao/&quot;&gt;OpenBao&lt;/a&gt; authentication
through the runner’s OIDC identity.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.17.0&quot;&gt;SecretSpec 0.17&lt;/a&gt;
also brings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/bws/&quot;&gt;Bitwarden Secrets Manager&lt;/a&gt;&lt;/strong&gt; — now uses the separately
installed official &lt;code dir=&quot;auto&quot;&gt;bws&lt;/code&gt; CLI instead of linking its SDK.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-interactive setup&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;secretspec config global init --provider ... --profile ...&lt;/code&gt; configures defaults without prompts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows packages&lt;/strong&gt; — for the Python, Ruby, and PHP SDKs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clearer status output&lt;/strong&gt; — plus more controlled concurrency and retry
behavior for Vault and OpenBao.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;See the &lt;a href=&quot;https://github.com/cachix/secretspec/blob/main/CHANGELOG.md&quot;&gt;full changelog&lt;/a&gt;
for every change in this release.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;future-work&quot;&gt;Future work&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The next work brings more control to local secret access:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/pull/122&quot;&gt;GUI confirmation
dialogs&lt;/a&gt;&lt;/strong&gt; — approve or deny a
secret request in a native prompt instead of requiring a terminal
interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;a href=&quot;https://github.com/cachix/secretspec/pull/127&quot;&gt;Passbolt provider&lt;/a&gt;&lt;/strong&gt; —
bring Passbolt’s open-source, collaboration-focused credential manager
behind the same SecretSpec interface for cloud and self-hosted teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;a href=&quot;https://github.com/cachix/secretspec/pull/166&quot;&gt;Bitwarden Password Manager
provider&lt;/a&gt;&lt;/strong&gt; — resolve regular
Bitwarden vault items, separately from the Bitwarden Secrets Manager provider
already available in SecretSpec.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A JVM SDK&lt;/strong&gt; — work is underway to bring the shared SecretSpec resolver to
Java, Kotlin, and other JVM languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;a href=&quot;https://github.com/domenkozar/factorseal&quot;&gt;FactorSeal&lt;/a&gt; provider&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Questions or feedback? Join us on
&lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>But I Use SOPS</title><link>https://secretspec.dev/blog/but-i-use-sops/</link><guid isPermaLink="true">https://secretspec.dev/blog/but-i-use-sops/</guid><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Whenever I show someone SecretSpec, I often hear the same response:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But I use SOPS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://getsops.io/docs/&quot;&gt;SOPS&lt;/a&gt; is good. It encrypts files so they can
live in Git without exposing their plaintext values.&lt;/p&gt;
&lt;p&gt;But SecretSpec solves a different problem: how applications declare, find, and
consume secrets.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-does-your-application-use-the-secret&quot;&gt;How does your application use the secret?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once you have encrypted &lt;code dir=&quot;auto&quot;&gt;secrets.yaml&lt;/code&gt;, how does your Python service consume
it? What about your Go worker or Node.js app?&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;SecretSpec starts at the other end. The project
&lt;a href=&quot;https://secretspec.dev/concepts/declarative/&quot;&gt;declares what the application needs&lt;/a&gt; without storing
any values:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[project]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;payments&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;revision&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;1.0&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Postgres connection string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;STRIPE_API_KEY&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Stripe secret key&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The same secret can come from a developer’s
&lt;a href=&quot;https://secretspec.dev/providers/keyring/&quot;&gt;system keyring&lt;/a&gt; or CI
&lt;a href=&quot;https://secretspec.dev/providers/env/&quot;&gt;environment variables&lt;/a&gt;, while a more sensitive production
environment resolves it from &lt;a href=&quot;https://secretspec.dev/providers/vault/&quot;&gt;Vault&lt;/a&gt;. Applications use the
same declaration through eight SDKs for &lt;a href=&quot;https://secretspec.dev/sdk/rust/&quot;&gt;Rust&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/sdk/python/&quot;&gt;Python&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/sdk/go/&quot;&gt;Go&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/sdk/ruby/&quot;&gt;Ruby&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/sdk/nodejs/&quot;&gt;Node.js/TypeScript&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/sdk/haskell/&quot;&gt;Haskell&lt;/a&gt;, &lt;a href=&quot;https://secretspec.dev/sdk/php/&quot;&gt;PHP&lt;/a&gt;, and &lt;a href=&quot;https://secretspec.dev/sdk/csharp/&quot;&gt;C#&lt;/a&gt; without
knowing the provider.&lt;/p&gt;
&lt;p&gt;Encrypted files also make the key workflow a project-wide requirement. Adding a
teammate means adding their key to &lt;code dir=&quot;auto&quot;&gt;.sops.yaml&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;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 &lt;a href=&quot;https://secretspec.dev/concepts/providers/&quot;&gt;provider configuration&lt;/a&gt;; SDK
calls and secret names stay the same.&lt;/p&gt;
&lt;p&gt;The same resolver provides &lt;a href=&quot;https://secretspec.dev/concepts/profiles/&quot;&gt;profiles&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#secret-variable-options&quot;&gt;required-secret checks&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/concepts/providers/fallback/&quot;&gt;per-secret provider routing and fallback&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/concepts/references/&quot;&gt;provider-native references&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#as_path-option&quot;&gt;temporary files&lt;/a&gt;, and
&lt;a href=&quot;https://secretspec.dev/concepts/audit/&quot;&gt;metadata-only audit logs&lt;/a&gt;. You build the integration once,
not once per provider and language.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;different-layers-different-jobs&quot;&gt;Different layers, different jobs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SOPS protects a file. SecretSpec gives applications a provider-independent
interface. The selected provider remains responsible for storage, encryption,
identity, access control, and availability.&lt;/p&gt;
&lt;p&gt;I wrote a fuller &lt;a href=&quot;https://secretspec.dev/comparison/&quot;&gt;SecretSpec comparison&lt;/a&gt; showing exactly where
SecretSpec ends, where providers begin, and which responsibilities belong to
each layer.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-secretspec-goes-next&quot;&gt;Where SecretSpec goes next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/issues/188&quot;&gt;Project security requirements&lt;/a&gt;
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.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/issues/11&quot;&gt;Lease-aware refresh&lt;/a&gt; would
let running applications follow key rotation and short-lived credentials
instead of restarting for a new value.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://secretspec.dev/providers/sops/&quot;&gt;SOPS provider&lt;/a&gt; (0.17+) brings SOPS itself behind the
same SDK interface, making your encrypted files one more place secrets can
come from.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that provider, perhaps “But I use SOPS” just needs two more words:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But I use SOPS with SecretSpec.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If encrypted files fit your workflow, keep using SOPS. Just recognize the
boundary: encryption at rest is not an application secrets interface.&lt;/p&gt;</content:encoded></item><item><title>Secrets Don’t Belong in Config</title><link>https://secretspec.dev/blog/secrets-dont-belong-in-config/</link><guid isPermaLink="true">https://secretspec.dev/blog/secrets-dont-belong-in-config/</guid><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Applications should not require passwords, API keys, or tokens in their
configuration files.&lt;/p&gt;
&lt;p&gt;Configuration describes behavior. It belongs in git, code review, bug reports,
and developer machines.&lt;/p&gt;
&lt;p&gt;A secret grants authority. It needs restricted access and independent rotation.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;nixos-contains-110-workarounds-for-this&quot;&gt;NixOS contains 110 workarounds for this&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We &lt;a href=&quot;https://github.com/NixOS/nixpkgs/issues/24288#issuecomment-5024009774&quot;&gt;audited all 445 NixOS modules that handle a real secret&lt;/a&gt;
in nixpkgs at commit &lt;code dir=&quot;auto&quot;&gt;141f212&lt;/code&gt;, classifying each by where its secret value ends
up.&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Where the secret value ends up&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;Modules&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;Share&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Merged into a config file at runtime&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;110&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;25%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Inlined into a config in &lt;code dir=&quot;auto&quot;&gt;/nix/store&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;42&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;9%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Delivered as an environment variable&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;161&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;36%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Left in a dedicated file opened by the app&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;58&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;13%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Loaded through systemd credentials&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;53&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;12%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Passed as a command-line argument&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;19&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;4%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Classification uncertain&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;These modules use &lt;code dir=&quot;auto&quot;&gt;envsubst&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;replace-secret&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;yq&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sed&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;As a side note, 42 modules can inline secrets into the world-readable
&lt;code dir=&quot;auto&quot;&gt;/nix/store&lt;/code&gt;. That direct security problem is tracked in
&lt;a href=&quot;https://github.com/NixOS/nixpkgs/issues/24288&quot;&gt;nixpkgs issue #24288&lt;/a&gt;. The 110
runtime mergers make the broader point: even when deployment authors avoid the
leak, the missing separation still creates work.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;give-secrets-their-own-interface&quot;&gt;Give secrets their own interface&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Applications should accept secret values through a dedicated runtime channel,
such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code dir=&quot;auto&quot;&gt;password_file&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;token_file&lt;/code&gt; setting;&lt;/li&gt;
&lt;li&gt;a systemd credential;&lt;/li&gt;
&lt;li&gt;a narrowly scoped environment variable;&lt;/li&gt;
&lt;li&gt;or an external secret provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-i-got-it-wrong-in-cachix&quot;&gt;How I got it wrong in Cachix&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Cachix historically stored its auth token and per-cache signing keys in
&lt;code dir=&quot;auto&quot;&gt;~/.config/cachix/cachix.dhall&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;A typical file mixed them directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;~/.config/cachix/cachix.dhall&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{ authToken = &quot;XXX-AUTH-TOKEN&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;, binaryCaches =&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[ { name = &quot;mycache&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;, secretKey = &quot;XXX-SIGNING-KEY&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The cache name is configuration; the auth token and signing key are secrets.
You could not share the cache configuration without also sharing credentials.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://devenv.sh/binary-caching/#setup-with-secretspec-recommended&quot;&gt;devenv 2.2 separates the token through SecretSpec&lt;/a&gt;.
The project declares &lt;code dir=&quot;auto&quot;&gt;CACHIX_AUTH_TOKEN&lt;/code&gt;, devenv resolves it from the configured
provider, and the value is passed to Cachix without being added to devenv’s
configuration.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/cachix/pull/737&quot;&gt;Cachix PR #737&lt;/a&gt; brings the same
boundary into the client through the SecretSpec Haskell SDK. It resolves
&lt;code dir=&quot;auto&quot;&gt;CACHIX_AUTH_TOKEN&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;CACHIX_SIGNING_KEY&lt;/code&gt; from SecretSpec and can store them
in the user’s chosen provider instead of &lt;code dir=&quot;auto&quot;&gt;cachix.dhall&lt;/code&gt;. Existing environment
variables and config files remain higher-priority fallbacks for compatibility.
The PR is still open.&lt;/p&gt;
&lt;p&gt;That is the problem SecretSpec is designed to solve: configuration declares the
requirement, while each environment chooses where the value lives.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;declare-once-resolve-anywhere&quot;&gt;Declare once, resolve anywhere&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SecretSpec applies that separation by making &lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt; a declaration of
what an application needs, without storing the values:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[project]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myapp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.production]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Postgres connection string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;STRIPE_API_KEY&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Stripe secret key&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://secretspec.dev/concepts/providers/&quot;&gt;Providers&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;An existing application can receive the resolved values at startup:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./myapp&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Applications can also resolve them directly through the
&lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SecretSpec SDKs&lt;/a&gt; for Rust, Python, Go, Ruby,
Node.js/TypeScript, Haskell, PHP, and C#, all sharing the same resolver
so behavior stays consistent across languages.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;making-the-boundary-practical&quot;&gt;Making the boundary practical&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you maintain an application, stop adding passwords and tokens to ordinary
configuration schemas. Accept a file reference, credential, environment
variable, or provider instead.&lt;/p&gt;
&lt;p&gt;For NixOS, &lt;a href=&quot;https://github.com/cachix/secretspec/issues/65&quot;&gt;SecretSpec issue #65&lt;/a&gt;
tracks how an official integration could declare and resolve secrets without
per-module substitution glue.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.16: Composed secrets, Infisical, and C# SDK</title><link>https://secretspec.dev/blog/secretspec-0-16-composed-secrets-infisical-and-csharp-sdk/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-16-composed-secrets-infisical-and-csharp-sdk/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.16.0&quot; title=&quot;SecretSpec 0.16 release&quot;&gt;SecretSpec 0.16&lt;/a&gt;
ships:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/concepts/composed-secrets/&quot;&gt;Composed secrets&lt;/a&gt;&lt;/strong&gt; — derive a read-only
value, such as a connection string, from other secrets declared in the
manifest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/infisical/&quot;&gt;Infisical&lt;/a&gt;&lt;/strong&gt; — read and write secrets in Infisical
Cloud or a self-hosted instance, with Universal Auth, access-token, and
provider-credential authentication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/sdk/csharp/&quot;&gt;C# SDK&lt;/a&gt;&lt;/strong&gt; — resolve the same manifests from .NET through the
shared native resolver, distributed as the &lt;code dir=&quot;auto&quot;&gt;Cachix.SecretSpec&lt;/code&gt; NuGet package.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;composed-secrets&quot;&gt;Composed secrets&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DB_USER&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Database user&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DB_PASSWORD&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Database password&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DB_HOST&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Database host&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PostgreSQL connection string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;composed&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;DB_USER&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DB_PASSWORD&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;DB_HOST&lt;/code&gt; still come from their configured
providers. &lt;code dir=&quot;auto&quot;&gt;DATABASE_URL&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;https://secretspec.dev/concepts/composed-secrets/&quot;&gt;Composed Secrets&lt;/a&gt; for optional values,
escaping, profile inheritance, and validation rules.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;infisical&quot;&gt;Infisical&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;infisical://&lt;/code&gt; provider works with Infisical Cloud, its EU service, and
self-hosted instances. Point SecretSpec at an Infisical project and authenticate
with Universal Auth:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;INFISICAL_CLIENT_ID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;INFISICAL_CLIENT_SECRET&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;infisical://app.infisical.com/7e2f1a4c-...?env=prod&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Access tokens are also supported. Credentials can come from environment
variables or SecretSpec’s
&lt;a href=&quot;https://secretspec.dev/concepts/providers/#provider-credentials&quot;&gt;provider credentials&lt;/a&gt;, allowing,
for example, an Infisical machine identity to be kept in the system keyring:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.infisical]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;infisical://app.infisical.com/7e2f1a4c-...&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.infisical.credentials]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client_id&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client_secret&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;By default, the active SecretSpec profile also names the Infisical environment.
A &lt;code dir=&quot;auto&quot;&gt;production&lt;/code&gt; profile therefore reads from the &lt;code dir=&quot;auto&quot;&gt;production&lt;/code&gt; environment,
while &lt;code dir=&quot;auto&quot;&gt;?env=&lt;/code&gt; can select a different one. The provider supports normal
SecretSpec reads and writes, as well as references to existing Infisical
secrets and versions.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://secretspec.dev/providers/infisical/&quot;&gt;Infisical provider guide&lt;/a&gt; for self-hosting,
authentication, paths, references, and permissions.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;c-sdk&quot;&gt;C# SDK&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;Cachix.SecretSpec&lt;/code&gt; NuGet package brings the shared SecretSpec resolver to
.NET 8:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Cachix.SecretSpec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Cachix&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; resolved &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Builder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WithProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WithProfile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WithReason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WriteLine&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Secrets&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetAsEnv&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://secretspec.dev/sdk/csharp/&quot;&gt;C# SDK guide&lt;/a&gt; for supported platforms, ASP.NET Core
integration, preflight reports, error handling, and typed access.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;All three additions are opt-in: existing manifests and provider configurations
continue to work unchanged. Add &lt;code dir=&quot;auto&quot;&gt;composed&lt;/code&gt; when a value should be derived,
select an &lt;code dir=&quot;auto&quot;&gt;infisical://&lt;/code&gt; provider to use Infisical, or install
&lt;code dir=&quot;auto&quot;&gt;Cachix.SecretSpec&lt;/code&gt; in a .NET application.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://github.com/cachix/secretspec/blob/main/CHANGELOG.md&quot;&gt;full changelog&lt;/a&gt;
for every change in this release.&lt;/p&gt;
&lt;p&gt;Questions or feedback? Join us on
&lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.15: Provider credentials, Azure Key Vault / Gopass, and PHP SDK</title><link>https://secretspec.dev/blog/secretspec-0-15-provider-credentials-azure-key-vault-gopass-and-php-sdk/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-15-provider-credentials-azure-key-vault-gopass-and-php-sdk/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.15.0&quot; title=&quot;SecretSpec 0.15 release&quot;&gt;SecretSpec 0.15&lt;/a&gt;
ships:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/concepts/providers/#provider-credentials&quot;&gt;Provider credentials&lt;/a&gt;&lt;/strong&gt; —
authenticate one secret provider with credentials stored in another, without
exporting them to the application environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/akv/&quot;&gt;Azure Key Vault&lt;/a&gt;&lt;/strong&gt; — store and resolve secrets with
service-principal, Azure CLI, managed-identity, or AKS workload-identity
authentication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/gopass/&quot;&gt;Gopass&lt;/a&gt;&lt;/strong&gt; — use a GPG-encrypted, git-synchronized
password store, including multi-user and multi-store setups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/sdk/php/&quot;&gt;PHP SDK&lt;/a&gt;&lt;/strong&gt; — use the shared SecretSpec resolver from PHP-FPM,
Laravel, Symfony, and CLI applications through a native extension or
&lt;code dir=&quot;auto&quot;&gt;ext-ffi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/providers/awssm/&quot;&gt;AWS creation guardrails&lt;/a&gt;&lt;/strong&gt; — set a customer-managed KMS
key and required tags when SecretSpec creates an AWS Secrets Manager secret.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/reference/cli/#export&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec export&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — resolve secrets without
launching a command, with shell, dotenv, JSON, and GitHub Actions output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://secretspec.dev/concepts/providers/&quot;&gt;Provider and resolution fixes&lt;/a&gt;&lt;/strong&gt; — ordered lazy
fallback chains, early &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; validation, correctly merged profile overrides,
stable output, and broader Node.js Linux compatibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;credentials-for-the-secret-store&quot;&gt;Credentials for the secret store&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Suppose &lt;a href=&quot;https://secretspec.dev/providers/bws/&quot;&gt;Bitwarden Secrets Manager&lt;/a&gt; holds an application’s
secrets, but its machine access token is kept in the user’s
&lt;a href=&quot;https://secretspec.dev/providers/keyring/&quot;&gt;OS keyring&lt;/a&gt;. Declare the relationship on the provider
alias:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.bws]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;bws://a9230ec4-5507-4870-b8b5-b3f500587e4c&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.bws.credentials]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;access_token&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Before SecretSpec connects to Bitwarden, it reads &lt;code dir=&quot;auto&quot;&gt;access_token&lt;/code&gt; from the
keyring at the normal &lt;code dir=&quot;auto&quot;&gt;{project}/{profile}/access_token&lt;/code&gt; address. The active
&lt;a href=&quot;https://secretspec.dev/concepts/profiles/&quot;&gt;profile&lt;/a&gt; is part of that address, so production and
development can authenticate as different machines without changing the alias.&lt;/p&gt;
&lt;p&gt;When a credential already has a provider-native address, use a &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;. Here a
&lt;a href=&quot;https://secretspec.dev/providers/vault/#approle-authentication&quot;&gt;Vault AppRole&lt;/a&gt; is kept as two fields of one
&lt;a href=&quot;https://secretspec.dev/providers/onepassword/#use-existing-secrets&quot;&gt;1Password item&lt;/a&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.vault_prod]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vault://secret/myapp?auth=approle&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.vault_prod.credentials]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;role_id.provider&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;onepassword&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;role_id.ref.vault&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Infra&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;role_id.ref.item&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vault-approle&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;role_id.ref.field&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;role_id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secret_id.provider&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;onepassword&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secret_id.ref.vault&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Infra&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secret_id.ref.item&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vault-approle&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secret_id.ref.field&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;secret_id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The credential source uses the same &lt;a href=&quot;https://secretspec.dev/concepts/references/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; coordinates&lt;/a&gt;
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
&lt;a href=&quot;https://secretspec.dev/reference/cli/#run&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec run&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Provider credential names are semantic and checked before a source is opened.
Bitwarden accepts &lt;code dir=&quot;auto&quot;&gt;access_token&lt;/code&gt;; Vault accepts &lt;code dir=&quot;auto&quot;&gt;token&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;role_id&lt;/code&gt;, and
&lt;code dir=&quot;auto&quot;&gt;secret_id&lt;/code&gt;; 1Password accepts &lt;code dir=&quot;auto&quot;&gt;service_account_token&lt;/code&gt;; Azure Key Vault accepts
&lt;code dir=&quot;auto&quot;&gt;tenant_id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;client_id&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;client_secret&lt;/code&gt;. A configured credential is
authoritative, while a provider’s usual environment fallback remains available
when no credential source is declared.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;log-in-once-without-an-environment-variable&quot;&gt;Log in once, without an environment variable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new
&lt;a href=&quot;https://secretspec.dev/reference/cli/#config-provider-login&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;config provider login&lt;/code&gt;&lt;/a&gt; command
prompts for every credential an alias declares and writes it to the configured
source:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ secretspec config provider login bws&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Enter access_token for provider &apos;bws&apos; (source: keyring): ****&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ stored access_token in keyring at my-app/default/access_token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A user-level alias and its credential source can also be declared entirely with
&lt;a href=&quot;https://secretspec.dev/reference/cli/#config-provider-add&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;config provider add&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bws&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;bws://project-uuid&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--credential&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;access_token=keyring&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bws&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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
&lt;code dir=&quot;auto&quot;&gt;login&lt;/code&gt;, gets an &lt;a href=&quot;https://secretspec.dev/concepts/audit/&quot;&gt;audit event&lt;/a&gt; marked with the semantic
credential name and source store. As with every SecretSpec audit event, the
credential value is never recorded.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;https://secretspec.dev/concepts/providers/#provider-credentials&quot;&gt;Provider Credentials&lt;/a&gt; for the
full configuration and resolution rules.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;azure-key-vault&quot;&gt;Azure Key Vault&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Azure Key Vault joins the provider list with the &lt;code dir=&quot;auto&quot;&gt;akv://&lt;/code&gt; scheme:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Use service-principal credentials, or the current Azure CLI session&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;akv://myvault&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Use the platform&apos;s managed identity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;akv://myvault?auth=managed_identity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Use AKS workload identity federation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;akv://myvault?auth=workload_identity&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./deploy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The default authentication mode first looks for the &lt;code dir=&quot;auto&quot;&gt;tenant_id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;client_id&lt;/code&gt;,
and &lt;code dir=&quot;auto&quot;&gt;client_secret&lt;/code&gt; provider credentials introduced above, then their
&lt;code dir=&quot;auto&quot;&gt;AZURE_TENANT_ID&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;AZURE_CLIENT_ID&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;AZURE_CLIENT_SECRET&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;That makes a service principal straightforward to keep in the system keyring:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.azure]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;akv://myvault&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers.azure.credentials]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tenant_id&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client_id&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client_secret&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;azure&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;azure&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./deploy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Sovereign clouds can use either a complete vault hostname or an explicit DNS
suffix such as &lt;code dir=&quot;auto&quot;&gt;akv://myvault?suffix=vault.azure.cn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;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 &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://secretspec.dev/providers/akv/&quot;&gt;Azure Key Vault provider guide&lt;/a&gt; for authentication,
naming, references, and required permissions.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;gopass-joins-the-local-providers&quot;&gt;Gopass joins the local providers&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;a href=&quot;https://secretspec.dev/providers/gopass/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;gopass://&lt;/code&gt; provider&lt;/a&gt; reads and writes through the
&lt;code dir=&quot;auto&quot;&gt;gopass&lt;/code&gt; CLI. Gopass builds on the Unix &lt;a href=&quot;https://secretspec.dev/providers/pass/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;pass&lt;/code&gt; provider&lt;/a&gt; with
multi-user and multi-store support while keeping entries GPG-encrypted and
synchronized through git.&lt;/p&gt;
&lt;p&gt;Once Gopass is installed and its password store is initialized, select it like
any other provider:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gopass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gopass&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;By default, entries live under
&lt;code dir=&quot;auto&quot;&gt;secretspec/{project}/{profile}/{key}&lt;/code&gt;. A custom URI can change that layout,
including omitting &lt;code dir=&quot;auto&quot;&gt;{project}&lt;/code&gt; to share secrets between repositories:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;~/.config/secretspec/config.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[defaults.providers]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;shared&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;gopass://secretspec/shared/{profile}/{key}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;An existing Gopass entry can also be addressed directly with a
&lt;a href=&quot;https://secretspec.dev/concepts/references/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;&lt;/a&gt;, including the mount-point prefix used by a
multi-store setup. See the &lt;a href=&quot;https://secretspec.dev/providers/gopass/&quot;&gt;Gopass provider guide&lt;/a&gt; for
installation, shared-store configuration, references, and current limitations.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;php-joins-the-sdks&quot;&gt;PHP joins the SDKs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;cachix/secretspec&lt;/code&gt; Composer package brings the shared resolver to PHP:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;composer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cachix/secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;?&lt;/span&gt;&lt;span&gt;php&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt; Secretspec\&lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$resolved&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;withProfile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;withReason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$resolved&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$resolved&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;setAsEnv&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It offers two native backends behind the same PHP API. The recommended native
extension embeds the resolver and works under PHP-FPM without &lt;code dir=&quot;auto&quot;&gt;ffi.enable&lt;/code&gt;, like
&lt;code dir=&quot;auto&quot;&gt;ext-redis&lt;/code&gt;. An &lt;code dir=&quot;auto&quot;&gt;ext-ffi&lt;/code&gt; 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,
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#as_path-option&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;as_path&lt;/code&gt;&lt;/a&gt;,
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#requiring-a-reason-for-secret-access&quot;&gt;audit reasons&lt;/a&gt;,
and typed missing-secret errors behave the same way.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;setAsEnv()&lt;/code&gt; updates &lt;code dir=&quot;auto&quot;&gt;getenv()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$_ENV&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;$_SERVER&lt;/code&gt;, which lets Laravel’s
&lt;code dir=&quot;auto&quot;&gt;env()&lt;/code&gt; helper and Symfony’s &lt;code dir=&quot;auto&quot;&gt;%env(...)%&lt;/code&gt; processors consume resolved secrets
during application boot. See the &lt;a href=&quot;https://secretspec.dev/sdk/php/&quot;&gt;PHP SDK guide&lt;/a&gt; for installation and
framework examples.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;aws-creation-guardrails&quot;&gt;AWS creation guardrails&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AWS accounts often require a customer-managed KMS key or specific tags in the
same &lt;code dir=&quot;auto&quot;&gt;CreateSecret&lt;/code&gt; request. The
&lt;a href=&quot;https://secretspec.dev/providers/awssm/&quot;&gt;AWS Secrets Manager provider&lt;/a&gt; now accepts both on its URI:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;secretspec.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[providers]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;prod&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;awssm://prod@us-east-1?kms_key_id=alias/my-key&amp;#x26;tag.team=platform&amp;#x26;tag.env=prod&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;kms_key_id&lt;/code&gt; and repeatable &lt;code dir=&quot;auto&quot;&gt;tag.NAME=VALUE&lt;/code&gt; 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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;export-secrets-for-shells-tools-and-ci&quot;&gt;Export secrets for shells, tools, and CI&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;export&lt;/code&gt; command resolves every secret for the active profile without
starting another process. Its default output can be evaluated by a POSIX shell:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;eval&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--profile&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;--format dotenv&lt;/code&gt; to write dotenv syntax or &lt;code dir=&quot;auto&quot;&gt;--format json&lt;/code&gt; to pass the
resolved values to another tool:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ secretspec &lt;/span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--profile&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--format&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;DATABASE_URL&quot;: &quot;postgresql://prod.example.com/mydb&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;GitHub and Forgejo Actions can use &lt;code dir=&quot;auto&quot;&gt;--format gha&lt;/code&gt;. SecretSpec masks every value
in the runner log and appends it to &lt;code dir=&quot;auto&quot;&gt;$GITHUB_ENV&lt;/code&gt;, making the secrets available
to later steps and third-party actions:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Export secrets&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;secretspec export --profile production --format gha&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Deploy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;./deploy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Like non-interactive &lt;a href=&quot;https://secretspec.dev/reference/cli/#check&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt;&lt;/a&gt;, &lt;code dir=&quot;auto&quot;&gt;export&lt;/code&gt; 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
&lt;a href=&quot;https://secretspec.dev/concepts/audit/&quot;&gt;audit log&lt;/a&gt;. See the
&lt;a href=&quot;https://secretspec.dev/reference/cli/#export&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;export&lt;/code&gt; CLI reference&lt;/a&gt; for every format and option.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;provider-and-resolution-fixes&quot;&gt;Provider and resolution fixes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;0.15 also tightens the behavior around profiles and fallback chains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://secretspec.dev/concepts/providers/fallback/&quot;&gt;Provider chains&lt;/a&gt; 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.&lt;/li&gt;
&lt;li&gt;Chain entries accept aliases, bare provider names such as
&lt;a href=&quot;https://secretspec.dev/providers/keyring/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;keyring&lt;/code&gt;&lt;/a&gt;, shorthand such as
&lt;a href=&quot;https://secretspec.dev/providers/dotenv/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;dotenv:.env&lt;/code&gt;&lt;/a&gt;, and complete provider URIs.&lt;/li&gt;
&lt;li&gt;A single destination provider rejects unsupported
&lt;a href=&quot;https://secretspec.dev/concepts/references/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; coordinates&lt;/a&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://secretspec.dev/concepts/profiles/&quot;&gt;Profile overrides&lt;/a&gt; inherit the base secret’s
&lt;code dir=&quot;auto&quot;&gt;description&lt;/code&gt; and generation &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt;. Validation now uses the effective merged
secret while still catching real conflicts, such as combining
&lt;a href=&quot;https://secretspec.dev/concepts/generation/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt;&lt;/a&gt; with a profile default.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt; passes non-UTF-8 environment variables through to the child untouched,
and command output that previously depended on map order is now stable.&lt;/li&gt;
&lt;li&gt;Prebuilt &lt;a href=&quot;https://secretspec.dev/sdk/nodejs/&quot;&gt;Node.js addons&lt;/a&gt; now target glibc 2.28 and statically
include libdbus, restoring support for Amazon Linux 2023, RHEL 8/9, and
similar distributions.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://github.com/cachix/secretspec/blob/main/CHANGELOG.md&quot;&gt;full changelog&lt;/a&gt;
for every change and fix in this release.&lt;/p&gt;
&lt;p&gt;Questions or feedback? Join us on &lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.14: Secret references</title><link>https://secretspec.dev/blog/secretspec-0-14-secret-references/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-14-secret-references/</guid><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;SecretSpec keeps a &lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt; that declares what secrets an application
needs, and resolves the values from a provider: your system keyring, 1Password,
Vault, a &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; file, and so on. Until now it stored every secret under a naming
convention it controlled, &lt;code dir=&quot;auto&quot;&gt;secretspec/{project}/{profile}/{key}&lt;/code&gt;, and that
convention was the only place it looked.&lt;/p&gt;
&lt;p&gt;That works when SecretSpec created the secret. It does not when the secret
already exists under a name something else chose: a &lt;code dir=&quot;auto&quot;&gt;db&lt;/code&gt; item in a 1Password
vault, a &lt;code dir=&quot;auto&quot;&gt;myapp/config&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.14.0&quot; title=&quot;SecretSpec 0.14 release&quot;&gt;SecretSpec 0.14&lt;/a&gt;
introduces &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;. A secret can name one that already exists, by the store’s own
coordinates, and SecretSpec reads and writes that secret in place:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.production]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Postgres DSN&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;field&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;providers&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;prod_op&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;DATABASE_URL&lt;/code&gt; now resolves from the &lt;code dir=&quot;auto&quot;&gt;password&lt;/code&gt; field of the 1Password item
&lt;code dir=&quot;auto&quot;&gt;db&lt;/code&gt;. SecretSpec does not prepend a project or profile, and does not create a
name of its own.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-not-just-paste-the-address&quot;&gt;Why not just paste the address&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;1Password will give you an address for that field: &lt;code dir=&quot;auto&quot;&gt;op://Production/db/password&lt;/code&gt;.
The obvious design is to accept that string in the config and be done. We built
that first and removed it.&lt;/p&gt;
&lt;p&gt;A string like &lt;code dir=&quot;auto&quot;&gt;op://Production/db/password&lt;/code&gt; 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 &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;SecretSpec already decides which store to use, through providers, profiles,
fallback chains, and the &lt;code dir=&quot;auto&quot;&gt;--provider&lt;/code&gt; override. A &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; names only the secret
and leaves the store to that existing machinery.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;coordinates&quot;&gt;Coordinates&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; is a table, not a URL. Each key names a level of structure that some
stores have:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;vault                which container holds the item        (1Password only)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;└── item             the store&apos;s own name for the secret   (always required)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── section      a named group of fields               (1Password only)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── field    one component inside the item          (structured stores)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── version   which revision to read            (GCSM only)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Only &lt;code dir=&quot;auto&quot;&gt;item&lt;/code&gt; is required, because every store names its secrets somehow. &lt;code dir=&quot;auto&quot;&gt;item&lt;/code&gt;
is the complete name: it replaces the whole convention path, with no project or
profile and no folder prefix prepended. &lt;code dir=&quot;auto&quot;&gt;ref = { item = &quot;GITHUB_PAT&quot; }&lt;/code&gt; on the
env provider reads the environment variable &lt;code dir=&quot;auto&quot;&gt;GITHUB_PAT&lt;/code&gt; and nothing else.&lt;/p&gt;
&lt;p&gt;The other keys refine &lt;code dir=&quot;auto&quot;&gt;item&lt;/code&gt; for stores that have that structure. A &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; key
holds a single value, so &lt;code dir=&quot;auto&quot;&gt;field&lt;/code&gt; on a dotenv ref is not meaningful. A Vault KV
entry is a map, so &lt;code dir=&quot;auto&quot;&gt;field&lt;/code&gt; is required. When a store has no equivalent for a
coordinate it reports an error naming that coordinate, rather than reading a
different secret:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GitHub token&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GITHUB_PAT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;field&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;providers&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Error: Provider operation failed: the env provider does not support the `field` coordinate. Drop `field` from the ref for `GITHUB_PAT`.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;references-name-providers-route&quot;&gt;References name, providers route&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; supplies the name only. Which provider resolves it follows the normal
&lt;a href=&quot;https://secretspec.dev/concepts/providers/fallback/&quot;&gt;provider resolution order&lt;/a&gt;: a &lt;code dir=&quot;auto&quot;&gt;--provider&lt;/code&gt; override, then
the secret’s &lt;code dir=&quot;auto&quot;&gt;providers&lt;/code&gt; chain, then profile and global defaults. That is the
same order every other secret uses.&lt;/p&gt;
&lt;p&gt;Because the store is not part of the reference, the same &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; 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:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[profiles.production]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Postgres DSN&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt; = { &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;field&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;providers&lt;/span&gt;&lt;span&gt; = [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;onepassword://Production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;] }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Chain entries can also be inline &lt;code dir=&quot;auto&quot;&gt;scheme://&lt;/code&gt; URIs, as above, with no
&lt;code dir=&quot;auto&quot;&gt;[providers]&lt;/code&gt; alias declared first.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;--provider&lt;/code&gt; override redirects a referenced secret the same way it
redirects a conventional one, so pointing a whole suite at a &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; fixture needs
no change to the manifest:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dotenv:.env.fixtures&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;writing-through-a-ref&quot;&gt;Writing through a ref&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Reads and writes use the same coordinates. &lt;code dir=&quot;auto&quot;&gt;secretspec set&lt;/code&gt; and interactive
&lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt; write to the referenced secret in place wherever the store supports
writes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# writes the `password` field of the 1Password item `db`, in place&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;1Password edits the field with &lt;code dir=&quot;auto&quot;&gt;op item edit&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; also composes with &lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt;. If the referenced secret does not exist
yet, SecretSpec generates the value and writes it to the coordinates, so the
first &lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt; populates the item everything else already reads.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;faster-resolution&quot;&gt;Faster resolution&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;check&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt;, 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 &lt;code dir=&quot;auto&quot;&gt;BatchGetSecretValue&lt;/code&gt;, 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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Three changes to be aware of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code dir=&quot;auto&quot;&gt;onepassword://&lt;/code&gt; URI carrying an item path used to drop the path and target a
vault literally named &lt;code dir=&quot;auto&quot;&gt;vault&lt;/code&gt;. Item paths, including pasted
&lt;code dir=&quot;auto&quot;&gt;op://vault/item/field&lt;/code&gt; strings, now fail with an error that gives the &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;
table to write instead. Provider URIs are store addresses only.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt; is always a table. String and URI forms are rejected, with the same
translation in the error.&lt;/li&gt;
&lt;li&gt;Manifest validation now runs on every load. Rules that &lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt;
documents (a required secret cannot have a &lt;code dir=&quot;auto&quot;&gt;default&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;generate&lt;/code&gt; needs a
&lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt;, 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a href=&quot;https://secretspec.dev/concepts/references/&quot;&gt;Secret References&lt;/a&gt; for the full model and the
&lt;a href=&quot;https://secretspec.dev/reference/configuration/#secret-references&quot;&gt;configuration reference&lt;/a&gt; for how
each provider maps the coordinates. Questions or feedback? Join us on
&lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell</title><link>https://secretspec.dev/blog/secretspec-0-13-sdks/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-13-sdks/</guid><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;SecretSpec separates &lt;em&gt;what&lt;/em&gt; secrets an application needs, declared in
&lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt;, from &lt;em&gt;where&lt;/em&gt; 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 &lt;code dir=&quot;auto&quot;&gt;secretspec run&lt;/code&gt; or reimplemented resolution yourself.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.13.0&quot; title=&quot;SecretSpec 0.13 release&quot;&gt;SecretSpec 0.13&lt;/a&gt;
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.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;native-bindings-over-one-resolver&quot;&gt;Native bindings over one resolver&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every SDK is a thin client over the same Rust core that powers the CLI. No
provider logic, profile resolution, chain fallback, &lt;code dir=&quot;auto&quot;&gt;as_path&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;The binding strategy is chosen per ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;: a pyo3 extension, statically linked, shipped as a self-contained
&lt;code dir=&quot;auto&quot;&gt;cp39-abi3&lt;/code&gt; wheel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt;: a napi-rs addon with prebuilt per-platform packages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ruby&lt;/strong&gt;: a native C extension (mkmf) with the resolver statically linked into
a platform gem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt;: the &lt;code dir=&quot;auto&quot;&gt;secretspec-ffi&lt;/code&gt; C ABI loaded at runtime via
&lt;a href=&quot;https://github.com/ebitengine/purego&quot;&gt;purego&lt;/a&gt; (no cgo).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Haskell&lt;/strong&gt;: the same C ABI, linked at build time through the Haskell FFI.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;the-same-three-steps-in-your-language&quot;&gt;The same three steps, in your language&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each SDK mirrors the vocabulary of the Rust derive crate: a builder that takes a
provider, a profile, and an access reason, then &lt;code dir=&quot;auto&quot;&gt;load()&lt;/code&gt; to resolve, then a map
of secrets you can read or export into the environment.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Python&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; secretspec &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; SecretSpec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecretSpec.&lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_provider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_profile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_reason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolved.secrets&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.get&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;# value, or file path for as_path&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved.&lt;/span&gt;&lt;span&gt;set_as_env&lt;/span&gt;&lt;span&gt;()                         &lt;/span&gt;&lt;span&gt;# export into os.environ&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Node.js / TypeScript&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const { &lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt; } = &lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withProfile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withReason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;()); &lt;/span&gt;&lt;span&gt;// value, or as_path file path&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAsEnv&lt;/span&gt;&lt;span&gt;();                              &lt;/span&gt;&lt;span&gt;// export into process.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Go&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;New&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;WithProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;WithProfile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;WithReason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Load&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fmt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Secrets&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;()) &lt;/span&gt;&lt;span&gt;// value, or as_path file path&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetAsEnv&lt;/span&gt;&lt;span&gt;()                                 &lt;/span&gt;&lt;span&gt;// export into the environment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Ruby&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Secretspec&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_provider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_profile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_reason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;puts&lt;/span&gt;&lt;span&gt; resolved.&lt;/span&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;].&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# value, or as_path file path&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved.&lt;/span&gt;&lt;span&gt;set_as_env!&lt;/span&gt;&lt;span&gt;                      &lt;/span&gt;&lt;span&gt;# export into ENV&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Haskell&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;resolved &lt;/span&gt;&lt;span&gt;&amp;#x3C;-&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;S.load&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;( S.builder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt; S.withProvider &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt; S.withProfile &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt; S.withReason &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;boot web app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;S.setAsEnv resolved &lt;/span&gt;&lt;span&gt;-- export into the environment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Across all of them, &lt;code dir=&quot;auto&quot;&gt;load()&lt;/code&gt; resolves every declared secret, a missing required
secret raises a typed &lt;code dir=&quot;auto&quot;&gt;MissingRequiredError&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;as_path&lt;/code&gt; 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 &lt;code dir=&quot;auto&quot;&gt;require_reason&lt;/code&gt; policy from
&lt;a href=&quot;https://secretspec.dev/blog/secretspec-0-12-audit-logs-and-coding-agents/&quot;&gt;0.12&lt;/a&gt;, so a Go service is
as accountable as the CLI.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;write-your-own-binding&quot;&gt;Write your own binding&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Under all five SDKs sits a new crate, &lt;code dir=&quot;auto&quot;&gt;secretspec-ffi&lt;/code&gt;: 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,
&lt;code dir=&quot;auto&quot;&gt;Secrets::resolve()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Secrets::report()&lt;/code&gt;, so a Rust program reaches the same
value-carrying and value-free entry points.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;typed-secrets-one-schema-for-every-language&quot;&gt;Typed secrets, one schema for every language&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt; already knows the shape of your secrets, so 0.13 can hand that
shape to your type system. &lt;code dir=&quot;auto&quot;&gt;secretspec schema&lt;/code&gt; emits a JSON Schema for your
manifest, the union of all profiles or one profile with &lt;code dir=&quot;auto&quot;&gt;--profile&lt;/code&gt;. Pipe it
through &lt;a href=&quot;https://quicktype.io&quot;&gt;quicktype&lt;/a&gt; to generate idiomatic typed classes in
any language, then populate them from each SDK’s &lt;code dir=&quot;auto&quot;&gt;fields()&lt;/code&gt; map:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;schema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;quicktype&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-s&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;schema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--top-level&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecretSpec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--lang&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;python&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secrets_gen.py&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;typed &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Secrets.&lt;/span&gt;&lt;span&gt;from_dict&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolved.&lt;/span&gt;&lt;span&gt;fields&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;typed.database_url&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;# typed str&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;One schema drives every language’s type system, with no hand-written emitter per
language.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;install&quot;&gt;Install&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pip&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt;                              &lt;/span&gt;&lt;span&gt;# Python&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt;                              &lt;/span&gt;&lt;span&gt;# Node.js / TypeScript&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;gem&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt;                              &lt;/span&gt;&lt;span&gt;# Ruby&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;github.com/cachix/secretspec/secretspec-go&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# Go&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Haskell, add &lt;code dir=&quot;auto&quot;&gt;secretspec&lt;/code&gt; from Hackage to your &lt;code dir=&quot;auto&quot;&gt;build-depends&lt;/code&gt;. The CLI and
Rust SDK upgrade as usual:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;See the &lt;a href=&quot;https://secretspec.dev/sdk/overview/&quot;&gt;SDK overview&lt;/a&gt; for the per-language guides. Questions or
feedback? Join us on &lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>SecretSpec 0.12: Audit logs and coding agents</title><link>https://secretspec.dev/blog/secretspec-0-12-audit-logs-and-coding-agents/</link><guid isPermaLink="true">https://secretspec.dev/blog/secretspec-0-12-audit-logs-and-coding-agents/</guid><pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cachix/secretspec/releases/tag/v0.12.0&quot; title=&quot;SecretSpec 0.12 release&quot;&gt;SecretSpec 0.12&lt;/a&gt;
makes that access accountable. It ships three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Audit log&lt;/strong&gt; — every secret read and write is appended to a local,
per-user JSONL log. On by default. Values are never recorded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reason-on-access&lt;/strong&gt; — secret access can require a human-readable reason,
enforced for coding agents by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;secretspec audit&lt;/code&gt; command&lt;/strong&gt; — filter and summarize the log, or pipe raw
JSON Lines to &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside aria-label=&quot;Behavior change in 0.12&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Behavior change in 0.12&lt;/p&gt;&lt;div&gt;&lt;p&gt;If you run SecretSpec inside a coding agent, secret access now &lt;strong&gt;fails&lt;/strong&gt; until a
reason is supplied. This is the new default (&lt;code dir=&quot;auto&quot;&gt;require_reason = &quot;agents&quot;&lt;/code&gt;). Opt
out with &lt;code dir=&quot;auto&quot;&gt;require_reason = false&lt;/code&gt; in the &lt;code dir=&quot;auto&quot;&gt;[project]&lt;/code&gt; table. Existing providers
and library callers keep working unchanged. See &lt;a href=&quot;#upgrading&quot;&gt;Upgrading&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;the-audit-log&quot;&gt;The audit log&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every secret read and write, from the CLI and the Rust SDK, is appended to a
local log as &lt;a href=&quot;https://jsonlines.org/&quot;&gt;JSON Lines&lt;/a&gt;, one event per line. Secret
&lt;strong&gt;values are never written&lt;/strong&gt;, 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.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;v&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;ts&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;2026-06-04T17:04:00.893Z&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;action&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;project&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;my-app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;profile&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;production&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;DATABASE_URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;provider&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;keyring://&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;outcome&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;found&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;reason&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;deploy web frontend&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;actor&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;alice&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;agent&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;claude-code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;is_agent&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;version&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0.12.0&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The log lives in your per-user state directory
(&lt;code dir=&quot;auto&quot;&gt;~/.local/state/secretspec/audit.log&lt;/code&gt;) and is created readable only by you. Read
it with any tool, or use the new &lt;code dir=&quot;auto&quot;&gt;secretspec audit&lt;/code&gt; command for filtering and a
readable summary:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Last 20 entries, formatted&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;audit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Only `run` events for one project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;audit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--project&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;my-app&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--action&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Raw JSON Lines, piped to jq&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;audit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;jq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;select(.outcome == &quot;missing&quot;)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It is configured in your &lt;strong&gt;user-global config&lt;/strong&gt;
(&lt;code dir=&quot;auto&quot;&gt;~/.config/secretspec/config.toml&lt;/code&gt;), not the project’s &lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt;, 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:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;~/.config/secretspec/config.toml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[audit]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;See &lt;a href=&quot;https://secretspec.dev/concepts/audit/&quot;&gt;Audit Logging&lt;/a&gt; for the full record schema and options.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supplying-a-reason&quot;&gt;Supplying a reason&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ secretspec run -- npm test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Error: Accessing secrets requires a reason. Provide one with --reason&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;&amp;#x3C;why you are accessing these secrets&gt;&quot;, the SECRETSPEC_REASON environment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;variable, or Secrets::with_reason() in the SDK. (Policy: require_reason in&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[project] of secretspec.toml — defaults to &quot;agents&quot;; set it to false to&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;disable.)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Claude Code reads that message, states why it needs the secret, and retries:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--reason&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;run the test suite before opening a PR&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;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:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Source&lt;/th&gt;&lt;th&gt;Scope&lt;/th&gt;&lt;th&gt;Precedence&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--reason&lt;/code&gt; flag&lt;/td&gt;&lt;td&gt;CLI&lt;/td&gt;&lt;td&gt;highest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Secrets::with_reason()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SDK&lt;/td&gt;&lt;td&gt;overrides env&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;SECRETSPEC_REASON&lt;/code&gt;&lt;/td&gt;&lt;td&gt;CLI + SDK + derive&lt;/td&gt;&lt;td&gt;lowest&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# CLI: the most explicit option, overrides the others&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--reason&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;deploying release 0.12&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;./deploy.sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// SDK: the programmatic equivalent of --reason&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secrets&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Secrets&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;with_reason&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;nightly backup job&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Env: lowest precedence, but honored everywhere&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SECRETSPEC_REASON&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;nightly backup job&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;SECRETSPEC_REASON&lt;/code&gt; is resolved by &lt;code dir=&quot;auto&quot;&gt;Secrets::load&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;load_from&lt;/code&gt;, which means
&lt;code dir=&quot;auto&quot;&gt;secretspec-derive&lt;/code&gt;-generated code and other library callers satisfy the policy
and supply an audit reason &lt;strong&gt;without any code changes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;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
&lt;code dir=&quot;auto&quot;&gt;Provider::set_reason&lt;/code&gt; trait method (a no-op by default), so existing providers
keep working unchanged.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;configuring-when-a-reason-is-required&quot;&gt;Configuring when a reason is required&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;require_reason&lt;/code&gt; policy in the &lt;code dir=&quot;auto&quot;&gt;[project]&lt;/code&gt; table controls when a reason
is mandatory:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[project]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;my-app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;require_reason&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;agents&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# require it from agents (default), or true / false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;agents&quot;&lt;/code&gt; (the default): require a reason only when a coding agent is detected.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;: require it from every caller.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;: never require it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because the policy lives in &lt;code dir=&quot;auto&quot;&gt;secretspec.toml&lt;/code&gt; and is enforced by SecretSpec, it
applies to everyone and every CI runner, and is inherited through &lt;code dir=&quot;auto&quot;&gt;extends&lt;/code&gt;.
Coding agents are spotted by the
&lt;a href=&quot;https://crates.io/crates/detect-coding-agent&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;detect-coding-agent&lt;/code&gt;&lt;/a&gt; crate
(Claude Code, Cursor, Codex, Gemini CLI, Copilot, and more); set
&lt;code dir=&quot;auto&quot;&gt;SECRETSPEC_AGENT&lt;/code&gt; for a harness it doesn’t recognize.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cargo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;secretspec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Remember the new default: agents must pass a reason: set &lt;code dir=&quot;auto&quot;&gt;require_reason = false&lt;/code&gt;
to opt out.&lt;/p&gt;
&lt;p&gt;Questions or feedback? Join us on &lt;a href=&quot;https://discord.gg/naMgvexb6q&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;</content:encoded></item></channel></rss>