Command line

goon

One static binary. No runtime, no dependencies, no package manager. Drive every goy from a terminal, and hand the same thing to CI.

$ goon vm ls
ID                                     HOSTNAME       STATUS    OS             IPV6               PLAN
be9ef25d-9acc-449a-aead-c2142fdb9575   giggle-fiber   running   ubuntu-24.04   fd00:6969:85d::8   Gooning
2448c363-0596-4bbf-a705-2d8bed878313   clownflare-2   running   ubuntu-24.04   fd00:6969:85d::7   Gooning

Install

macOS & Linux

curl -fsSL https://s3.ua-west.goonhost.rocks/releases/cli/v0.1.3/goon_0.1.3_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -o goon
chmod +x goon
sudo mv goon /usr/local/bin/

Windows

curl.exe -fsSL https://s3.ua-west.goonhost.rocks/releases/cli/v0.1.3/goon_0.1.3_windows_amd64.exe -o goon.exe

Put it anywhere on your PATH.

Verify what you downloaded against SHA256SUMS.

Sign in

goon uses an API token, never your password. Create one in settings, then paste it when asked — it is read from stdin so it stays out of your shell history.

$ goon auth login
Paste your API token: ****
Signed in as you@example.com

A read-scoped token is refused on anything that changes state, server side. Use one for anything you do not fully trust. In CI, set GOONHOST_TOKEN and skip the config file entirely.

Describe a lab in a file

goon apply creates whatever is missing and nothing else. Run it twice and the second run does nothing.

# goonhost.yaml
apiVersion: goonhost/v1
kind: Stack
spec:
  networks:
    - name: lab-net
  firewalls:
    - name: lab-guard
      rules:
        - allow: tcp/22
        - allow: tcp/179     # BGP
  servers:
    - name: node
      count: 4               # node-1 .. node-4
      plan: Edging
      sshKey: laptop
      network: lab-net
      firewall: lab-guard
$ goon apply -f goonhost.yaml --dry-run
Planned changes:
  create  network   lab-net
  create  firewall  lab-guard
  create  rule      lab-guard  allow tcp/22
  create  rule      lab-guard  allow tcp/179
  create  server    node-1
  create  server    node-2
  create  server    node-3
  create  server    node-4

Dry run — nothing was changed.

Additive by default: anything the file does not mention is left alone. Deleting takes --prune, which says so plainly and is never implied. Plan and key names are resolved before the first change is made, so a typo cannot leave you with half a stack.

Built to be scripted

Every command takes --json. Anything you can read, you can pipe.

$ goon vm ls --json | jq -r '.[] | select(.status=="running") | .hostname'
$ goon fw allow lab tcp/179 --from fd00::/8
$ goon vm rdns <id> node1.example.com

The same binary is an MCP server, so agents that cannot run a shell can still ask about your infrastructure.

{
  "mcpServers": {
    "goonhost": {
      "command": "goon",
      "args": ["mcp"],
      "env": { "GOONHOST_TOKEN": "gh_..." }
    }
  }
}

Read-only unless you opt in, and it cannot destroy anything in either mode — there is no tool to delete a server, a network or a firewall group. Pair it with a read-scoped token and the API refuses every write regardless of what the agent is asked to do.