Lightweight Go client and CLI for NVIDIA OpenShell gateways. A pure-Go reimplementation of the upstream Rust CLI that is a full standalone replacement — it does not depend on or shell out to the upstream openshell binary. Built for embedding in operators and CI pipelines as a single static binary with zero external dependencies.
go install github.com/openshift-online/openshellctl/cmd/openshellctl@latestOr build from source:
git clone https://github.com/openshift-online/openshellctl.git
cd openshellctl
make build # binary at ./openshellctl# Point at a gateway (env vars or config file)
export OPENSHELL_GATEWAY_ENDPOINT=https://gateway.example.com
export OPENSHELL_TOKEN=$(cat /path/to/token)
# Create a sandbox and connect
openshellctl sandbox create --from python
openshellctl sandbox connect my-sandbox
# Run a one-shot command
openshellctl sandbox exec --name my-sandbox -- python3 -c "print('hello')"
# Upload code, run it
openshellctl sandbox upload my-sandbox ./myproject /workspace
openshellctl sandbox exec --name my-sandbox -- python3 /workspace/main.py
# Clean up
openshellctl sandbox delete my-sandboxopenshellctl resolves authentication in priority order — the first match wins:
If you already have an openshell gateway login session, openshellctl reads the same token file — no separate login required:
openshell gateway login <gateway-name> # one-time setup (upstream CLI)
openshellctl sandbox list # uses your login token automaticallyopenshellctl reads oidc_token.json from $XDG_CONFIG_HOME/openshell/gateways/<name>/. If the access token is expired and a refresh token is present, openshellctl exchanges it for a new one automatically. The upstream openshell binary is only needed for the initial gateway login — after that, openshellctl is fully standalone.
Set a client secret and openshellctl mints tokens via client_credentials — no login required, and tokens are re-minted automatically when they expire (even during long-running sessions):
export OPENSHELL_OIDC_CLIENT_SECRET=<secret>
# Optional overrides (defaults come from gateway metadata):
# export OPENSHELL_OIDC_CLIENT_ID=<client-id>
# export OPENSHELL_OIDC_ISSUER=<issuer-url>
openshellctl sandbox create -f job.yaml --no-keep
# Token re-mints transparently — the --no-keep cleanup always has a valid tokenThis is the recommended method for automation, CI pipelines, and any non-interactive use. Unlike the upstream Rust CLI (which bakes the token once at startup), openshellctl refreshes per-request, so long-running jobs never fail with ExpiredSignature.
For quick testing or external token management:
openshellctl --token <JWT> sandbox list
# or
export OPENSHELL_TOKEN=<JWT>This overrides all other auth methods. The token is used as-is with no refresh — if it expires, commands will fail.
Every API call goes through an automatic retry: if the gateway returns Unauthenticated, openshellctl invalidates its cached token and retries once with a fresh one. For service accounts this means a full re-mint; for personal logins it means a refresh-token exchange. This is why --no-keep cleanup works reliably even after hour-long sessions.
openshellctl token show # display token subject, expiry, roles
openshellctl token refresh # force a token refresh
openshellctl token refresh --write # refresh and persist to oidc_token.json
openshellctl token inspect <JWT> # decode a JWT's claims (no sig verify)If a command fails with an expired or invalid token, openshellctl prints:
Error: invalid token: ExpiredSignature
Hint: try `openshellctl token refresh` to obtain a new token.
Configuration is resolved in priority order:
- CLI flags (highest)
- Environment variables (
OPENSHELL_*) - Config file (
$XDG_CONFIG_HOME/openshellctl/config.yaml, override with--config) - Gateway config dirs (
$XDG_CONFIG_HOME/openshell/gateways/<name>/)
These flags apply to every command:
| Flag | Env var | Description |
|---|---|---|
--gateway, -g |
OPENSHELL_GATEWAY |
Gateway name (selects config directory) |
--gateway-endpoint |
OPENSHELL_GATEWAY_ENDPOINT |
Gateway gRPC endpoint URL |
--gateway-insecure |
OPENSHELL_GATEWAY_INSECURE |
Skip TLS verification |
--workspace |
OPENSHELL_WORKSPACE |
Workspace name (default: default) |
--token |
OPENSHELL_TOKEN |
Bearer token (overrides OIDC) |
--config |
— | Config file path |
--verbose, -v |
— | Increase verbosity (repeat for more) |
--no-color |
NO_COLOR |
Disable coloured output |
--oidc-issuer |
— | OIDC issuer URL override |
--oidc-client-id |
— | OIDC client ID override |
--oidc-audience |
— | OIDC audience override |
--oidc-scopes |
— | OIDC scopes (space-separated) |
--client-secret-file |
— | File containing the OIDC client secret |
--token-leeway |
— | Token expiry leeway (default: 30s) |
--write-token |
— | Write refreshed tokens to disk (Rust CLI schema) |
Create a sandbox and optionally connect to it.
# Create from a community image
openshellctl sandbox create --from python
openshellctl sandbox create --from python --name my-sandbox
# Create with GPU
openshellctl sandbox create --from pytorch --gpu # driver default count
openshellctl sandbox create --from pytorch --gpu 2 # explicit count
# Create from a full image reference
openshellctl sandbox create --from ghcr.io/org/image:tag
# Create from a manifest file (everything in one file)
openshellctl sandbox create -f sandbox.yaml
cat sandbox.yaml | openshellctl sandbox create -f -
# Create with uploads and port forwarding
openshellctl sandbox create --from python \
--upload ./src:/workspace \
--upload ./data:/data \
--forward 8080 \
--no-git-ignore
# Create and run a command
openshellctl sandbox create --from python -- python3 main.py
# Create without attaching
openshellctl sandbox create --from python --detach
# Output as JSON/YAML instead of table
openshellctl sandbox create --from python -o json| Flag | Description |
|---|---|
--from |
Image reference or community name (e.g. python, pytorch) |
--name |
Sandbox name (auto-generated if omitted; max 19 chars) |
-f, --file |
Manifest YAML file (- for stdin) |
--gpu [COUNT] |
Request GPU resources (bare = driver default, or specify count) |
--cpu |
CPU request (e.g. 500m, 2) |
--memory |
Memory request (e.g. 1Gi, 512Mi) |
--env |
Environment variable KEY=VALUE (repeatable) |
--label |
Label key=value (repeatable) |
--tty |
Force pseudo-terminal allocation |
--no-tty |
Disable pseudo-terminal (last-wins with --tty) |
--upload |
Upload local[:dest] after create (repeatable) |
--no-git-ignore |
Disable .gitignore filtering for uploads |
--forward |
Forward [bind:]port to sandbox |
--detach |
Return after create without attaching |
--provider |
Provider to attach (repeatable) |
--auto-providers |
Enable auto provider detection |
--no-auto-providers |
Disable auto provider detection |
--no-credential-warnings |
Suppress credential exposure warnings |
--approval-mode |
Proposal approval mode: manual (default) or auto |
--no-keep |
Delete the sandbox after the session ends |
--driver-config-json |
Driver config as inline JSON |
--policy |
Sandbox policy file (env: OPENSHELL_SANDBOX_POLICY) |
-o, --output |
Output format: table (default), json, yaml |
A manifest file (-f) lets you put everything — sandbox spec, command, and session behavior — into one YAML file:
apiVersion: openshell.managed.openshift.io/v1alpha1
kind: Sandbox
metadata:
name: sop-improve
labels:
team: sre-platform
spec:
image: quay.io/redhat-services-prod/rosa-tenant/rosa-agent/rosa-agent:latest
command: ["claude", "/job-sop-improve"]
providerRefs:
- name: rosa-general-vertex
- name: rosa-agent-github
- name: rosa-agent-jira
env:
ANTHROPIC_BASE_URL: https://inference.local
ANTHROPIC_API_KEY: unused
JIRA_EMAIL: sd-sre-platform+rosa-agent@redhat.com
JIRA_BASE_URL: https://redhat.atlassian.net
resources:
cpu: "2"
memory: 4Gi
gpu:
count: 1
upload:
- local: ./src
dest: /workspace
sessionOpts:
noKeep: true
forward: "8080"
approvalMode: auto
output: tableThen create and delete are both one-liners:
openshellctl sandbox create -f sop-improve.yaml
openshellctl sandbox delete -f sop-improve.yamlspec.command works like a Kubernetes pod spec — it's the command (with arguments) to run inside the sandbox. If omitted, the sandbox's default entrypoint (/bin/bash -l) is used.
spec.sessionOpts groups client-side session behavior:
| Field | Type | Description |
|---|---|---|
noKeep |
bool | Delete the sandbox when the session ends |
detach |
bool | Return after create, do not attach |
forward |
string | [bind:]port to forward to the sandbox |
approvalMode |
string | manual (default) or auto |
output |
string | table (default), json, or yaml |
These fields are ignored by the operator — they control what the CLI does after creation. CLI flags always override manifest values.
spec.policy (inline map) and spec.policyFile (path string) are mutually exclusive. spec.policyFile is resolved relative to the manifest file's directory; when the manifest is read from stdin, policyFile must be an absolute path.
Show details of a single sandbox.
openshellctl sandbox get my-sandbox
openshellctl sandbox get my-sandbox -o json
openshellctl sandbox get my-sandbox -o yaml
openshellctl sandbox get -f sandbox.yaml # read name from manifest
openshellctl sandbox get # uses last-used sandbox| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
-o, --output |
Output format: table (default), json, yaml |
List sandboxes in the current workspace.
openshellctl sandbox list
openshellctl sandbox list -o json
openshellctl sandbox list --all-workspaces
openshellctl sandbox list --names # one name per line
openshellctl sandbox list --ids # one ID per line
openshellctl sandbox list --selector "gpu=true"
openshellctl sandbox list --limit 10 --offset 20| Flag | Description |
|---|---|
-o, --output |
Output format: table (default), json, yaml |
--all-workspaces |
List across all workspaces |
--names |
Print names only (one per line) |
--ids |
Print IDs only (one per line) |
--selector |
Label selector filter |
--limit |
Max results (default: 100) |
--offset |
Result offset for pagination |
Delete one or more sandboxes. Supports -f to read the sandbox name from a manifest, like kubectl delete -f.
openshellctl sandbox delete my-sandbox
openshellctl sandbox delete sb-1 sb-2 sb-3
openshellctl sandbox delete -f sandbox.yaml # reads name from manifest
openshellctl sandbox delete --all
openshellctl sandbox delete my-sandbox --wait
openshellctl sandbox delete my-sandbox --wait --wait-timeout 2m| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
--all |
Delete all sandboxes in the workspace |
--wait |
Wait for deletion to complete |
--wait-timeout |
Wait timeout (default: 5m) |
Stop or start a sandbox.
openshellctl sandbox stop my-sandbox
openshellctl sandbox start my-sandbox
openshellctl sandbox stop -f sandbox.yaml # read name from manifest
openshellctl sandbox start -f sandbox.yaml
openshellctl sandbox stop # uses last-used sandbox
openshellctl sandbox start # uses last-used sandbox| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
Run a command inside a sandbox. The command must follow --.
openshellctl sandbox exec --name my-sandbox -- ls -la
openshellctl sandbox exec --name my-sandbox -- python3 -c "print('hello')"
# With environment variables
openshellctl sandbox exec --name my-sandbox --env FOO=bar -- env
# With working directory
openshellctl sandbox exec --name my-sandbox --workdir /workspace -- make build
# With timeout (seconds)
openshellctl sandbox exec --name my-sandbox --timeout 60 -- long-running-task
# Force/disable TTY
openshellctl sandbox exec --name my-sandbox --tty -- bash
openshellctl sandbox exec --name my-sandbox --no-tty -- cat /etc/os-release
# Interactive mode (stdin is forwarded when --tty + terminal detected)
openshellctl sandbox exec --name my-sandbox --tty -- bash| Flag | Description |
|---|---|
-n, --name |
Sandbox name (defaults to last-used) |
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
--env |
Environment variable KEY=VALUE (repeatable) |
--workdir |
Working directory inside the sandbox |
--timeout |
Timeout in seconds (0 = none) |
--tty |
Force pseudo-terminal allocation |
--no-tty |
Disable pseudo-terminal (last-wins with --tty) |
The process exit code matches the remote command's exit code.
Open an interactive shell session to a sandbox.
openshellctl sandbox connect my-sandbox
openshellctl sandbox connect -f sandbox.yaml # read name from manifest
openshellctl sandbox connect # uses last-used sandbox| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
Detach from the session with Ctrl-P Ctrl-Q (the connection closes cleanly and exits 0).
Upload files or directories to a sandbox.
openshellctl sandbox upload my-sandbox ./myproject
openshellctl sandbox upload my-sandbox ./myproject /workspace
openshellctl sandbox upload my-sandbox ./file.txt /remote/path/file.txt
openshellctl sandbox upload -f sandbox.yaml ./src /workspaceArguments are NAME LOCAL_PATH [DEST]. If DEST is omitted, files are placed in the sandbox's home directory. Directory uploads use scp-like semantics (./myproject → ~/myproject/).
By default, .gitignore rules are respected for directory uploads. Use --no-git-ignore to include all files.
| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
--no-git-ignore |
Disable .gitignore filtering for directory uploads |
Download files or directories from a sandbox.
openshellctl sandbox download my-sandbox /workspace/output
openshellctl sandbox download my-sandbox /workspace/output ./local-dir
openshellctl sandbox download -f sandbox.yaml /workspace/outputArguments are NAME SANDBOX_PATH [DEST]. If DEST is omitted, files are placed in the current directory.
| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
Print an SSH configuration block for use with the upstream openshell binary.
openshellctl sandbox ssh-config my-sandbox
openshellctl sandbox ssh-config -f sandbox.yaml # read name from manifest
openshellctl sandbox ssh-config # uses last-used sandbox| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
This outputs a block suitable for ~/.ssh/config, using openshellctl ssh-proxy as the ProxyCommand.
Manage providers attached to a sandbox.
# List providers
openshellctl sandbox provider list my-sandbox
openshellctl sandbox provider list -f sandbox.yaml
# Attach a provider
openshellctl sandbox provider attach my-sandbox github
# Detach a provider
openshellctl sandbox provider detach my-sandbox githubAttach and detach use optimistic concurrency — if another operation modifies the sandbox concurrently, you will see:
Failed to attach provider: sandbox was modified by another operation.
Please retry the command.
View sandbox logs. This is a top-level command (alias: lg).
openshellctl logs my-sandbox
openshellctl logs -f sandbox.yaml # read name from manifest
openshellctl logs my-sandbox --tail # stream live
openshellctl logs my-sandbox -n 50 # last 50 lines
openshellctl logs my-sandbox --since 5m # last 5 minutes
openshellctl logs my-sandbox --level error # errors only
openshellctl logs my-sandbox --source gateway # gateway logs only| Flag | Description |
|---|---|
-f, --file |
Manifest file to read sandbox name from (- for stdin) |
--tail |
Stream live logs |
-n |
Number of log lines (default: 200) |
--since |
Show logs from this duration ago (e.g. 5m, 1h, 30s) |
--level |
Minimum level: error, warn, info, debug, trace |
--source |
Filter by source: gateway, sandbox, or all (default) |
Inspect and manage the gateway authentication token.
openshellctl token show # show token metadata
openshellctl token refresh # force refresh
openshellctl token refresh --write # refresh and persist
openshellctl token inspect <JWT> # decode JWT claimsClient-side validation of a sandbox policy file.
openshellctl policy lint policy.yamlChecks cover process uid/gid ranges, filesystem path constraints, middleware selectors, endpoint ports, and TCP access rules. The gateway's server-side validation remains authoritative.
Print build information.
openshellctl versionShows: version, commit, OpenShell pin version, and SDK version.
Generate shell completion scripts.
openshellctl completion bash
openshellctl completion zsh
openshellctl completion fish
openshellctl completion powershell| Command | Alias |
|---|---|
sandbox |
sb |
logs |
lg |
openshellctl sb list # same as: openshellctl sandbox list
openshellctl lg my-sandbox # same as: openshellctl logs my-sandboxCommands that take a sandbox name remember the last-used sandbox per gateway and workspace. If you omit the name argument, the last-used sandbox is used automatically:
openshellctl sandbox exec --name my-sandbox -- whoami # saves my-sandbox
openshellctl sandbox exec -- whoami # reuses my-sandbox
openshellctl sandbox connect # reuses my-sandbox
openshellctl logs # reuses my-sandbox| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic / RPC error |
| 2 | Usage error (bad flags, validation failure, invalid argument rejected by gateway) |
| 3 | Authentication failure (expired/invalid token, missing credentials) |
| 4 | Not found (sandbox, gateway) |
| 5 | Conflict (already exists, concurrent modification) |
| 6 | Provisioning failure or timeout |
| N | Remote process exit code (from exec/connect/create with attached command) |
| Variable | Description |
|---|---|
OPENSHELL_GATEWAY |
Default gateway name |
OPENSHELL_GATEWAY_ENDPOINT |
Gateway endpoint URL |
OPENSHELL_GATEWAY_INSECURE |
Skip TLS verification (true/1) |
OPENSHELL_WORKSPACE |
Default workspace |
OPENSHELL_TOKEN |
Bearer token (overrides all other auth) |
OPENSHELL_OIDC_CLIENT_SECRET |
OIDC client secret (enables service account auth) |
OPENSHELL_OIDC_CLIENT_ID |
OIDC client ID override (default from gateway metadata) |
OPENSHELL_OIDC_ISSUER |
OIDC issuer URL override (default from gateway metadata) |
OPENSHELL_SANDBOX_POLICY |
Default sandbox policy file path |
NO_COLOR |
Disable coloured output (any value) |
openshellctl is a compatible reimplementation with these intentional differences:
- Pure Go, zero dependencies: no shell-out to
ssh,tar,git, or any other binary — including the upstreamopenshellCLI. All SSH transfer, tar streaming, and gitignore filtering are implemented in-process. Single static binary. - Per-request auth with automatic retry: tokens are resolved per-RPC (not baked in at startup). On
Unauthenticated, the token is refreshed (or re-minted for service accounts) and the request retried. Long-running sessions and--no-keepcleanup work reliably even after token expiry. - Embeddable:
pkg/gateway,pkg/transfer,pkg/sandbox, andpkg/authare importable Go packages with interface-driven I/O seams, suitable for use in operators and controllers. - No interactive spinner: provisioning progress uses plain text output instead of terminal spinners.
policy lintis a separate top-level command (not undersandbox).- Gitignore filtering: uses compiled
.gitignorerules instead of shelling out togit ls-files. Tracked-but-ignored files are excluded (matching untracked behaviour) rather than re-included.
See LICENSE.