Skip to content

Go SDK

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.

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).

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.

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.

spec := map[string]any{
"project": map[string]any{"name": "my-app"},
"profiles": map[string]any{
"default": map[string]any{"secrets": map[string]any{
"API_TOKEN": map[string]any{"description": "API token"},
}},
},
}
resolved, err := secretspec.New().
WithInlineSpec(spec, "/logical/project").
WithReason("application startup").
Load()

Inline specification v1 uses project, profiles, and each profile’s secrets object; optional providers, scopes, profile defaults, and the normal secret declaration fields are also supported. Unknown declaration fields are rejected. project.extends resolves parent manifests relative to baseDir. The SDK requires the native secretspec_call capability for inline specs; an older library returns a capability error rather than falling back to a filesystem search.

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()
}

Generate typed structs with secretspec schema plus quicktype, then unmarshal resolved.FieldsJSON():

Terminal window
$ secretspec schema | quicktype -s schema --top-level SecretSpec --lang go -o secrets_gen.go
package main

import (
	"fmt"
	"log"

	secretspec "github.com/cachix/secretspec/secretspec-go"
)

func main() {
	resolved, err := secretspec.New().Load()
	if err != nil {
		log.Fatal(err)
	}
	defer resolved.Close()

	data, _ := resolved.FieldsJSON()
	typed, _ := UnmarshalSecretSpec(data) // typed, generated
	fmt.Println(typed.DatabaseURL)
}

The native libsecretspec cdylib is resolved at runtime, in order:

  1. The SECRETSPEC_FFI_LIB environment variable (an explicit path).
  2. A library embedded at build time with -tags embed_lib.
  3. A Cargo target directory found by searching up from the working directory (the development path).

The SDK uses purego, 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.

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:

Terminal window
$ bash scripts/stage-staticlib.sh
$ CGO_ENABLED=1 go build -tags static ./...

Install one library type with cargo-c:

Terminal window
# 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:

Terminal window
$ 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.