nono-go
May 24, 2026 · View on GitHub
Go CGo bindings for the nono capability-based security sandbox.
nono applies an irreversible, least-privilege sandbox to the current process using Linux Landlock (Linux) or Seatbelt/sandbox_init (macOS). You declare the paths and network modes the process needs; nono enforces them at the kernel level.
Platform support
| OS | Arch | Library bundled? |
|---|---|---|
| macOS | arm64 | yes |
| macOS | amd64 | yes |
| Linux | amd64 | yes |
| Linux | arm64 | yes |
All platforms work out of the box. The static libraries for all four targets are bundled in the repository.
Prerequisites
- Go 1.24+
- A C toolchain (
gccorclang) for CGo
Installation
go get github.com/always-further/nono-go
Building native libraries
The bundled libraries are built from the upstream nono repository using scripts/build-libs.sh. Run this script when you want to update the bundled libraries to a newer nono upstream commit.
Requirements: cargo (for Apple targets), Docker (for Linux targets — uses rust:latest via emulation)
# Clone nono automatically and build all targets
./scripts/build-libs.sh
# Use an existing nono checkout
./scripts/build-libs.sh --nono-src /path/to/nono
Bundled library provenance is recorded in internal/clib/MANIFEST.json.
After rebuilding libraries locally, run make clib-manifest-update; CI verifies
the manifest with make clib-manifest.
Testing
make ci # local hygiene, tests, coverage, race tests, staticcheck
make test-apply # require the irreversible Apply subprocess test to pass
Usage
Apply a sandbox
caps := nono.New()
defer caps.Close()
if err := caps.AllowPath("/home/user/data", nono.AccessRead); err != nil {
log.Fatal(err)
}
if err := caps.AllowPath("/tmp", nono.AccessReadWrite); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkBlocked); err != nil {
log.Fatal(err)
}
// Irreversible — applies to this process and all children.
if err := nono.Apply(caps); err != nil {
log.Fatal(err)
}
Query permissions without applying
QueryContext lets you check what a capability set would allow before (or instead of) applying it. The capability set is cloned internally, so later changes to caps don't affect the query context.
caps := nono.New()
if err := caps.AllowPath("/home/user/data", nono.AccessRead); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkAllowAll); err != nil {
log.Fatal(err)
}
qc, err := nono.NewQueryContext(caps)
if err != nil {
log.Fatal(err)
}
defer qc.Close()
result, err := qc.QueryPath("/home/user/data/file.txt", nono.AccessRead)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status) // nono.QueryAllowed
netResult, err := qc.QueryNetwork()
if err != nil {
log.Fatal(err)
}
fmt.Println(netResult.Status) // nono.QueryAllowed
Serialize and deserialize state
SandboxState provides a JSON-serializable snapshot of a CapabilitySet, useful for persisting or transmitting sandbox configuration.
caps := nono.New()
if err := caps.AllowPath("/data", nono.AccessReadWrite); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkBlocked); err != nil {
log.Fatal(err)
}
state, err := nono.StateFromCaps(caps)
if err != nil {
log.Fatal(err)
}
defer state.Close()
jsonStr, err := state.JSON()
if err != nil {
log.Fatal(err)
}
// Later: restore from JSON
restored, err := nono.StateFromJSON(jsonStr)
if err != nil {
log.Fatal(err)
}
defer restored.Close()
caps2, err := restored.Caps()
if err != nil {
log.Fatal(err)
}
defer caps2.Close()
Error handling
All failing operations return *nono.Error. Use errors.Is with a sentinel error to test for specific failure kinds:
err := caps.AllowPath("/nonexistent", nono.AccessRead)
if errors.Is(err, nono.ErrPathNotFound) {
// path does not exist
}
Named sentinel errors: ErrPathNotFound, ErrExpectedDirectory, ErrExpectedFile, ErrPathCanonicalization, ErrNoCapabilities, ErrSandboxInit, ErrUnsupportedPlatform, ErrBlockedCommand, ErrConfigParse, ErrProfileParse, ErrIO, ErrInvalidArg, ErrTrustVerification, ErrUnknown.
macOS path canonicalization
On macOS, paths under /var (including those returned by os.TempDir and t.TempDir()) are symlinks to /private/var. nono canonicalizes paths, so the resolved capability will be under /private/var. When checking PathCovered, resolve symlinks first:
dir, _ := filepath.EvalSymlinks(t.TempDir())
covered, err := caps.PathCovered(filepath.Join(dir, "file.txt"))