Skip to content

Repository files navigation

RunOS CLI

CI Release Go Report Card

Command-line interface for RunOS -- a self-hosted cloud platform where you bring your own hardware to run cloud infrastructure across multiple providers.

This CLI communicates with the same REST API used by the RunOS web console, giving you full control over your clusters, services, and deployments from the terminal.

Features

  • Cluster management -- create, list, and manage Kubernetes clusters
  • Service provisioning -- managed services (PostgreSQL, Valkey, MySQL, etc.)
  • Application deployment -- deploy from local source (runos deploy) or from a linked GitHub/GitLab integration at a specific commit (VCS deploys)
  • Apps + services as IaC -- apps pull / apps diff / apps sync and the matching services triplet round-trip cluster state to/from yaml on disk for git-versioned config
  • Job tracking -- follow long-running operations with real-time progress and per-step build logs
  • Headless auth -- personal access tokens for CI/CD via RUNOS_API_KEY (no interactive login required)
  • MCP integration -- Model Context Protocol server for AI coding assistants (Claude Code, Gemini CLI, OpenCode, OpenAI Codex)
  • Dynamic commands -- most CLI commands auto-update from the API manifest, so new server-side features appear without a CLI upgrade

Installation

Install Script

macOS / Linux:

curl -fsSL https://get.beta.runos.com/cli.sh | bash

Windows (PowerShell):

irm https://get.beta.runos.com/cli.ps1 | iex

From Source

git clone https://github.com/runos-official/cli.git
cd cli
make local

Requires Go 1.25+. The binary is installed to ~/.local/bin/runos.

Quick Start

# Log in via browser (also picks up the default environment on first run)
runos login

# List your clusters
runos clusters list

# Deploy an app from the current directory
runos deploy

The first run auto-fetches the default environment from the RunOS CDN, so there's no manual environment-switch step. Use runos config env <name> only when you want to point the CLI at a different environment.

Authentication

Two paths, depending on whether the CLI runs interactively or headless:

Interactive (laptop)

runos login

Opens a browser to complete sign-in (Google, GitHub, or email/password, with optional 2FA). Tokens are stored under ~/.runos/ with 0600 permissions.

Headless (CI / scripts)

Generate a personal access token in the console under Account → API Keys, or via runos account api-keys add. Then export the token plus your account ID:

export RUNOS_API_KEY=...
export RUNOS_ACCOUNT_ID=...
# Optional: pin a non-default environment
export RUNOS_API_URL=https://api.your-env.runos.com

When RUNOS_API_KEY is set, the CLI bypasses Firebase auth entirely. No ~/.runos/config.json is required, and no interactive runos login is needed.

Configuration

Configuration is stored in ~/.runos/config.json.

Environment Variables

Variable Description
RUNOS_API_KEY Personal access token for headless auth
RUNOS_ACCOUNT_ID Account ID (required alongside RUNOS_API_KEY)
RUNOS_API_URL Override the API endpoint
CONSOLE_URL Override the web console URL
RUNOS_CLUSTER_ID Default cluster ID for cluster-scoped commands

Custom URLs

For local development or custom endpoints:

runos config set api-url http://localhost:3025
runos config set console-url http://localhost:5177

Deployment

runos deploy dispatches on the app's deploy type:

  • CLI deploy (deployType: cli): the CLI tarballs your local source (respecting .dockerignore), uploads it, and tracks the build/deploy job to completion.
  • VCS deploy (deployType: vcs): the CLI sends {sha, configPath} only; the cluster pulls source from your linked GitHub/GitLab integration at the SHA, builds in-cluster, and rolls out. SHA defaults to git rev-parse HEAD; pass --sha to pin and --allow-dirty to waive the dirty-tree refusal. Use --app <id> --sha <sha> for CI mode (no local yaml on disk).

A minimal runos.yaml:

app: my-app
port: 3000
requires:
    my-app-db:
        type: postgresql
        class: postgresql.c0.beff
        config:
            databaseName: myapp
            databaseUsername: myapp
        env:
            url: DATABASE_URL

Environment variables

Two parallel files, both flow into the running pod via envFrom:

File Purpose Permissions VCS
.runos.<cid>.<id>.env Sensitive credentials (Secret-backed) 0600 gitignored
runos.<cid>.<id>.config.env Plain config (ConfigMap-backed; log level, feature flags, public URLs) 0644 committed

App code reads them identically as process.env.X. A key may not appear in both files at once; the API refuses the deploy if it does.

Apps and services as IaC

The CLI round-trips an app's full configuration (yaml, env vars, secret files, manifest overrides) and any service the app depends on between cluster and disk, so cluster state can live in version control.

# Pull cluster state to disk (writes runos.<cid>.<id>.yaml + env files + linked services)
runos apps pull <yaml>           # one app
runos apps pull --all            # every app in the cluster

# Show drift between local files and cluster
runos apps diff <yaml>           # exit 0 = in sync, exit 2 = drift, anything else = real failure

# Push local files to the cluster
runos apps sync <yaml>           # interactive; --dry-run to plan, --yes to skip prompt

The runos services triplet (pull / diff / sync) follows the same shape for managed services. The yaml is the source of truth for cluster id, so once committed, CI loops over yamls don't need a default cluster set or --cid per call.

For CI workflows, see runos apps sync --dry-run (drift detection) and runos apps diff --redact-secrets (secret-safe diff output).

MCP Integration

The CLI includes a built-in Model Context Protocol server that exposes RunOS operations as tools for AI coding assistants.

Four permission levels control what operations are available:

Level Risk Description
read Low Query-only operations (list clusters, apps, services)
sensitive-read High Read operations that return credentials and connection strings -- these will be visible to the AI model
write High Create, update, and delete operations that modify live infrastructure
sensitive-write Critical Credential rotation and secret management on live infrastructure

Security note: Choose the minimum permission level you need. The sensitive-read and above levels expose secrets to the AI assistant's context. Only use these in trusted environments.

Configure for your AI assistant

# Claude Code
runos mcp configure claude

# Gemini CLI
runos mcp configure gemini

# OpenCode
runos mcp configure opencode

# OpenAI Codex
runos mcp configure codex

How tools are exposed

Tools surface to MCP clients via two paths:

  1. Manifest-driven (the default). Most tools come from the RunOS API CLI manifest at ~/.runos/manifest.json. Each command entry maps directly to an HTTP endpoint, so adding a new API on the server side automatically produces a new MCP tool the next time runos manifest update runs. Categorisation (read / sensitive-read / write / sensitive-write) is set per command via the manifest's mcp field.

  2. Custom handlers. A small set of tools need orchestration that a single API call can't express -- e.g. deploy (tar + upload + poll), and apps_pull / apps_diff / apps_sync / apps_list_previous_uploads (which combine API calls with local filesystem work). These live in internal/mcp/ and run as runos subprocesses so behaviour stays in lockstep with the CLI. To add one: register a Tool descriptor in buildTools() (or a helper called from it) and dispatch by name in handleToolsCall().

If a feature can be a single endpoint, prefer adding it to the manifest. Reach for a custom handler only when the tool needs to drive multiple endpoints, touch the local filesystem, or run a long subprocess.

Development

Prerequisites

  • Go 1.25+
  • A RunOS account with access to a cluster

Building

# Install the tracked git hooks (run once, right after you clone)
make hooks

# Build and install locally
make local

# Build all platforms
make build

# Run tests
make test

Git hooks

Run make hooks once per clone. It points core.hooksPath at the tracked .githooks/ directory. .git/hooks is not tracked, so a hook that is not installed is a hook nobody has. The pre-commit hook runs the leak gate over your staged diff and blocks a commit that would publish a credential or a new internal identifier.

Public repo: no credentials, no internal identifiers

This repo is public. Never commit credentials, tokens, API keys or private keys; real account, cluster or app IDs; org or customer names; internal hostnames; or pasted terminal output that carries a real address.

Use placeholders in examples and fixtures. For addresses, use the ranges that exist for exactly that purpose: 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 (RFC 5737), and 2001:db8::/32 (RFC 3849).

scripts/leakcheck.py enforces this. It runs in three places: the pre-commit hook (staged diff only, fast), make leakcheck (on demand), and scripts/release.sh (whole tree, and it cannot be skipped).

make leakcheck          # scan every tracked file
make leakcheck-staged   # scan only what is staged
make leakcheck-test     # test the checker itself
make leakcheck-update   # ratchet the baseline down after removing an identifier

It has two severities.

  • Credentials hard fail, always. They can never be baselined.
  • Internal identifiers are ratcheted. scripts/leakcheck.baseline records what this repo has already published, so existing work is not blocked. A NEW identifier fails the gate.

An internal identifier is a machine name or account id listed in scripts/leakcheck.config, or any IP address literal outside the documentation, loopback, link-local, unspecified, broadcast and well-known multicast ranges. Addresses are allow-listed rather than deny-listed, because you cannot tell a real address from an invented one by reading it. A project constant such as a service CIDR is absorbed into the baseline once and never asked about again.

Do not hand-add a line to scripts/leakcheck.baseline to get a commit through. A line in that file records a leak that already shipped, it is not a licence to add another. Remove the identifier from the source, then run make leakcheck-update so the baseline shrinks.

The pre-commit hook can be skipped in a genuine emergency with git commit --no-verify, and it says so when it fires. Skipping does not get the change released: the release gate runs the same checker over the whole tree.

Project Structure

cli/
├── .githooks/              # Tracked git hooks (install with `make hooks`)
├── cmd/                    # Cobra command definitions
├── scripts/                # release.sh and the public-repo leak gate
├── internal/
│   ├── api/                # HTTP client for the RunOS API
│   ├── apps/               # apps pull / diff / sync (apps as IaC)
│   ├── auth/               # Firebase + PAT authentication
│   ├── cache/              # File-based TTL cache
│   ├── config/             # Configuration management
│   ├── deploy/             # Deployment pipeline (archive, upload)
│   ├── dynacmd/            # Dynamic command builder from API manifest
│   ├── git/                # Thin git wrapper for VCS-deploy SHA resolution
│   ├── jobs/               # Job polling and progress display
│   ├── manifest/           # CLI manifest loading and parsing
│   ├── mcp/                # MCP server implementation
│   ├── output/             # Response formatting (text, JSON)
│   ├── services/           # services pull / diff / sync (services as IaC)
│   └── update/             # CLI self-update mechanism
└── version/                # Version variable (set via ldflags)

Releasing

  1. Add a ## vX.Y.Z section to CHANGELOG.md with release notes
  2. Commit, tag (git tag vX.Y.Z), and push with --tags
  3. CI builds all platforms, extracts the matching changelog section, and creates the GitHub release with those notes
  4. If no changelog entry exists for the version, CI falls back to auto-generated notes from commits

How Dynamic Commands Work

The CLI fetches a manifest from the API that defines available commands, their flags, and endpoint mappings. This means most commands are generated at runtime -- when the API adds new endpoints, the CLI picks them up automatically on the next runos manifest update (or when the cached manifest expires after 1 hour).

License

The RunOS CLI is source-available under the Elastic License 2.0: the source is published for transparency and security review, not as open source. Use is subject to the license terms. See LICENSE and NOTICE. Copyright 2026 RunOS.

About

RunOS command-line interface: manage clusters, deploy apps, and run services on your RunOS platform. Source-available (Elastic License 2.0).

Topics

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages