Go SDK
Supported modes: Hosted Hybrid Local Available in: Free Solo Teams
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.
Go SDK documentation refreshed 2026-04-15 against actual SDK. For framework integrations, Python is the canonical SDK.
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 key | Local policy | Mode | Policy source | Audit destination |
|---|---|---|---|---|
| no | yes | Local | Local map or file | Rotating local log file |
| yes | no | Hosted | Signed bundle pulled from backend | Remote (bearer API key) |
| yes | yes | Hybrid | Local override (dashboard bundle ignored) | Remote (bearer API key) |
| no | no | Pass-through (warn once) | n/a | n/a |
Policy resolution order for local mode:
WithPolicy(...)orWithPolicyFile(...)optionCONTROLZERO_POLICY_FILEenv var./controlzero.yamlin the current working directoryCONTROLZERO_API_KEYenv var (triggers hosted mode)- 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
| Option | Description |
|---|---|
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):
- Admin creates an enrollment token in the dashboard.
- User runs
controlzero enroll --token <...>on the machine. - The CLI generates a signing keypair, binds it to the canonical
machine_id, and POSTs to/api/enroll. - 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
- Policies — write policy rules and conditions
- Local-Only Mode — run without an API key
- Python SDK — full feature set including integrations