Skip to main content
Extensions let you add new commands to the nuon CLI. Once installed, an extension called policies is available as nuon policies, just like a built-in command. Extensions are generally available and do not require preview flags. The extension system was heavily inspired by GitHub CLI extensions.

Compiled vs interpreted extensions

Extensions come in two flavors:
  • Compiled extensions ship precompiled binaries via GitHub Releases. The CLI downloads the correct binary for your platform during install. This is the standard distribution model for production extensions.
  • Interpreted extensions are cloned as source and run directly. The CLI supports two interpreted types:
    • Script: a single executable file (bash, Python, etc.) with a shebang line
    • Python: a uv-managed project executed via uv run
The extension type is auto-detected during installation, you don’t need to declare it. The CLI checks for GitHub Release assets first; if none match your platform, it clones the repo and detects the type from the directory contents.

Quick start

# Browse available extensions
nuon ext browse

# Install an extension
nuon ext install nuonco/nuon-ext-policies

# Install at a specific branch, tag, or commit
nuon ext install nuonco/nuon-ext-api@main

# Install from a local directory (for development)
nuon ext install ./nuon-ext-my-tool

# Run it
nuon policies --help

Managing extensions

All extension management commands live under nuon extensions (alias: nuon ext).

Browsing available extensions

nuon ext browse
This queries the nuonco GitHub organization for repositories matching the nuon-ext-* naming convention and shows which ones you already have installed. Extensions can be installed from any GitHub organization, but browse lists Nuon-authored extensions by default. Browse results are filtered to repositories with the nuon-extensions GitHub topic.

Installing an extension

nuon ext install <name>
You can provide the extension name in several formats:
InputResolved to
policiesnuonco/nuon-ext-policies (latest)
nuon-ext-policiesnuonco/nuon-ext-policies (latest)
nuonco/nuon-ext-policiesnuonco/nuon-ext-policies (latest)
myorg/nuon-ext-foomyorg/nuon-ext-foo (latest)
nuonco/nuon-ext-demo@mainnuonco/nuon-ext-demo at branch main
demo@v1.0.0nuonco/nuon-ext-demo at tag v1.0.0
demo@abc123nuonco/nuon-ext-demo at commit abc123
./nuon-ext-my-toolLocal directory (symlink)
Shorthand names (without an org prefix) default to the nuonco GitHub organization. To install an extension from another org, use the full org/nuon-ext-name format.

How install works

The install behavior depends on the extension type:
  • Compiled extensions: The CLI checks the latest GitHub Release for a platform-specific binary asset (e.g. nuon-ext-policies-darwin-arm64). It also supports release archives like nuon-ext-policies-darwin-arm64.tar.gz and nuon-ext-policies-darwin-arm64.zip. If a match is found, it downloads and installs the binary directly. No clone is needed.
  • Interpreted extensions: If no release asset matches your platform, the CLI clones the repository and detects the extension type from the directory contents (pyproject.toml for python, executable nuon-ext-<name> for script).
When @ref is specified (e.g. demo@main, nuonco/nuon-ext-demo@v1.0), the CLI first checks for a release at that tag and installs a matching binary asset if one exists. If no matching release asset is found (for example when using a branch name or commit SHA), it clones the repo at that exact ref.

Local install

nuon ext install ./path/to/nuon-ext-my-tool
Local installs create a symlink from the extensions directory to your source directory. This enables a live development loop: rebuild the binary or edit a script and the changes take effect immediately. The directory must contain a nuon-ext.toml and the directory name must match nuon-ext-<name>. If the extension is already installed, the command fails. For local development installs, remove and re-install from source to refresh the symlinked install.

Listing installed extensions

nuon ext list
Shows a table of installed extensions with their name, version, repository, and description. Use --json for machine-readable output.

Upgrading extensions

# Upgrade a specific extension
nuon ext upgrade policies

# Force re-download even if already at the latest version
nuon ext upgrade policies --force

# Upgrade all installed extensions
nuon ext upgrade
Upgrade checks for newer GitHub releases and re-downloads the binary if a new version is available. This currently only works for compiled extensions installed from a release. Interpreted extensions installed via clone should be re-installed to pick up changes.

Removing an extension

nuon ext remove policies
This deletes the extension and its local files. For locally installed extensions (symlinks), the symlink is removed but the source directory is left untouched.

Running an extension explicitly

nuon ext exec policies [args...]
The exec subcommand is an escape hatch for cases where an extension name conflicts with a built-in command. In most cases, you can run extensions directly as top-level commands (e.g. nuon policies).

How extensions work

Top-level commands

When the CLI starts, it scans the extensions directory and registers each installed extension as a top-level command. This means nuon policies works the same as nuon ext exec policies. When using top-level extension commands, Nuon root flags (for example -f / --config) are handled by the CLI, and only arguments after the extension command are forwarded to the extension process.

Environment variables

When running an extension, the CLI provides context through environment variables:
VariableDescription
NUON_API_URLAPI endpoint URL
NUON_ORG_IDCurrent organization context
NUON_APP_IDCurrent app context (if set)
NUON_INSTALL_IDCurrent install context (if set)
NUON_API_TOKENAuthentication token
NUON_EXT_NAMEExtension name
NUON_EXT_DIRPath to the extension’s local directory
NUON_CONFIG_FILEPath to the Nuon config file
Extensions can use these variables to interact with the Nuon API or read the local configuration. These values are populated from your active CLI config/context.

Authentication

Extensions declare their auth requirements in a nuon-ext.toml manifest. If an extension requires an API token or org context and one isn’t configured, the CLI prints a warning before running the extension. The extension still runs and is allowed to handle the missing credentials on its own.

Local storage

Extensions are stored under ~/.config/nuon/extensions/. Each extension gets its own directory:
~/.config/nuon/extensions/
├── nuon-ext-policies/                    # compiled - downloaded binary only
│   ├── nuon-ext-policies                 # platform binary
│   ├── nuon-ext.toml                     # cached manifest
│   └── manifest.json                     # install metadata
├── nuon-ext-gen-readme/                  # interpreted - full repo clone
│   ├── pyproject.toml                    # project definition
│   ├── nuon-ext.toml                     # manifest (from repo)
│   ├── manifest.json                     # install metadata
│   └── src/                              # source code
├── nuon-ext-my-tool -> /home/user/...    # local install (symlink)

Creating an extension

Every extension needs two things: a GitHub repository named nuon-ext-<name> and a nuon-ext.toml manifest at the root. Beyond that, the approach differs depending on whether you’re building a compiled or interpreted extension.

Repository name

The repository must use the nuon-ext- prefix. The rest becomes the command name:
nuonco/nuon-ext-policies        →  nuon policies
nuonco/nuon-ext-cost-report     →  nuon cost-report

Extension manifest

Every extension repository must include a nuon-ext.toml file at the root:
nuon-ext.toml
[extension]
name            = "policies"
description     = "Check deployment health across installs"
min_cli_version = "1.5.0"

[extension.auth]
requires_token = true
requires_org   = true
FieldRequiredDescription
extension.nameYesMust match the repo suffix after nuon-ext-
extension.descriptionYesShown in list and browse output
extension.min_cli_versionNoMinimum CLI version required
extension.auth.requires_tokenNoWarn if no API token is configured
extension.auth.requires_orgNoWarn if no org is selected

Compiled extensions (binary)

Compiled extensions ship precompiled binaries for each platform via GitHub Releases. This is the best choice when you want fast startup, no runtime dependencies, and a clean upgrade path via nuon ext upgrade. Create GitHub releases with assets following this naming scheme:
nuon-ext-<name>-<os>-<arch>[.exe]
Archive variants are also supported with the same base name:
nuon-ext-<name>-<os>-<arch>[.exe].tar.gz
nuon-ext-<name>-<os>-<arch>[.exe].zip
For example:
nuon-ext-policies-darwin-arm64
nuon-ext-policies-darwin-amd64
nuon-ext-policies-linux-amd64
nuon-ext-policies-linux-arm64
nuon-ext-policies-windows-amd64.exe
The CLI automatically selects the correct binary for the user’s platform during install. You can use any language and build system. GoReleaser works well for Go extensions.

Interpreted extensions (script and python)

Interpreted extensions are cloned as source and run directly. They’re simpler to set up and great for tooling that doesn’t need to be compiled. For example: documentation generators, linters, config validators, and similar utilities. The CLI supports two interpreted types:
TypeDetectionExecutionRequires
scriptExecutable nuon-ext-<name> at repo rootRun script directlyNothing extra
pythonpyproject.toml at repo rootuv run nuon-ext-<name>uv on the host

Script extensions

Place a single executable file named nuon-ext-<name> at the repo root with a shebang line:
nuon-ext-hello
#!/usr/bin/env bash
echo "Hello from $NUON_EXT_NAME"
The script receives the same environment variables as any other extension. Make sure the file is marked executable (chmod +x).

Python extensions

Python extensions use a standard pyproject.toml with a console script entry point. The CLI runs them via uv run nuon-ext-<name>, so uv manages the virtualenv and dependencies automatically. A minimal project structure:
nuon-ext-my-tool/
├── nuon-ext.toml
├── pyproject.toml
└── src/
    └── nuon_ext_my_tool/
        ├── __init__.py
        └── cli.py
With a pyproject.toml entry point like:
[project.scripts]
nuon-ext-my-tool = "nuon_ext_my_tool.cli:main"
We recommend click for building the CLI interface. Users install interpreted extensions with @ref to pin a branch:
nuon ext install myorg/nuon-ext-my-tool@main