# Kongctl Extension Builder

> Scaffold and maintain kongctl CLI extensions. Use when a user wants to create a script or Go extension, define extension command paths, use the kongctl Go SDK helper, test install/link workflows, or debug extension context handling.

- Skill: `kong/kongctl-extension-builder` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add kong/kongctl-extension-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kong/kongctl-extension-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: Kong (https://skillmd.com/u/kong)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kong/kongctl-extension-builder

---


# kongctl extension builder

## Goal

Help users create a runnable `kongctl` CLI extension with a valid
`kongctl-extension.yaml`, an executable runtime, and local and remote test
workflows.

## Core Rules

- Extensions run as separate processes; do not import `internal/...` packages
  from `kongctl`.
- The manifest file must be named `kongctl-extension.yaml`.
- Required manifest fields are:
  - `schema_version: 1`
  - `publisher`
  - `name`
  - `runtime.command`
  - at least one `command_paths[].path`
- Extension IDs use `publisher/name`, and both segments should be lowercase
  path-safe identifiers.
- v1 command paths may contribute below `get` or `list`, or define a custom
  root verb that does not collide with built-ins.
- Built-in root segments such as `get` and `list` cannot declare aliases.
- `runtime.command` must be relative to the extension root and already
  executable. `kongctl` does not compile source during install.
- `version` in `kongctl-extension.yaml` is optional. For remotely installed
  extensions, prefer Git tags/releases as the package version source; kongctl
  displays the release tag, ref, or short commit when the manifest omits a
  version.
- GitHub repository names do not need a `kongctl-*` prefix. Use names that are
  clear for humans and keep the manifest ID in `publisher/name` form.
- Extension-specific args and flags should be shown directly in examples, such
  as `kongctl get foo --limit 10`. Use `--` only as an escape hatch when an
  extension needs to receive a token that collides with a host `kongctl` flag,
  such as `--output`, `--profile`, `--help`, or their shorthands.
- For Go extensions, prefer `github.com/kong/kongctl/pkg/sdk` over hand-rolled
  context parsing. Use it to load the runtime context, create an authenticated
  `sdk-konnect-go` client, run reentrant `kongctl` commands, and render output
  with parent `--output` and `--jq` settings.
- Do not make Go extensions call `kongctl api` as the primary Konnect access
  path when typed SDK access is appropriate. Use reentrant `kongctl` only for
  script extensions, debugging, or commands that are easier to delegate back to
  the host.
- Put extension diagnostics on stderr when stdout needs to preserve structured
  output from a reentrant `kongctl` command.

## Workflow

1. Choose a runtime style:
   - shell script for small wrappers
   - Go binary for richer parsing or reusable logic
2. Create `kongctl-extension.yaml` with one or more command paths.
3. Add the runtime file named by `runtime.command`.
4. Make script runtimes executable with `chmod +x`.
5. Test with a development link:
   ```sh
   kongctl link extension <extension-dir>
   kongctl get extension <publisher>/<name>
   kongctl get <resource>
   ```
6. Use local install when testing managed package copying:
   ```sh
   kongctl install extension <extension-dir>
   kongctl list extensions
   kongctl uninstall extension <publisher>/<name>
   ```
7. Prepare remote repositories so install can consume release archives:
   - release archives are preferred over source clone fallback
   - `kongctl-extension.yaml` must be at the archive root
   - `runtime.command` points to a runnable file inside the archive
   - publish one archive asset per release, or include the target platform in
     each platform-specific asset name
   - use `.tar.gz` for Go binary releases because it preserves executable bits
     reliably
   ```sh
   kongctl install extension <owner>/<repo>
   kongctl install extension <owner>/<repo> --ref <branch-or-tag>
   kongctl install extension <owner>/<repo>@<tag>
   kongctl install extension <owner>/<repo>@<tag> --yes
   kongctl upgrade extension <publisher>/<name>
   kongctl upgrade extension <owner>/<repo>
   kongctl upgrade extension <publisher>/<name>@<tag-or-version> --yes
   kongctl upgrade extension
   kongctl upgrade extensions
   ```

When no compatible release archive exists, `kongctl install extension
<owner>/<repo>` falls back to cloning the repository. Source fallback is only
valid when the repository root already contains `kongctl-extension.yaml` and an
already-runnable script or binary referenced by `runtime.command`.

For release-artifact installs, `kongctl upgrade extension <publisher>/<name>`
or `kongctl upgrade extension <owner>/<repo>` selects the latest compatible
GitHub release asset from the originally recorded repository. Add
`@<tag-or-version>` to pin the upgrade target, for example
`kongctl upgrade extension kong/debug@0.2.0` or
`kongctl upgrade extension kong/kongctl-ext-debug@0.2.0`. A bare semantic
version tries the exact release tag first and then a `v`-prefixed tag.
Source-clone installs require an explicit `@<tag|ref|commit>` target because
there is no stable "latest" release asset to resolve.

`kongctl upgrade extension` and `kongctl upgrade extensions` without a selector
upgrade all installed GitHub release-asset extensions. Linked extensions, local
path installs, and GitHub source-clone installs are skipped in batch mode.

Remote installs show a trust warning prompt with the selected source, extension
name, asset or ref, executable, command paths, and package/manifest/executable
hashes. Use `--yes` for automated tests or release workflow verification.

## Minimal Manifest

```yaml
schema_version: 1

publisher: kong
name: foo

runtime:
  command: kongctl-ext-foo

command_paths:
  - path:
      - name: get
      - name: foo
        aliases: [foos]
```

## Script Runtime Pattern

Use shell for lightweight wrappers and debugging helpers.

```sh
#!/bin/sh
set -eu

echo "context_path=${KONGCTL_EXTENSION_CONTEXT:-}" >&2

kongctl_bin="kongctl"
if [ -n "${KONGCTL_EXTENSION_CONTEXT:-}" ] &&
  [ -r "$KONGCTL_EXTENSION_CONTEXT" ]; then
  kongctl_bin=$(sed -n \
    's/^[[:space:]]*"kongctl_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
    "$KONGCTL_EXTENSION_CONTEXT")
fi

"$kongctl_bin" api get /v2/control-planes
```

## Go Runtime Pattern

Use Go when argument parsing, typed Konnect access, JSON handling, or richer
errors matter. Import the public kongctl SDK helper, never
`github.com/kong/kongctl/internal/...`.

```go
import "github.com/kong/kongctl/pkg/sdk"
```

`pkg/sdk` provides:

- `sdk.LoadRuntimeContextFromEnv()`
- `runtimeCtx.Args()` for extension-specific args after host flags are removed
- `runtimeCtx.DataDir()` for extension-owned persistent files
- `runtimeCtx.KonnectSDK(ctx)` for an authenticated `sdk-konnect-go` client
- `runtimeCtx.Output().Render(display, raw)` for native output and jq behavior
- `runtimeCtx.RunKongctl(ctx, args...)` for deliberate reentrant host calls

Use this shape for Go runtimes:

```go
func run() error {
    runtimeCtx, err := sdk.LoadRuntimeContextFromEnv()
    if err != nil {
        return err
    }

    ctx := context.Background()
    konnect, err := runtimeCtx.KonnectSDK(ctx)
    if err != nil {
        return fmt.Errorf("create Konnect SDK client: %w", err)
    }

    res, err := konnect.Me.GetUsersMe(ctx)
    if err != nil {
        return fmt.Errorf("get current user: %w", err)
    }

    display := map[string]any{
        "profile": runtimeCtx.Resolved.Profile,
        "user":    res.GetUser(),
    }
    return runtimeCtx.Output().Render(display, res.GetUser())
}
```

For `Render(display, raw)`, text output uses `display`; JSON/YAML and jq use
`raw`. When raw is omitted, the display value is used for all formats. Keep
stdout for command data and send diagnostics to stderr.

For a dedicated Go extension repository, use a normal module:

```sh
go mod init github.com/<owner>/<repo>
go get github.com/kong/kongctl@<released-version>
go mod tidy
```

While developing against an unreleased local kongctl checkout, use a temporary
replace directive:

```go
replace github.com/kong/kongctl => /path/to/kongctl
```

Remove local replace directives before publishing release artifacts unless the
repository intentionally vendors or pins an internal development checkout.
Commit `go.sum` for Go extension repositories.

## Release Artifact Workflow

For GitHub installs, prefer a release artifact that extracts to this shape:

```text
kongctl-extension.yaml
bin/kongctl-ext-foo
README.md
```

With:

```yaml
runtime:
  command: bin/kongctl-ext-foo
```

For a Go extension, add a workflow like this and adjust the binary package path
and artifact names for the repository:

```yaml
name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - goos: linux
            goarch: amd64
          - goos: darwin
            goarch: arm64
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: mkdir -p dist/package/bin
      - run: |
          CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
            go build -o dist/package/bin/kongctl-ext-foo ./cmd/kongctl-ext-foo
          cp kongctl-extension.yaml README.md dist/package/
          chmod +x dist/package/bin/kongctl-ext-foo
          archive="dist/kongctl-ext-foo-${{ matrix.goos }}-"
          archive="${archive}${{ matrix.goarch }}.tar.gz"
          tar -C dist/package -czf "$archive" \
            kongctl-extension.yaml README.md bin/kongctl-ext-foo
      - uses: softprops/action-gh-release@v2
        with:
          files: dist/*.tar.gz
```

For a script extension, publish a single universal archive:

```sh
mkdir -p dist/package/bin
cp kongctl-extension.yaml README.md dist/package/
cp kongctl-ext-foo dist/package/bin/
chmod +x dist/package/bin/kongctl-ext-foo
tar -C dist/package -czf dist/kongctl-ext-foo-universal.tar.gz \
  kongctl-extension.yaml README.md bin/kongctl-ext-foo
```

The installer accepts `.tar.gz`, `.tgz`, and `.zip` assets. If a release has
more than one archive asset, platform-specific assets should include the current
`GOOS` and `GOARCH` in the name, such as
`kongctl-ext-foo-linux-amd64.tar.gz`.

## Runtime Context

When an extension runs, `kongctl` sets `KONGCTL_EXTENSION_CONTEXT` to a
generated `context.json` file. Read this file for:

- matched command path
- remaining extension arguments
- selected profile
- resolved base URL
- output, jq, color theme, and log settings
- extension data directory
- host `kongctl` path and version

Never expect secrets in `context.json`. Go extensions should use
`github.com/kong/kongctl/pkg/sdk` when they need a configured Konnect client or
native output rendering. Script extensions can invoke `kongctl api` or other
built-in commands as subprocesses when they need host-authenticated Konnect
calls. Child `kongctl` commands inherit the parent extension context unless
they explicitly override flags such as `--profile`, `--output`, or
`--base-url`.

## Good Educational Extension Ideas

- Debug context: print `context.json`, remaining args, and reentrant command
  behavior.
- Who am I: call `kongctl get me` and render the current user in the parent
  output format.
- Go current user: use `pkg/sdk` to create a Konnect client and render
  `sdk-konnect-go` results with parent output settings.
- Control plane summary: call `kongctl api` to list control planes and print a
  compact operational summary.
- Portal/API report: combine a few `kongctl api` calls into a read-only report
  that would be awkward as a one-liner.
- Declarative helper: wrap existing `kongctl plan`, `diff`, or `apply` command
  sequences for a team-specific workflow.

Prefer read-only examples for public educational repositories. They are easier
to try safely and demonstrate auth/context reuse without destructive setup.

## Examples

- Script extension: `docs/examples/extensions/script`
- Go extension: `docs/examples/extensions/go`

The Go example must be built before linking because install and link expect
`runtime.command` to point to an existing executable.

