Generate a command line interface for your API
Generate a production-ready command line tool (CLI) from your OpenAPI spec.
The Stainless CLI generator creates a command line tool from your OpenAPI specification. Your generated CLI includes automatic pagination, interactive TUI explorer, and man pages. The CLI wraps your Go SDK with command line argument parsing.
Example repositories:
Considerations
Section titled “Considerations”A CLI tool lets your users interact with your API from the terminal and integrate it into shell scripts and automation workflows. CLI tools are particularly valuable for developer-focused APIs and CI/CD integrations.
Before generating a CLI tool, be aware of the following requirements:
Go SDK dependency
The CLI generator creates a wrapper around your Go SDK, so Go must be included as one of your SDK targets and you must have a public-facing repository for your Go SDK.
Argument structure limitations
Command line interfaces have limitations when passing deeply nested structures as arguments. The generator creates ergonomic flags, but you may need to provide deeply nested parameters in JSON or YAML format.
Configuration
Section titled “Configuration”To generate a CLI tool, add the cli target to your Stainless configuration file.
The CLI generator creates a wrapper around your Go SDK, so you’ll need to enable both targets:
targets: go: package_name: github.com/my-company/my-sdk-go production_repo: my-org/my-sdk-go cli: binary_name: my-tool production_repo: my-org/my-tool edition: cli.2025-10-08For a complete list of configuration options, see the CLI target reference.
Prerequisites for local testing
Section titled “Prerequisites for local testing”To test or install the CLI locally, you need Go version 1.22 or later installed.
Verify your installation:
go version# go version go1.22.0 darwin/arm64Understanding where Go installs binaries
When you run go install, the binary is placed in your Go bin directory:
- Default location:
$HOME/go/bin(or$GOPATH/binif GOPATH is set) - Check your path: Run
go env GOPATHto see the base directory
If commands aren’t found after installation, add the Go bin directory to your PATH:
# Add to your shell profile (.zshrc, .bashrc, etc.)export PATH="$PATH:$(go env GOPATH)/bin"Test locally
Section titled “Test locally”To test your CLI tool before release:
- Clone your CLI staging repository
- Navigate to the repository directory
- Run the CLI using the provided script:
./scripts/run [resource] [command] [flags]For example, to test an endpoint:
./scripts/run people retrieve --id 123Installing locally
To build and install the CLI to your Go bin directory:
go install ./cmd/my-toolAfter installation, run the CLI directly:
my-tool --versionBasic usage
Section titled “Basic usage”The basic structure of your command line tool follows this format:
my-tool [resource [sub-resource...]] method-name --method-arg valueFor example, if your Stainless configuration has the following resources:
resources: $client: methods: current_status: get /status people: methods: retrieve: get /person/{id} create: post /person list: get /peopleThen your generated CLI tool can be used like this:
my-tool current-status# Output: {"status": "Up and running!"}
my-tool people create --job "President" \ --name.full-name "Abraham Lincoln" \ --name.nickname "Abe Lincoln"
my-tool people retrieve --id 123
my-tool people listNote that method names like current-status and flags like --full-name use kebab-case, which is conventional for command line tools.
Argument parsing
Section titled “Argument parsing”Get help for any command using the --help flag:
# General helpmy-tool --help
# Help for a specific endpointmy-tool people create --helpYou can also pipe JSON or YAML data as body parameters:
my-tool people create <<YAMLname: full_name: Abraham Lincoln nickname: Honest Abejob: PresidentYAML
# Or from a file:cat person.json | my-tool people createPassing files as arguments
Section titled “Passing files as arguments”To pass files to your API, you can use the @myfile.ext syntax:
my-tool people update --photo @abe.jpgFiles can also be passed inside JSON or YAML blobs:
my-tool people update --profile '{pic: "@abe.jpg"}'# Equivalent:my-tool people update <<YAMLprofile: pic: "@abe.jpg"YAMLIf you need to pass a string literal that begins with an @ sign, you can
escape the @ sign to avoid accidentally passing a file.
my-tool people update --username '\@abe'Explicit encoding
Section titled “Explicit encoding”For JSON endpoints, the CLI tool does filetype sniffing to determine whether the
file contents should be sent as a string literal (for plain text files) or as a
base64-encoded string literal (for binary files). If you need to explicitly send
the file as either plain text or base64-encoded data, you can use
@file://myfile.txt (for string encoding) or @data://myfile.dat (for
base64-encoding). Note that absolute paths will begin with @file:// or
@data://, followed by a third / (for example, @file:///tmp/file.txt).
my-tool upload-arbitrary-file --file @data://file.txtTop-level flags
Section titled “Top-level flags”Built-in top-level flags:
--help,-h: Show help message and exit--version,-v: Print version and exit--base-url: Provide a base URL for the API backend--format=...: Change output formatting (see below)--debug: Show debug information for HTTP requests and responses
Environment variables
Section titled “Environment variables”Generated CLIs support configuration through environment variables. The environment variable names are defined in your Stainless configuration under client_settings.opts.
For example, if your config includes:
client_settings: opts: api_key: type: string read_env: MY_TOOL_KEYThen the CLI reads authentication from the MY_TOOL_KEY environment variable.
MY_TOOL_KEY=sk_my_api_key_abc123xyz my-tool people retrieve --id 123
# Or export to reuseexport MY_TOOL_KEY=sk_my_api_key_abc123xyz
my-tool people create --job "President" \ --name.full-name "Abraham Lincoln" \ --name.nickname "Abe Lincoln"
my-tool people retrieve --id 123
my-tool people listOutput formatting
Section titled “Output formatting”The default output format is formatted and syntax-highlighted JSON. You can select different formats using the --format flag:
--format=auto: Automatically chosen format (currently defaults tojson)--format=json: JSON with autoformatting and syntax highlighting--format=jsonl: JSON formatted to fit on a single line--format=raw: Exact raw JSON response from server--format=yaml: Response in YAML format--format=pretty: Human-readable format similar to YAML with a box--format=explore: Interactive TUI explorer for browsing nested data (see below)
Interactive explorer
Section titled “Interactive explorer”The --format=explore option opens an interactive terminal UI for navigating complex JSON responses. This is useful for exploring deeply nested data structures.
my-tool people retrieve --id 123 --format=exploreKeyboard shortcuts:
| Key | Action |
|---|---|
↑ or k | Move up |
↓ or j | Move down |
→ or l | Expand nested object/array |
← or h | Collapse or go back |
Enter | Expand selected item |
p | Print the current value |
r | Show raw JSON format |
q | Quit explorer |
The explorer uses your system pager (configured via the PAGER environment variable, defaulting to less) for scrolling through large datasets.
Data transformation
Section titled “Data transformation”Use the --transform flag to filter or reshape JSON responses using GJSON query syntax. This is useful for extracting specific fields or navigating nested structures.
# Extract just the name fieldmy-tool people retrieve --id 123 --transform "name"
# Get the first item from an arraymy-tool people list --transform "items.0"
# Extract multiple fieldsmy-tool people retrieve --id 123 --transform "{name,job}"
# Filter array itemsmy-tool people list --transform 'items.#(job=="President")#'Common GJSON patterns:
| Pattern | Description |
|---|---|
field | Get a top-level field |
parent.child | Get a nested field |
items.0 | Get the first array element |
items.# | Get the array length |
items.#.name | Get the name field from all array items |
{field1,field2} | Extract multiple fields into an object |
items.#(status=="active")# | Filter array items by condition |
Use --transform-error to apply the same transformation to error responses.
Paginated endpoints
Section titled “Paginated endpoints”For paginated endpoints, your CLI supports automatic pagination. The CLI lazily streams items to your user’s terminal pager (e.g., $PAGER or less). New pages load automatically as the user scrolls.
The CLI respects HTTP 429 (too many requests) responses and throttles according to your response headers.
Man pages
Section titled “Man pages”Published CLI tools include automatically generated man pages. Users can run man my-tool to see the full usage manual.
When running locally, generate man pages by running ./scripts/run @manpages, which creates a compressed file in ./man/man1/, viewable with man ./man/man1/my-tool.1.gz.
Shell completions
Section titled “Shell completions”Generated CLIs include tab completion support for Bash, Zsh, and Fish shells. Completions are included in Homebrew installations automatically.
Editions
Section titled “Editions”Editions allow Stainless to make improvements to SDKs that aren’t backwards-compatible. You can explicitly opt in to new editions when you’re ready. See the SDK and config editions reference for more information.
cli.2025-10-08
Section titled “cli.2025-10-08”- Initial edition for CLI (used by default if no edition is specified)
Publishing and distribution
Section titled “Publishing and distribution”Once published, users can install your CLI without having Go installed on their machine. Stainless uses GoReleaser to build cross-platform binaries and publish releases.
For details on how Stainless opens Release PRs and manages versioning, see Versioning and releases.
Installation options for users
Section titled “Installation options for users”After publishing a release, your users have several installation options:
Homebrew (macOS/Linux) — Recommended for most users:
brew install your-org/tools/your-toolPre-built binaries — Download from GitHub releases for any platform.
Go users — Install directly with Go:
go install github.com/your-org/your-tool/cmd/your-tool@latestSecure your Homebrew publish secrets
Section titled “Secure your Homebrew publish secrets”The Homebrew tap token and macOS signing and notarization secrets described below are sensitive credentials. As repository secrets, they are readable by any workflow run, including runs from feature branches and pull requests. Place them in a GitHub environment scoped to main so only the publish workflow can read them.
-
In your CLI production repository, go to Settings > Environments > New environment and create an environment named
production. -
Under Deployment branches and tags, select Selected branches and tags and add a rule for
main. -
Optionally, add required reviewers to gate publish runs on manual approval.
-
Add
publish.release_environmentto your Stainless config:targets:cli:publish:release_environment: production
Add the secrets in the sections below to the production environment instead of as repository secrets.
Publishing to Homebrew
Section titled “Publishing to Homebrew”Stainless handles formula creation and management when you publish to Homebrew.
Create a Homebrew tap repository
- Create a public GitHub repository named
homebrew-toolsunder your organization (or any name starting withhomebrew-).
The repository name must start with homebrew- according to Homebrew conventions.
Generate a GitHub Personal Access Token
- Click on your account profile picture > Settings > Developer Settings > Personal Access Tokens > Fine-grained tokens > Generate new token.
- Choose your organization as the resource owner.
- Set “No expiration” to avoid regular renewal (recommended), or choose an expiration date.
- Choose “Only select repositories” and select the homebrew repository you created.
- Add permissions for “Contents” and “Pull requests” with read and write access.
- Generate the token and save it securely.
Add the token to your production repo
- In your CLI tool’s production repository, navigate to Secrets and variables > Actions > New repository secret.
The URL should look like
https://github.com/<org>/<repo>/settings/secrets/actions/new. - Add a new secret named
HOMEBREW_TAP_GITHUB_TOKENwith your token.
Update your Stainless config
Update the Stainless config and save:
targets: cli: publish: homebrew: tap_repo: your-org/homebrew-tools homepage: https://example.com description: The official CLI for YourOrg.Install and use
Once published, users can install your CLI using:
brew tap your-org/toolsbrew install your-tool# or more concise:brew install your-org/tools/your-toolNotarize macOS binaries
Section titled “Notarize macOS binaries”macOS requires applications to be notarized by Apple before users can run them without security warnings. Stainless uses GoReleaser’s notarization support to handle this process during builds, which requires an Apple Developer account. See the GoReleaser notarization documentation for details on obtaining the necessary credentials.
Required credentials
Section titled “Required credentials”You’ll need two files from Apple:
- Signing certificate (.p12) — Export your “Developer ID Application” certificate from Keychain Access
- App Store Connect API key (.p8) — Create an API key at App Store Connect with the “Developer” role
Set up GitHub secrets
Section titled “Set up GitHub secrets”Configure your CLI repository with these GitHub Actions secrets:
| Secret Name | Description |
|---|---|
MACOS_SIGN_P12 | Base64-encoded .p12 signing certificate (base64 < ./Certificates.p12) |
MACOS_SIGN_PASSWORD | Password to unlock the .p12 file |
MACOS_NOTARY_KEY | Base64-encoded .p8 App Store Connect API key (base64 < ./AuthKey_AAABBBCCC.p8) |
MACOS_NOTARY_KEY_ID | Key identifier from the .p8 filename (e.g., AAABBBCCC) |
MACOS_NOTARY_ISSUER_ID | Issuer UUID from App Store Connect |
Automated setup script
This script automates the process of configuring your GitHub repository with the required secrets. By default, it stores the secrets in a GitHub environment named production. Save it as setup-notarize.sh and run it
with your repository name:
bash ./setup-notarize.sh org/repo# or pick a different environment:bash ./setup-notarize.sh org/repo staging# or store as plain repository secrets:bash ./setup-notarize.sh org/repo ""The script requires the GitHub CLI to be installed and authenticated.
#!/usr/bin/env bash# Configure a GitHub repository with Apple notarization secrets for GoReleaser.## Usage:# ./setup-notarize.sh [OWNER/REPO] [ENVIRONMENT]## If OWNER/REPO is omitted, the current git remote origin is used.# If ENVIRONMENT is omitted, defaults to 'production'. Pass "" to store# secrets as plain repository secrets instead of environment-scoped.# Requires the `gh` CLI to be authenticated.
set -euo pipefail
# ── helpers ──────────────────────────────────────────────────────────────────
die() { echo "error: $*" >&2; exit 1; }info() { echo " $*"; }ask() { printf "%s: " "$1" >&2; read -r REPLY; echo "${REPLY/#\~/$HOME}"; }ask_path() { printf "%s: " "$1" >&2; read -re REPLY; echo "${REPLY/#\~/$HOME}"; }ask_secret() { printf "%s: " "$1" >&2 read -rs REPLY echo >&2 echo "$REPLY"}
# ── repo detection ────────────────────────────────────────────────────────────
if [[ $# -ge 1 ]]; then REPO="$1"else REMOTE=$(git remote get-url origin 2>/dev/null) || die "No git remote 'origin' found and no OWNER/REPO argument given." # Normalize SSH or HTTPS URL → owner/repo REPO=$(echo "$REMOTE" \ | sed -E 's|git@github\.com:||; s|https://github\.com/||; s|\.git$||')fi
[[ "$REPO" == */* ]] || die "Could not parse a valid OWNER/REPO from: $REPO"
ENV_NAME="${2-production}"
# ── confirm gh is available and authed ───────────────────────────────────────
command -v gh &>/dev/null || die "'gh' CLI not found. Install it: https://cli.github.com"gh auth status &>/dev/null || die "Not logged in to GitHub. Run: gh auth login"
# ── ensure environment exists (if requested) ─────────────────────────────────
if [[ -n "$ENV_NAME" ]]; then gh api -X PUT "/repos/$REPO/environments/$ENV_NAME" >/dev/null \ || die "Failed to create or access environment '$ENV_NAME' on $REPO." SCOPE_DESC="environment '$ENV_NAME'"else SCOPE_DESC="repository secrets"fi
cat <<END
Setting up Apple notarization secrets for: $REPOScope: $SCOPE_DESC
END
# ── collect credentials ───────────────────────────────────────────────────────
cat <<ENDStep 1 of 3 — Signing certificate (.p12) Export your 'Developer ID Application' certificate from Keychain Access as a .p12 file, then supply the path and its password below.
END
P12_PATH=$(ask_path " Path to .p12 file")[[ -f "$P12_PATH" ]] || die "File not found: $P12_PATH"MACOS_SIGN_P12=$(base64 < "$P12_PATH")MACOS_SIGN_PASSWORD=$(ask_secret " .p12 password (hidden)")openssl pkcs12 -in "$P12_PATH" -passin "pass:$MACOS_SIGN_PASSWORD" -noout 2>/dev/null \ || die "Could not open .p12 with that password — check the file and password and try again."
cat <<END
Step 2 of 3 — App Store Connect API key (.p8) Create an API key at https://appstoreconnect.apple.com/access/api with 'Developer' role. Download the .p8 file.
END
P8_PATH=$(ask_path " Path to .p8 file")[[ -f "$P8_PATH" ]] || die "File not found: $P8_PATH"MACOS_NOTARY_KEY=$(base64 < "$P8_PATH")_p8_base=$(basename "$P8_PATH" .p8)if [[ "$_p8_base" == AuthKey_* ]]; then MACOS_NOTARY_KEY_ID="${_p8_base#AuthKey_}" info "Key ID detected from filename: $MACOS_NOTARY_KEY_ID"else MACOS_NOTARY_KEY_ID=$(ask " Key ID (10-character identifier from App Store Connect)")fiMACOS_NOTARY_ISSUER_ID=$(ask " Issuer ID (UUID shown on the API Keys page)")
cat <<END
Step 3 of 3 — Uploading secrets to GitHub...
END
set_secret() { local name="$1" local value="$2" printf " %-30s" "$name" local args=(--repo "$REPO") [[ -n "$ENV_NAME" ]] && args+=(--env "$ENV_NAME") if printf '%s' "$value" | gh secret set "$name" "${args[@]}" --body - >/dev/null; then echo "set" else echo "FAILED" return 1 fi}
set_secret MACOS_SIGN_P12 "$MACOS_SIGN_P12"set_secret MACOS_SIGN_PASSWORD "$MACOS_SIGN_PASSWORD"set_secret MACOS_NOTARY_KEY "$MACOS_NOTARY_KEY"set_secret MACOS_NOTARY_KEY_ID "$MACOS_NOTARY_KEY_ID"set_secret MACOS_NOTARY_ISSUER_ID "$MACOS_NOTARY_ISSUER_ID"
cat <<ENDDone. The following GitHub Actions secrets are now configured on $REPO ($SCOPE_DESC):
MACOS_SIGN_P12 – base64-encoded .p12 signing certificate MACOS_SIGN_PASSWORD – password to unlock the .p12 MACOS_NOTARY_KEY – base64-encoded .p8 App Store Connect API key MACOS_NOTARY_KEY_ID – key identifier (e.g. XXXXXXXXXX) MACOS_NOTARY_ISSUER_ID – issuer UUID from App Store Connect
END
if [[ -n "$ENV_NAME" ]]; thencat <<ENDNext steps: 1. In the GitHub UI, open Settings > Environments > $ENV_NAME and restrict deployment branches to 'main' so only the production branch can read these secrets. 2. Add 'publish.release_environment: $ENV_NAME' under 'targets.cli.publish' in your Stainless config so the generated publish workflow opts into the environment.
ENDfi
cat <<ENDReference these in your GoReleaser config: https://goreleaser.com/customization/sign/notarize/
ENDConfigure GoReleaser
Section titled “Configure GoReleaser”After configuring the GitHub secrets, add the following to your CLI project’s .goreleaser.yml file:
notarize: macos: - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' ids: [macos]
sign: certificate: "{{.Env.MACOS_SIGN_P12}}" password: "{{.Env.MACOS_SIGN_PASSWORD}}"
notarize: issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}" key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}" key: "{{.Env.MACOS_NOTARY_KEY}}"Alternative: bypass notarization
Section titled “Alternative: bypass notarization”If you don’t have an Apple Developer account, GoReleaser supports an alternate workaround that uses a post-install hook to remove the macOS quarantine attribute. See GoReleaser’s signing and notarizing documentation for details.