▸ Agent Skills
3 min read

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.

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

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

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.

Scopes

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

Terminal window

package main

import (
 "fmt"
 "log"

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

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

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

Library discovery

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.

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

Terminal window

Linking with pkg-config

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

Terminal window

Then use the same build command for either type:

$ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" CGO_ENABLED=1 go build -tags pkgconfig ./...

Terminal window

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 08, 2026