Go SDK
Resolve SecretSpec secrets from Go
Changed in version 0.20
libsecretspec was named secretspec-ffi through SecretSpec 0.19. The current Go loader prefers the new artifact name and still accepts pre-0.20 shared libraries.
The Go SDK (secretspec-go) is a thin client over the libsecretspec C ABI, loaded via purego (dlopen, no cgo). Resolution happens in the Rust core, so the SDK inherits every provider with no Go-side logic.
Quick start
package main
import (
"fmt"
"log"
secretspec "github.com/cachix/secretspec/secretspec-go"
)
func main() {
resolved, err := secretspec.New().
WithProvider("keyring://").
WithProfile("production").
WithReason("boot web app").
Load()
if err != nil {
log.Fatal(err)
}
fmt.Println(resolved.Provider, resolved.Profile)
db := resolved.Secrets["DATABASE_URL"]
fmt.Println(db.Get()) // the value, or the file path for as_path secrets
resolved.SetAsEnv() // export everything into the process environment
}
A missing required secret returns *MissingRequiredError; any other failure returns *Error (with a stable .Kind).
Caller context
New in version 0.20
builder := secretspec.New().WithCaller(secretspec.CallerContext{
Name: "git",
Version: "2.51.0",
Operation: "credential_get",
Resource: "github.com",
})
Caller context identifies the invoking integration in audit records but never satisfies require_reason. Do not put credentials or secret values in it.
Inline specifications
New in version 0.20
Applications that own their declarations in code can resolve a strict JSON inline specification without creating a temporary secretspec.toml file. Pass the wire document to WithInlineSpec; baseDir resolves relative provider paths just as a manifest’s directory would. The current SDK sends inline schema v2, which requires SecretSpec 0.21+.
spec := map[string]any{
"project": map[string]any{"name": "my-app"},
"profiles": map[string]any{
"default": map[string]any{"secrets": map[string]any{
"API_TOKEN": map[string]any{"description": "API token"},
}},
},
}
resolved, err := secretspec.New().
WithInlineSpec(spec, "/logical/project").
WithReason("application startup").
Load()
Inline specification v2 (SecretSpec 0.21+) uses project, profiles, and each profile’s secrets object; optional providers, scopes, project defaults, profile defaults, and the normal secret declaration fields are also supported. A 0.21+ library still accepts the v1 documents sent by 0.20 SDKs. Unknown declaration fields are rejected. project.extends resolves parent manifests relative to baseDir. The SDK requires the native secretspec_call capability for inline specs; an older library returns a capability error rather than falling back to a filesystem search.
Scopes
New in version 0.17
Use WithScope("api") to resolve only a named [scopes.api] subset. The selected name is available as Resolved.Scope and Report.Scope:
package main
import (
"log"
secretspec "github.com/cachix/secretspec/secretspec-go"
)
func main() {
resolved, err := secretspec.New().WithScope("api").Load()
if err != nil {
log.Fatal(err)
}
defer resolved.Close()
}Typed access (codegen)
Generate typed structs with secretspec schema plus quicktype, then unmarshal resolved.FieldsJSON():
$ secretspec schema | quicktype -s schema --top-level SecretSpec --lang go -o secrets_gen.gopackage main
import (
"fmt"
"log"
secretspec "github.com/cachix/secretspec/secretspec-go"
)
func main() {
resolved, err := secretspec.New().Load()
if err != nil {
log.Fatal(err)
}
defer resolved.Close()
data, _ := resolved.FieldsJSON()
typed, _ := UnmarshalSecretSpec(data) // typed, generated
fmt.Println(typed.DatabaseURL)
}Library discovery
The native libsecretspec cdylib is resolved at runtime, in order:
- The
SECRETSPEC_FFI_LIBenvironment variable (an explicit path). - A library embedded at build time with
-tags embed_lib. - A Cargo
targetdirectory found by searching up from the working directory (the development path).
The SDK uses purego, so the cdylib is loaded at runtime, not linked. Either install/build libsecretspec and set SECRETSPEC_FFI_LIB, or stage the per-platform library into lib/ and build with -tags embed_lib for a self-contained binary. The embedded library is extracted to a per-user, owner-only cache directory at first use, and is not distributed through the Go module proxy.
Static linking
For a self-contained binary with no runtime library to locate, build with -tags static instead. This uses cgo and links libsecretspec.a directly into the Go binary. In a development checkout:
$ bash scripts/stage-staticlib.sh
$ CGO_ENABLED=1 go build -tags static ./...Linking with pkg-config
New in version 0.19
Install one library type with cargo-c:
# Use "static" (the default) or "shared"; use separate prefixes for both.
$ bash libsecretspec/scripts/cinstall.sh "$PREFIX" static
Then use the same build command for either type:
$ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" CGO_ENABLED=1 go build -tags pkgconfig ./...
Unlike staging, this also works for a go get dependency. A shared install in a non-system prefix also requires PREFIX/lib in the platform’s runtime library search path.
Last updated Oct 11, 2026