Skip to main content

Go SDK

Supported modes: Hosted Hybrid Local Available in: Free Solo Teams

When NOT to use this

If you are not changing your application code, the gateway proxy gives you governance with zero code changes. If you are governing developer AI tools (Claude Code, Cursor, Codex CLI), use coding hooks instead.

The Control Zero Go SDK provides core policy enforcement for AI agents written in Go.

Documentation refreshed 2026-04-15

Go SDK documentation refreshed 2026-04-15 against actual SDK. For framework integrations, Python is the canonical SDK.

Scope

Go SDK v1.4.0 ships core policy enforcement only: Guard(), policy loading (YAML/JSON), signed bundle verification in hosted mode, local audit logging, DLP scanning, and quarantine handling. No LLM provider wrappers (OpenAI, Anthropic, Google), no framework integrations (LangChain, LlamaIndex, Pydantic AI), and no secrets API are shipped in the Go SDK. For any of those, use the Python SDK or Node.js SDK.

Installation

go get controlzero.ai/sdk/go

Requirements: Go 1.24 or later.

Deployment modes

The Go SDK supports three modes, picked automatically from the combination of API key and local policy:

API keyLocal policyModePolicy sourceAudit destination
noyesLocalLocal map or fileRotating local log file
yesnoHostedSigned bundle pulled from backendRemote (bearer API key)
yesyesHybridLocal override (dashboard bundle ignored)Remote (bearer API key)
nonoPass-through (warn once)n/an/a

Policy resolution order for local mode:

  1. WithPolicy(...) or WithPolicyFile(...) option
  2. CONTROLZERO_POLICY_FILE env var
  3. ./controlzero.yaml in the current working directory
  4. CONTROLZERO_API_KEY env var (triggers hosted mode)
  5. Nothing — pass-through with a one-time stderr warning

Local mode (no API key)

package main

import (
"log"

controlzero "controlzero.ai/sdk/go"
)

func main() {
cz, err := controlzero.New(
controlzero.WithPolicyFile("./controlzero.yaml"),
)
if err != nil {
log.Fatalf("controlzero: %v", err)
}
defer cz.Close()

decision, err := cz.Guard("llm", controlzero.GuardOptions{
Method: "generate",
Args: map[string]any{"model": "gpt-5.4"},
})
if err != nil {
log.Fatalf("guard: %v", err)
}
if decision.Denied() {
log.Printf("blocked by %s: %s", decision.PolicyID, decision.Reason)
return
}
// proceed
}

Inline policy map (no file):

cz, err := controlzero.New(
controlzero.WithPolicy(map[string]any{
"version": "1",
"rules": []map[string]any{
{"allow": "llm:generate", "reason": "LLM calls permitted"},
{"deny": "filesystem:write_file", "reason": "no writes"},
},
}),
)

Hosted mode (API key, no local policy)

cz, err := controlzero.New(
controlzero.WithAPIKey("cz_live_your_api_key_here"),
)
if err != nil {
log.Fatalf("controlzero: %v", err)
}
defer cz.Close()

On construction, the SDK pulls the signed policy bundle from the backend, verifies the cryptographic signature, decrypts the payload, and caches it in memory. Any network or signature failure causes New to return an error (fail-closed).

Use NewWithContext to pass your own context for the bootstrap:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cz, err := controlzero.NewWithContext(ctx,
controlzero.WithAPIKey(os.Getenv("CONTROLZERO_API_KEY")),
)

Hybrid mode (API key + local policy)

When both an API key and a local policy are present, the SDK logs a warning and uses the local policy. Audit still ships to the remote backend. To make this an error instead of a warning, pass WithStrictHosted():

cz, err := controlzero.New(
controlzero.WithAPIKey(os.Getenv("CONTROLZERO_API_KEY")),
controlzero.WithPolicyFile("./local-override.yaml"),
controlzero.WithStrictHosted(), // fail instead of warn
)
// err is ErrHybridMode

Options

OptionDescription
WithAPIKey(key)Project API key (cz_live_* / cz_test_*). Also read from CONTROLZERO_API_KEY.
WithPolicy(map)Inline policy as a map[string]any.
WithPolicyFile(p)Path to a YAML or JSON policy file.
WithStrictHosted()Turn the hybrid-mode warning into a hard error.
WithLogPath(p)Path for the local audit log (default ./controlzero.log). Ignored in hosted/hybrid.
WithLogFormat(f)"json" (default) or "text". Ignored in hosted/hybrid.
WithLogRotation(sizeMB, maxBackups, maxAgeDays, compress)Configure local log rotation. Ignored in hosted/hybrid.

Log options only take effect in local mode. In hosted/hybrid mode, audit is shipped to the backend via a bearer-authenticated sink and the SDK prints a one-time stderr warning if log options were set.

Guard

The enforcement entry point:

func (c *Client) Guard(tool string, opts GuardOptions) (PolicyDecision, error)
type GuardOptions struct {
Args map[string]any
Method string // default "*"
RaiseOnDeny bool // if true, deny returns a *PolicyDeniedError
Context *EvalContext
}

type EvalContext struct {
Resource string
Tags map[string]string
}

Returns:

type PolicyDecision struct {
Effect string // "allow" | "deny" | "warn" | "audit"
PolicyID string
Reason string
EvaluatedRules int
}

func (d PolicyDecision) Allowed() bool
func (d PolicyDecision) Denied() bool
func (d PolicyDecision) Decision() string // alias for Effect

Basic check

decision, err := cz.Guard("llm", controlzero.GuardOptions{
Method: "generate",
Args: map[string]any{"model": "gpt-5.4"},
})
if err != nil {
log.Fatalf("guard: %v", err)
}
if decision.Denied() {
log.Printf("blocked: %s (policy %s)", decision.Reason, decision.PolicyID)
return
}

Raise on deny

Set RaiseOnDeny: true to get a typed error on deny — convenient for control flow:

_, err := cz.Guard("filesystem", controlzero.GuardOptions{
Method: "write_file",
Args: map[string]any{"path": "/etc/passwd"},
RaiseOnDeny: true,
})
var denied *controlzero.PolicyDeniedError
if errors.As(err, &denied) {
log.Printf("denied: %s", denied.Decision.Reason)
return
}

With context

Use EvalContext to pass resource and tags for condition matching:

decision, _ := cz.Guard("database", controlzero.GuardOptions{
Method: "query",
Context: &controlzero.EvalContext{
Resource: "table/orders",
Tags: map[string]string{
"agent_id": "research-bot-1",
"environment": "production",
},
},
})

See Policies for how conditions match against context, tags, and args.

Wildcard resource matching

A rule whose resources list is ["*"] matches every call regardless of whether the caller populated EvalContext.Resource. Non-wildcard resource patterns (for example, ["table/orders"]) still require the caller to set a matching Resource for the rule to fire.

// Rule shape: { actions: ["database:read"], resources: ["*"] }
// matches even when no Resource is provided:
decision, _ := cz.Guard("database", controlzero.GuardOptions{Method: "query"})

Action name compatibility

Legacy action names (database:query, database:execute, database:delete) and canonical class names (database:read, database:write, database:admin) both match the same calls. New policies should prefer the canonical class names. Existing rules with the legacy names continue to work without changes.

Error handling

The Go SDK defines typed error values in errors.go:

// Returned by Guard when RaiseOnDeny=true and the decision is deny.
type PolicyDeniedError struct {
Decision PolicyDecision
}

// Returned by New/LoadPolicy when a policy fails schema validation.
type PolicyValidationError struct {
Errors []string
Source string
}

// Returned by New/LoadPolicy when a policy file cannot be loaded.
type PolicyLoadError struct {
Message string
Source string
Cause error
}

// Hybrid-mode strict error.
type HostedAuthError struct{ Msg string }
type HostedBootstrapError struct{ Msg string }
type BundleFormatError struct{ Msg string }
type BundleSignatureError struct{ Msg string }

Sentinel errors:

controlzero.ErrHostedModeNotImplemented // legacy guard in some code paths
controlzero.ErrHybridMode // hybrid mode + WithStrictHosted

Typical matching:

cz, err := controlzero.New(controlzero.WithPolicyFile("./controlzero.yaml"))
if err != nil {
var pve *controlzero.PolicyValidationError
var ple *controlzero.PolicyLoadError
switch {
case errors.As(err, &pve):
log.Fatalf("invalid policy in %s: %v", pve.Source, pve.Errors)
case errors.As(err, &ple):
log.Fatalf("could not load %s: %v", ple.Source, ple.Cause)
default:
log.Fatalf("controlzero: %v", err)
}
}

decision, err := cz.Guard("tool", controlzero.GuardOptions{RaiseOnDeny: true})
if err != nil {
var denied *controlzero.PolicyDeniedError
if errors.As(err, &denied) {
// Typed denial
}
}

Quarantine

If the SDK detects tamper with the local policy state (signature failure, bundle corruption, or an explicit quarantine lockfile), every subsequent Guard() call returns:

PolicyDecision{
Effect: "deny",
Reason: "Machine quarantined: policy tampering detected. Run 'controlzero enroll' or 'controlzero policy-pull' to recover.",
}

There is no way to override quarantine from code. Recovery requires re-enrolling the machine or re-pulling the policy via the controlzero CLI.

Enrollment flow (hosted mode)

Hosted mode uses an API key for convenience, but the SDK also supports enrollment-based auth where each request carries a cryptographic signature and no bearer token exists on the wire.

High-level flow (performed by the controlzero CLI, not in application code):

  1. Admin creates an enrollment token in the dashboard.
  2. User runs controlzero enroll --token <...> on the machine.
  3. The CLI generates a signing keypair, binds it to the canonical machine_id, and POSTs to /api/enroll.
  4. Subsequent SDK policy pulls (GET /api/policy) and audit submissions (POST /api/audit) are signed with the enrolled key. Private key stays in the OS keystore.

Once enrolled, the SDK consumes the bundle transparently — your application code still just calls controlzero.New(...) and Guard(...).

Concurrency

*Client is safe for concurrent use. Create one at application startup and share it across goroutines. The internal audit sink and policy evaluator use appropriate synchronization.

cz, err := controlzero.New(controlzero.WithPolicyFile("./controlzero.yaml"))
if err != nil {
log.Fatal(err)
}
defer cz.Close()

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(agentID string) {
defer wg.Done()
decision, _ := cz.Guard("llm", controlzero.GuardOptions{
Method: "generate",
Context: &controlzero.EvalContext{Tags: map[string]string{"agent_id": agentID}},
})
_ = decision
}(fmt.Sprintf("agent-%d", i))
}
wg.Wait()

Not in the Go SDK

The following features are Python-and-Node only today:

  • Provider wrappers (wrap_openai, wrap_anthropic, wrap_google, etc.)
  • Framework integrations (LangChain, LlamaIndex, Pydantic AI, OpenAI Agents, Vercel AI SDK, Google ADK)
  • Secrets vault retrieval (get_secret)
  • Hosted MCP server bindings

If you need any of these, call the Python SDK or Node.js SDK from the relevant process. For pure policy enforcement in a Go service, the Go SDK is sufficient.

Next steps