Skip to main content
A datumctl plugin is a standalone binary that datumctl runs on your users’ behalf. When a user runs datumctl dns zones list, datumctl finds your datumctl-dns binary, injects the current context and a credentials helper, and hands off to it. This guide walks through building a plugin in Go with the official SDK, go.datum.net/datumctl/plugin.
The SDK reads only environment variables and execs subprocesses, so your plugin depends on it without pulling in any datumctl internals. A worked reference plugin lives at examples/plugin-dns in the datumctl repository.

The model

  • A plugin is a binary named datumctl-<name> (for datumctl-native plugins) or milo-<name> (for portable milo-os platform plugins).
  • Users install it with datumctl plugin install, which places it in the managed plugins directory (~/.datumctl/plugins/ by default), or it can live on their PATH.
  • Users then run it as datumctl <name> — datumctl forwards the arguments, injects context and credentials, and forwards shell completion.
  • Before running a managed plugin, datumctl verifies its SHA256 fingerprint every time.

Building your plugin

1

Add the SDK dependency

go get go.datum.net/datumctl/plugin
2

Declare a manifest and serve it

Call plugin.ServeManifest at the very top of main(). datumctl invokes your binary with --plugin-manifest to read its metadata; ServeManifest handles that protocol and exits before your command logic runs.
var manifest = plugin.Manifest{
    Name:          "dns",
    Version:       "v0.1.0",
    Description:   "Manage Datum Cloud DNS zones",
    APIVersion:    1,
    MinAPIVersion: 1,
}
3

Build a root command with pre-wired flags

plugin.NewRootCmd returns a Cobra command with --org, --project, and --output flags already wired to the values datumctl injects.
root := plugin.NewRootCmd("dns", "Manage Datum Cloud DNS resources")
4

Read context and fetch a token

plugin.Context() reads the injected DATUM_* environment, and plugin.Token() calls the datumctl credentials helper for a fresh, short-lived access token. Call Token() immediately before each API request.
ctx := plugin.Context()
token, err := plugin.Token()

A minimal plugin

The following is a complete plugin grounded in the reference example. It serves its manifest, wires the root command, and adds a zones list subcommand that acquires a token and calls the Datum Cloud API:
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/spf13/cobra"
	"go.datum.net/datumctl/plugin"
)

var manifest = plugin.Manifest{
	Name:          "dns",
	Version:       "v0.1.0",
	Description:   "Manage Datum Cloud DNS zones",
	APIVersion:    1,
	MinAPIVersion: 1,
}

func main() {
	// Handle --plugin-manifest and exit before cobra runs.
	plugin.ServeManifest(manifest)

	root := plugin.NewRootCmd("dns", "Manage Datum Cloud DNS resources")

	zones := &cobra.Command{Use: "zones", Short: "Manage DNS zones"}
	zones.AddCommand(&cobra.Command{
		Use:   "list",
		Short: "List DNS zones",
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx := plugin.Context()

			// Fetch a fresh token right before the API call.
			token, err := plugin.Token()
			if err != nil {
				return fmt.Errorf("failed to get credentials: %w", err)
			}

			url := fmt.Sprintf("https://%s/v1/organizations/%s/projects/%s/dnszones",
				ctx.APIHost, ctx.Org, ctx.Project)

			req, err := http.NewRequestWithContext(cmd.Context(), http.MethodGet, url, nil)
			if err != nil {
				return err
			}
			req.Header.Set("Authorization", "Bearer "+token)

			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				return fmt.Errorf("API request failed: %w", err)
			}
			defer resp.Body.Close()

			fmt.Fprintf(cmd.OutOrStdout(),
				"DNS zones for org=%s project=%s (status %d)\n",
				ctx.Org, ctx.Project, resp.StatusCode)
			return nil
		},
	})

	root.AddCommand(zones)

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

The injected context

plugin.Context() returns the values datumctl sets before running your plugin — these come from the user’s active context:
FieldEnvironment variableDescription
OrgDATUM_ORGCurrent organization slug.
ProjectDATUM_PROJECTCurrent project slug (may be empty).
APIHostDATUM_API_HOSTAPI base URL, e.g. api.datum.net.
PluginAPIVersionDATUM_PLUGIN_API_VERSIONPlugin API version the host declares.
CredentialsHelperDATUM_CREDENTIALS_HELPERAbsolute path to the datumctl binary.
SessionDATUM_SESSIONActive session name (may be empty).
plugin.Token() runs the credentials helper ($DATUM_CREDENTIALS_HELPER auth get-token, adding --session when a session is active) and returns the token. Because tokens are short-lived, fetch one immediately before each request rather than caching it.

Building and testing locally

# Build with the datumctl- prefix so datumctl recognizes it.
go build -o datumctl-dns .

# Drop it into the managed plugins directory.
cp datumctl-dns ~/.datumctl/plugins/

# Run it through datumctl — context and credentials are injected automatically.
datumctl dns zones list

# Verify the manifest protocol directly.
./datumctl-dns --plugin-manifest
Shell completion is automatic: datumctl forwards completion requests to your binary, so your Cobra command’s built-in completion works when users run datumctl dns <TAB>. For flag-first commands, wrap your completion function with plugin.WithFlagCompletion to surface flags on a bare <TAB>.

Trust and verification for authors

datumctl treats your plugin as untrusted code, which shapes how you distribute it:
  • Ship checksums. Whether users install from a catalog or directly from a GitHub release, datumctl verifies a SHA256 checksum before running your binary. Publish accurate checksums for every release archive.
  • Managed installs are fingerprinted. After install, datumctl records the binary’s hash and re-verifies it on every run. Rebuilding or replacing the binary in place will cause datumctl to refuse it until it’s reinstalled.
  • A binary a user drops on their PATH is blocked by default. datumctl will not run an unmanaged datumctl-<name> binary until the user explicitly trusts it with datumctl plugin trust <name>.

Getting your plugin into a catalog

Once your plugin is released with archives and checksums, list it in a catalog so users can install it by name. See Publishing catalogs for the manifest format, validation, and hosting. Users can always install directly from a GitHub release without a catalog:
datumctl plugin install your-org/datumctl-dns
This path requires a checksums.txt alongside your release archives, in goreleaser’s default two-column format.

Next steps

Last modified on July 2, 2026