SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell

domenkozar1 pts0 comments

SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell | SecretSpec<br>Skip to content

SecretSpec 0.13: SDKs for Python, Node.js, Go, Ruby, and Haskell

Jul 3, 2026<br>Domen Kožar

SecretSpec separates what secrets an application needs, declared in<br>secretspec.toml, from where the values live, a provider like your system<br>keyring, 1Password, or Vault. Until now, reading those resolved secrets at<br>runtime meant the CLI or the Rust SDK. If your service was written in Python or<br>Go, you shelled out to secretspec run or reimplemented resolution yourself.

SecretSpec 0.13<br>closes that gap. It ships native SDKs for five languages: Python,<br>Node.js / TypeScript, Go, Ruby, and Haskell. Each resolves the exact secrets your<br>manifest declares, through the same providers, profiles, fallback chains, and<br>generators as the CLI, with no per-language configuration.

Native bindings over one resolver<br>Section titled “Native bindings over one resolver”

Every SDK is a thin client over the same Rust core that powers the CLI. No<br>provider logic, profile resolution, chain fallback, as_path materialization, or<br>secret generation lives in the binding. A provider added to SecretSpec works in<br>every language the day it lands, and every SDK behaves identically.

The binding strategy is chosen per ecosystem:

Python : a pyo3 extension, statically linked, shipped as a self-contained<br>cp39-abi3 wheel.

Node.js : a napi-rs addon with prebuilt per-platform packages.

Ruby : a native C extension (mkmf) with the resolver statically linked into<br>a platform gem.

Go : the secretspec-ffi C ABI loaded at runtime via<br>purego (no cgo).

Haskell : the same C ABI, linked at build time through the Haskell FFI.

The same three steps, in your language<br>Section titled “The same three steps, in your language”

Each SDK mirrors the vocabulary of the Rust derive crate: a builder that takes a<br>provider, a profile, and an access reason, then load() to resolve, then a map<br>of secrets you can read or export into the environment.

# Python

from secretspec import SecretSpec

resolved = (

SecretSpec.builder()

.with_provider("keyring://")

.with_profile("production")

.with_reason("boot web app")

.load()

print(resolved.secrets["DATABASE_URL"].get) # value, or file path for as_path

resolved.set_as_env() # export into os.environ

// Node.js / TypeScript

const { SecretSpec } = require('secretspec');

const resolved = SecretSpec.builder()

.withProvider('keyring://')

.withProfile('production')

.withReason('boot web app')

.load();

console.log(resolved.secrets.DATABASE_URL.get()); // value, or as_path file path

resolved.setAsEnv(); // export into process.env

// Go

resolved, err := secretspec.New().

WithProvider("keyring://").

WithProfile("production").

WithReason("boot web app").

Load()

fmt.Println(resolved.Secrets["DATABASE_URL"].Get()) // value, or as_path file path

resolved.SetAsEnv() // export into the environment

# Ruby

resolved = Secretspec::SecretSpec.builder

.with_provider("keyring://")

.with_profile("production")

.with_reason("boot web app")

.load

puts resolved.secrets["DATABASE_URL"].get # value, or as_path file path

resolved.set_as_env! # export into ENV

-- Haskell

resolved

S.load

( S.builder

& S.withProvider "keyring://"

& S.withProfile "production"

& S.withReason "boot web app"

S.setAsEnv resolved -- export into the environment

Across all of them, load() resolves every declared secret, a missing required<br>secret raises a typed MissingRequiredError, and as_path secrets come back as<br>a readable file path with a cleanup that removes the backing temp file. The<br>access reason feeds the same audit log and require_reason policy from<br>0.12, so a Go service is<br>as accountable as the CLI.

Write your own binding<br>Section titled “Write your own binding”

Under all five SDKs sits a new crate, secretspec-ffi: a small, versioned C ABI<br>for resolving secrets. If we do not ship your language yet, you can bind to it<br>directly. It also exposes the public Rust building blocks the SDKs share,<br>Secrets::resolve() and Secrets::report(), so a Rust program reaches the same<br>value-carrying and value-free entry points.

Typed secrets, one schema for every language<br>Section titled “Typed secrets, one schema for every language”

secretspec.toml already knows the shape of your secrets, so 0.13 can hand that<br>shape to your type system. secretspec schema emits a JSON Schema for your<br>manifest, the union of all profiles or one profile with --profile. Pipe it<br>through quicktype to generate idiomatic typed classes in<br>any language, then populate them from each SDK’s fields() map:

Terminal windowsecretspec schema | quicktype -s schema --top-level SecretSpec --lang python -o secrets_gen.py

typed = Secrets.from_dict(resolved.fields())

print(typed.database_url) # typed str

One schema drives every language’s type system, with no hand-written emitter per<br>language.

Install<br>Section titled “Install”

Terminal windowpip install secretspec # Python

npm install secretspec # Node.js /...

secretspec secrets resolved language python node

Related Articles