diff --git a/.agents/skills/debug-navigator-cluster/SKILL.md b/.agents/skills/debug-navigator-cluster/SKILL.md
index f3eb51531..714dbcd94 100644
--- a/.agents/skills/debug-navigator-cluster/SKILL.md
+++ b/.agents/skills/debug-navigator-cluster/SKILL.md
@@ -13,8 +13,8 @@ Diagnose why a openshell cluster failed to start after `openshell gateway start`
 
 1. **Pre-deploy check**: `openshell gateway start` in interactive mode prompts to **reuse** (keep volume, clean stale nodes) or **recreate** (destroy everything, fresh start). `mise run cluster` always recreates before deploy.
 2. Ensure cluster image is available (local build or remote pull)
-3. Create Docker network (`navigator-cluster`) and volume (`navigator-cluster-{name}`)
-4. Create and start a privileged Docker container (`navigator-cluster-{name}`)
+3. Create Docker network (`openshell-cluster`) and volume (`openshell-cluster-{name}`)
+4. Create and start a privileged Docker container (`openshell-cluster-{name}`)
 5. Wait for k3s to generate kubeconfig (up to 60s)
 6. **Clean stale nodes**: Remove any `NotReady` k3s nodes left over from previous container instances that reused the same persistent volume
 7. **Prepare local images** (if `OPENSHELL_PUSH_IMAGES` is set): In `internal` registry mode, bootstrap waits for the in-cluster registry and pushes tagged images there. In `external` mode, bootstrap uses legacy `ctr -n k8s.io images import` push-mode behavior.
@@ -35,7 +35,7 @@ The host port is configurable via `--port` on `openshell gateway start` (default
 
 The TCP host is also added as an extra gateway TLS SAN so mTLS hostname validation succeeds.
 
-The default cluster name is `openshell`. The container is `navigator-cluster-{name}`.
+The default cluster name is `openshell`. The container is `openshell-cluster-{name}`.
 
 ## Prerequisites
 
@@ -51,7 +51,7 @@ When the user asks to debug a cluster failure, **run diagnostics automatically**
 
 Before running commands, establish:
 
-1. **Cluster name**: Default is `openshell`, giving container name `navigator-cluster-openshell`
+1. **Cluster name**: Default is `openshell`, giving container name `openshell-cluster-openshell`
 2. **Remote or local**: If the user deployed with `--remote <host>`, all Docker commands must target that host
 3. **Config directory**: `~/.config/openshell/clusters/{name}/`
 
@@ -62,14 +62,14 @@ For remote clusters, prefix Docker commands with SSH:
 ssh <host> docker <command>
 
 # Remote kubectl inside the container
-ssh <host> docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl <command>'
+ssh <host> docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl <command>'
 ```
 
 For local clusters, run Docker commands directly:
 
 ```bash
 docker <command>
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl <command>'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl <command>'
 ```
 
 ### Step 1: Check Docker Container State
@@ -77,14 +77,14 @@ docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yam
 First, determine if the container exists and its state:
 
 ```bash
-docker ps -a --filter name=navigator-cluster- --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
+docker ps -a --filter name=openshell-cluster- --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
 ```
 
 If the container does not exist:
 
 ```bash
 # Check if the image is available
-docker images 'navigator/cluster*' --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'
+docker images 'openshell/cluster*' --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'
 ```
 
 If the image is missing, re-deploy so bootstrap can pull the published cluster image (or set `OPENSHELL_CLUSTER_IMAGE` explicitly).
@@ -92,7 +92,7 @@ If the image is missing, re-deploy so bootstrap can pull the published cluster i
 If the container exists but is not running, inspect it:
 
 ```bash
-docker inspect navigator-cluster-<name> --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'
+docker inspect openshell-cluster-<name> --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'
 ```
 
 - **OOMKilled=true**: The host doesn't have enough memory.
@@ -103,7 +103,7 @@ docker inspect navigator-cluster-<name> --format '{{.State.Status}} exit={{.Stat
 Get recent container logs to identify startup failures:
 
 ```bash
-docker logs navigator-cluster-<name> --tail 100
+docker logs openshell-cluster-<name> --tail 100
 ```
 
 Look for:
@@ -119,13 +119,13 @@ Verify k3s itself is functional:
 
 ```bash
 # API server readiness
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get --raw="/readyz"'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get --raw="/readyz"'
 
 # Node status
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide'
 
 # All pods
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A -o wide'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A -o wide'
 ```
 
 If `/readyz` fails, k3s is still starting or has crashed. Check container logs (Step 2).
@@ -138,16 +138,16 @@ The OpenShell server is deployed via a HelmChart CR as a StatefulSet with persis
 
 ```bash
 # StatefulSet status
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get statefulset/navigator -o wide'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get statefulset/navigator -o wide'
 
 # OpenShell pod logs
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator logs statefulset/navigator --tail=100'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator logs statefulset/navigator --tail=100'
 
 # Describe statefulset for events
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator describe statefulset/navigator'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator describe statefulset/navigator'
 
 # Helm install job logs (the job that installs the OpenShell chart)
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n kube-system logs -l job-name=helm-install-navigator --tail=200'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n kube-system logs -l job-name=helm-install-navigator --tail=200'
 ```
 
 Common issues:
@@ -162,15 +162,15 @@ The Envoy Gateway provides HTTP/gRPC ingress:
 
 ```bash
 # Gateway status
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get gateway/navigator-gateway'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get gateway/navigator-gateway'
 
 # Check port bindings on the host
-docker port navigator-cluster-<name>
+docker port openshell-cluster-<name>
 ```
 
 Expected ports: `6443/tcp`, `30051/tcp` (mapped to configurable host port, default 8080; set via `--port` on deploy).
 Only one local cluster can run on a Docker host at a time because `6443` is fixed.
-`mise run cluster` handles this by removing conflicting local `navigator-cluster-*` containers first.
+`mise run cluster` handles this by removing conflicting local `openshell-cluster-*` containers first.
 
 If ports are missing or conflicting, another process may be using them. Check with:
 
@@ -185,37 +185,37 @@ If using Docker-in-Docker (`DOCKER_HOST=tcp://docker:2375`), verify metadata poi
 
 Component images (server, sandbox, pki-job) can reach kubelet via two paths:
 
-**Local/external pull mode** (default local via `mise run cluster` / `mise run cluster:build`): Local images are tagged to the configured local registry base (default `127.0.0.1:5000/navigator/*`), pushed to that registry, and pulled by k3s via `registries.yaml` mirror endpoint (typically `host.docker.internal:5000`). `cluster:build` builds then pushes images; `cluster` pushes prebuilt local tags (`navigator/*:dev`, falling back to `localhost:5000/navigator/*:dev` or `127.0.0.1:5000/navigator/*:dev`).
+**Local/external pull mode** (default local via `mise run cluster`): Local images are tagged to the configured local registry base (default `127.0.0.1:5000/openshell/*`), pushed to that registry, and pulled by k3s via `registries.yaml` mirror endpoint (typically `host.docker.internal:5000`). The `cluster` task pushes prebuilt local tags (`openshell/*:dev`, falling back to `localhost:5000/openshell/*:dev` or `127.0.0.1:5000/openshell/*:dev`).
 
 ```bash
 # Verify image refs currently used by openshell deployment
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get deploy navigator -o jsonpath="{.spec.template.spec.containers[*].image}"'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get deploy navigator -o jsonpath="{.spec.template.spec.containers[*].image}"'
 
 # Verify registry mirror/auth endpoint configuration
-docker exec navigator-cluster-<name> cat /etc/rancher/k3s/registries.yaml
+docker exec openshell-cluster-<name> cat /etc/rancher/k3s/registries.yaml
 ```
 
-**Legacy push mode** (`mise run cluster:push`): Images are imported into the k3s containerd `k8s.io` namespace.
+**Legacy push mode**: Images are imported into the k3s containerd `k8s.io` namespace.
 
 ```bash
 # Check if images were imported into containerd (k3s default namespace is k8s.io)
-docker exec navigator-cluster-<name> ctr -a /run/k3s/containerd/containerd.sock images ls | grep navigator
+docker exec openshell-cluster-<name> ctr -a /run/k3s/containerd/containerd.sock images ls | grep navigator
 ```
 
 If images are missing, re-import with:
 
 ```bash
-docker save <image-ref> | docker exec -i navigator-cluster-<name> ctr -a /run/k3s/containerd/containerd.sock images import -
+docker save <image-ref> | docker exec -i openshell-cluster-<name> ctr -a /run/k3s/containerd/containerd.sock images import -
 ```
 
 **External pull mode** (remote deploy, or local with `OPENSHELL_REGISTRY_HOST`/`IMAGE_REPO_BASE` pointing at a non-local registry): Images are pulled from an external registry at runtime. The entrypoint generates `/etc/rancher/k3s/registries.yaml`.
 
 ```bash
 # Verify registries.yaml exists and has credentials
-docker exec navigator-cluster-<name> cat /etc/rancher/k3s/registries.yaml
+docker exec openshell-cluster-<name> cat /etc/rancher/k3s/registries.yaml
 
 # Test pulling an image manually from inside the cluster
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml crictl pull ghcr.io/nvidia/nemoclaw/server:latest'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml crictl pull ghcr.io/nvidia/nemoclaw/server:latest'
 ```
 
 If `registries.yaml` is missing or has wrong values, verify env wiring (`OPENSHELL_REGISTRY_HOST`, `OPENSHELL_REGISTRY_INSECURE`, username/password for authenticated registries).
@@ -226,10 +226,10 @@ TLS certificates are generated by the `navigator-bootstrap` crate (using `rcgen`
 
 ```bash
 # Check if the three TLS secrets exist
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls navigator-server-client-ca navigator-client-tls'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls navigator-server-client-ca navigator-client-tls'
 
 # Inspect server cert expiry (if openssl is available in the container)
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls -o jsonpath="{.data.tls\.crt}" | base64 -d | openssl x509 -noout -dates 2>/dev/null || echo "openssl not available"'
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls -o jsonpath="{.data.tls\.crt}" | base64 -d | openssl x509 -noout -dates 2>/dev/null || echo "openssl not available"'
 
 # Check if CLI-side mTLS files exist locally
 ls -la ~/.config/openshell/clusters/<name>/mtls/
@@ -247,7 +247,7 @@ Common mTLS issues:
 Events catch scheduling failures, image pull errors, and resource issues:
 
 ```bash
-docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get events -A --sort-by=.lastTimestamp' | tail -n 50
+docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get events -A --sort-by=.lastTimestamp' | tail -n 50
 ```
 
 Look for:
@@ -264,13 +264,13 @@ DNS misconfiguration is a common root cause, especially on remote/Linux hosts:
 
 ```bash
 # Check the resolv.conf k3s is using
-docker exec navigator-cluster-<name> cat /etc/rancher/k3s/resolv.conf
+docker exec openshell-cluster-<name> cat /etc/rancher/k3s/resolv.conf
 
 # Test DNS resolution from inside the container
-docker exec navigator-cluster-<name> sh -c 'nslookup google.com || wget -q -O /dev/null http://google.com && echo "network ok" || echo "network unreachable"'
+docker exec openshell-cluster-<name> sh -c 'nslookup google.com || wget -q -O /dev/null http://google.com && echo "network ok" || echo "network unreachable"'
 
 # Check the entrypoint's DNS decision (in container logs)
-docker logs navigator-cluster-<name> 2>&1 | head -20
+docker logs openshell-cluster-<name> 2>&1 | head -20
 ```
 
 The entrypoint script selects DNS resolvers in this priority:
@@ -317,15 +317,15 @@ For clusters deployed with `--remote <host>`, all commands must target the remot
 
 ```bash
 ssh <host> docker ps -a
-ssh <host> docker logs navigator-cluster-<name>
-ssh <host> docker exec navigator-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A'
+ssh <host> docker logs openshell-cluster-<name>
+ssh <host> docker exec openshell-cluster-<name> sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A'
 ```
 
 **Option B: Docker SSH context**:
 
 ```bash
 docker -H ssh://<host> ps -a
-docker -H ssh://<host> logs navigator-cluster-<name>
+docker -H ssh://<host> logs openshell-cluster-<name>
 ```
 
 **Setting up kubectl access** (requires tunnel):
@@ -344,7 +344,7 @@ Run all diagnostics at once for a comprehensive report:
 ```bash
 HOST="<host>"  # leave empty for local, or set to SSH destination
 NAME="openshell"  # cluster name
-CONTAINER="navigator-cluster-${NAME}"
+CONTAINER="openshell-cluster-${NAME}"
 KCFG="KUBECONFIG=/etc/rancher/k3s/k3s.yaml"
 
 # Helper: run docker command locally or remotely
diff --git a/.agents/skills/nemoclaw-cli/SKILL.md b/.agents/skills/nemoclaw-cli/SKILL.md
index b7db60e52..02e383ae2 100644
--- a/.agents/skills/nemoclaw-cli/SKILL.md
+++ b/.agents/skills/nemoclaw-cli/SKILL.md
@@ -436,11 +436,11 @@ openshell sandbox delete work-session
 
 ---
 
-## Workflow 7: Cluster Inference
+## Workflow 7: Gateway Inference
 
-Configure the cluster's managed inference route for `inference.local`.
+Configure the gateway's managed inference route for `inference.local`.
 
-### Set cluster inference
+### Set gateway inference
 
 First ensure the provider record exists:
 
@@ -448,20 +448,20 @@ First ensure the provider record exists:
 openshell provider list
 ```
 
-Then point cluster inference at that provider and model:
+Then point gateway inference at that provider and model:
 
 ```bash
-openshell cluster inference set \
+openshell inference set \
   --provider nvidia \
   --model nvidia/nemotron-3-nano-30b-a3b
 ```
 
-This updates the cluster-managed `inference.local` route. There is no per-route create/list/update/delete workflow for sandbox inference.
+This updates the gateway-managed `inference.local` route. There is no per-route create/list/update/delete workflow for sandbox inference.
 
 ### Inspect current inference config
 
 ```bash
-openshell cluster inference get
+openshell inference get
 ```
 
 ### How sandboxes use it
@@ -549,8 +549,8 @@ $ openshell sandbox upload --help
 | Download files from sandbox | `openshell sandbox download <name> <path>` |
 | Create provider | `openshell provider create --name N --type T --from-existing` |
 | List providers | `openshell provider list` |
-| Configure cluster inference | `openshell cluster inference set --provider P --model M` |
-| View cluster inference | `openshell cluster inference get` |
+| Configure gateway inference | `openshell inference set --provider P --model M` |
+| View gateway inference | `openshell inference get` |
 | Delete sandbox | `openshell sandbox delete <name>` |
 | Destroy cluster | `openshell gateway destroy` |
 | Self-teach any command | `openshell <group> <cmd> --help` |
diff --git a/.agents/skills/nemoclaw-cli/cli-reference.md b/.agents/skills/nemoclaw-cli/cli-reference.md
index af12ca0f0..82a261139 100644
--- a/.agents/skills/nemoclaw-cli/cli-reference.md
+++ b/.agents/skills/nemoclaw-cli/cli-reference.md
@@ -9,13 +9,13 @@ Quick-reference for the `openshell` command-line interface. For workflow guidanc
 | Flag | Description |
 |------|-------------|
 | `-v`, `--verbose` | Increase verbosity (`-v` = info, `-vv` = debug, `-vvv` = trace) |
-| `-g`, `--gateway <NAME>` | Gateway to operate on. Also settable via `OPENSHELL_CLUSTER` env var. Falls back to active gateway in `~/.config/openshell/active_cluster`. |
+| `-g`, `--gateway <NAME>` | Gateway to operate on. Also settable via `OPENSHELL_GATEWAY` env var. Falls back to active gateway in `~/.config/openshell/active_gateway`. |
 
 ## Environment Variables
 
 | Variable | Description |
 |----------|-------------|
-| `OPENSHELL_CLUSTER` | Override active gateway name (same as `--gateway`) |
+| `OPENSHELL_GATEWAY` | Override active gateway name (same as `--gateway`) |
 | `OPENSHELL_SANDBOX_POLICY` | Path to default sandbox policy YAML (fallback when `--policy` is not provided) |
 
 ---
@@ -122,7 +122,7 @@ Print or start an SSH tunnel for kubectl access to a remote cluster.
 
 ### `openshell gateway select [name]`
 
-Set the active gateway. Writes to `~/.config/openshell/active_cluster`. When called without arguments, lists all provisioned gateways with the active one marked with `*`.
+Set the active gateway. Writes to `~/.config/openshell/active_gateway`. When called without arguments, lists all provisioned gateways with the active one marked with `*`.
 
 ---
 
@@ -319,29 +319,29 @@ Delete one or more providers by name.
 
 ---
 
-## Cluster Inference Commands
+## Inference Commands
 
-### `openshell cluster inference set`
+### `openshell inference set`
 
-Configure the managed cluster inference route used by `inference.local`. Both flags are required.
+Configure the managed gateway inference route used by `inference.local`. Both flags are required.
 
 | Flag | Default | Description |
 |------|---------|-------------|
 | `--provider <NAME>` | -- | Provider record name (required) |
 | `--model <ID>` | -- | Model identifier to use for generation requests (required) |
 
-### `openshell cluster inference update`
+### `openshell inference update`
 
-Partially update the cluster inference configuration. Fetches the current config and applies only the provided overrides. At least one flag is required.
+Partially update the gateway inference configuration. Fetches the current config and applies only the provided overrides. At least one flag is required.
 
 | Flag | Default | Description |
 |------|---------|-------------|
 | `--provider <NAME>` | unchanged | Provider record name |
 | `--model <ID>` | unchanged | Model identifier |
 
-### `openshell cluster inference get`
+### `openshell inference get`
 
-Show the current cluster inference configuration.
+Show the current gateway inference configuration.
 
 ---
 
diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md
index f1ec47906..3eaa6d7c7 100644
--- a/.agents/skills/tui-development/SKILL.md
+++ b/.agents/skills/tui-development/SKILL.md
@@ -24,22 +24,22 @@ The OpenShell TUI is a ratatui-based terminal UI for the OpenShell platform. It
 
 ## 2. Domain Object Hierarchy
 
-The data model follows a strict hierarchy: **Cluster > Sandboxes > Logs**.
+The data model follows a strict hierarchy: **Gateway > Sandboxes > Logs**.
 
 ```
-Cluster (discovered via navigator_bootstrap::list_clusters())
+Gateway (discovered via navigator_bootstrap::list_gateways())
   └── Sandboxes (fetched via gRPC ListSandboxes)
         └── Logs (fetched via GetSandboxLogs + streamed via WatchSandbox)
 ```
 
-- **Clusters** are discovered from on-disk config via `navigator_bootstrap::list_clusters()`. Each cluster has a name, gateway endpoint, and local/remote flag.
+- **Gateways** are discovered from on-disk config via `navigator_bootstrap::list_gateways()`. Each gateway has a name, endpoint, and local/remote flag.
 - **Sandboxes** belong to the active cluster. Fetched via `ListSandboxes` gRPC call with a periodic tick refresh. Each sandbox has: `id`, `name`, `phase`, `created_at_ms`, and `spec.template.image`.
 - **Logs** belong to a single sandbox. Initial batch fetched via `GetSandboxLogs` (500 lines), then live-tailed via `WatchSandbox` with `follow_logs: true`.
 
 The **title bar** always reflects this hierarchy, reading left-to-right from general to specific:
 
 ```
- OpenShell │ Current Cluster: <name> (<status>) │ <screen/context>
+ OpenShell │ Current Gateway: <name> (<status>) │ <screen/context>
 ```
 
 ## 3. Navigation & Screen Architecture
@@ -50,7 +50,7 @@ Top-level layouts that own the full content area. Each has its own nav bar hints
 
 | Screen | Description | Module |
 | --- | --- | --- |
-| `Dashboard` | Cluster list (top) + sandbox table (bottom) | `ui/dashboard.rs` |
+| `Dashboard` | Gateway list (top) + sandbox table (bottom) | `ui/dashboard.rs` |
 | `Sandbox` | Single-sandbox view — detail or logs depending on `Focus` | `ui/sandbox_detail.rs`, `ui/sandbox_logs.rs` |
 
 ### Focus (`Focus` enum)
@@ -59,7 +59,7 @@ Tracks which panel currently receives keyboard input.
 
 | Focus | Screen | Description |
 | --- | --- | --- |
-| `Clusters` | Dashboard | Cluster list panel has input focus |
+| `Gateways` | Dashboard | Gateway list panel has input focus |
 | `Sandboxes` | Dashboard | Sandbox table panel has input focus |
 | `SandboxDetail` | Sandbox | Sandbox detail view (name, status, image, age) |
 | `SandboxLogs` | Sandbox | Log viewer with structured rendering |
@@ -104,8 +104,8 @@ Every frame renders four vertical regions:
 
 ### Title bar examples
 
-- Dashboard: ` OpenShell │ Current Cluster: openshell (Healthy) │ Dashboard`
-- Sandbox detail: ` OpenShell │ Current Cluster: openshell (Healthy) │ Sandbox: my-sandbox`
+- Dashboard: ` OpenShell │ Current Gateway: openshell (Healthy) │ Dashboard`
+- Sandbox detail: ` OpenShell │ Current Gateway: openshell (Healthy) │ Sandbox: my-sandbox`
 
 ### Adding a new screen
 
@@ -205,7 +205,7 @@ All colors and styles are defined in `crates/navigator-tui/src/theme.rs`.
 
 #### Visual conventions
 
-- **Selected row**: Green `▌` left-border marker on the selected row. Active cluster also gets a green `●` dot.
+- **Selected row**: Green `▌` left-border marker on the selected row. Active gateway also gets a green `●` dot.
 - **Focused panel**: Border changes from `EVERGLADE` to `NVIDIA_GREEN`.
 - **Status indicators**: Green for healthy/ready/info, yellow for degraded/provisioning/warn, red for unhealthy/error.
 - **Separators**: Muted `│` characters between title bar segments and nav bar sections.
@@ -290,7 +290,7 @@ All actions are accessible via keyboard shortcuts displayed in the nav bar. The
 
 ### Screen-specific key hints
 
-**Dashboard (Clusters focus):**
+**Dashboard (Gateways focus):**
 `[Tab] Switch Panel  [Enter] Select  [j/k] Navigate  │  [:] Command  [q] Quit`
 
 **Dashboard (Sandboxes focus):**
@@ -307,12 +307,12 @@ Same as above.
 | File | Purpose |
 | --- | --- |
 | `crates/navigator-tui/Cargo.toml` | Crate manifest — dependencies on `navigator-core`, `navigator-bootstrap`, `ratatui`, `crossterm`, `tonic`, `tokio` |
-| `crates/navigator-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), cluster switching, mTLS channel building |
-| `crates/navigator-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `ClusterEntry`, all key handling logic |
+| `crates/navigator-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), gateway switching, mTLS channel building |
+| `crates/navigator-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `GatewayEntry`, all key handling logic |
 | `crates/navigator-tui/src/event.rs` | `Event` enum (`Key`, `Mouse`, `Tick`, `Resize`, `LogLines`), `EventHandler` with mpsc channels and crossterm polling |
 | `crates/navigator-tui/src/theme.rs` | `colors` module (NVIDIA_GREEN, EVERGLADE, BG, FG) and `styles` module (all `Style` constants) |
 | `crates/navigator-tui/src/ui/mod.rs` | Top-level `draw()` dispatcher, `draw_title_bar`, `draw_nav_bar`, `draw_command_bar`, screen routing |
-| `crates/navigator-tui/src/ui/dashboard.rs` | Dashboard screen — cluster list table (top) + sandbox table (bottom) |
+| `crates/navigator-tui/src/ui/dashboard.rs` | Dashboard screen — gateway list table (top) + sandbox table (bottom) |
 | `crates/navigator-tui/src/ui/sandboxes.rs` | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image |
 | `crates/navigator-tui/src/ui/sandbox_detail.rs` | Sandbox detail view — name, status, image, created, age, delete confirmation dialog |
 | `crates/navigator-tui/src/ui/sandbox_logs.rs` | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter |
@@ -336,8 +336,8 @@ lib.rs (event loop, gRPC, async tasks)
 
 ### Dependency constraints
 
-- **`navigator-tui` cannot depend on `navigator-cli`** — this would create a circular dependency. TLS channel building for cluster switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`).
-- mTLS certs are read from `~/.config/openshell/clusters/<name>/mtls/` (ca.crt, tls.crt, tls.key).
+- **`navigator-tui` cannot depend on `navigator-cli`** — this would create a circular dependency. TLS channel building for gateway switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`).
+- mTLS certs are read from `~/.config/openshell/gateways/<name>/mtls/` (ca.crt, tls.crt, tls.key).
 
 ### Proto generated code
 
@@ -389,12 +389,12 @@ The connect timeout for cluster switching is 10 seconds with HTTP/2 keepalive at
 6. Auto-scroll kicks in if the user is near the bottom (within 5 lines)
 7. Stream is cancelled when user presses `Esc` or navigates away (handle is `.abort()`ed)
 
-### Cluster switching lifecycle
+### Gateway switching lifecycle
 
-1. User selects a different cluster and presses `Enter` → `pending_cluster_switch = Some(name)`
-2. Event loop calls `handle_cluster_switch()`
-3. New mTLS channel is built via `connect_to_cluster()`
-4. On success: `app.client` is replaced, `reset_sandbox_state()` clears all sandbox data, `refresh_data()` fetches health + sandboxes for the new cluster
+1. User selects a different gateway and presses `Enter` → `pending_gateway_switch = Some(name)`
+2. Event loop calls `handle_gateway_switch()`
+3. New mTLS channel is built via `connect_to_gateway()`
+4. On success: `app.client` is replaced, `reset_sandbox_state()` clears all sandbox data, `refresh_data()` fetches health + sandboxes for the new gateway
 5. On failure: `status_text` shows the error
 
 ## 9. Development Workflow
@@ -426,9 +426,9 @@ Always run before committing:
 mise run pre-commit
 ```
 
-### Cluster changes
+### Gateway changes
 
-If you change sandbox or server code that affects the backend, redeploy the cluster:
+If you change sandbox or server code that affects the backend, redeploy the gateway:
 
 ```bash
 mise run cluster:deploy all
diff --git a/architecture/build-containers.md b/architecture/build-containers.md
index bffc3adbb..b5ac8aa8b 100644
--- a/architecture/build-containers.md
+++ b/architecture/build-containers.md
@@ -56,7 +56,7 @@ tasks/
 
 The project produces three runtime container images and two build-only wheel images.
 
-### Sandbox Image (`navigator/sandbox`)
+### Sandbox Image (`openshell/sandbox`)
 
 The sandbox container runs inside each sandbox pod. It contains the sandbox supervisor binary, Python runtime, AI agent tooling, and a virtual environment for the Python SDK.
 
@@ -74,7 +74,7 @@ The sandbox container runs inside each sandbox pod. It contains the sandbox supe
 - Policy files are mounted at `/var/navigator/policy.rego` (rules) and `/var/navigator/data.yaml` (data) when running in file-based policy mode.
 - The Python SDK is copied directly into the venv's site-packages at `/app/.venv/lib/python3.12/site-packages/navigator/`.
 
-### Gateway Image (`navigator/server`)
+### Gateway Image (`openshell/server`)
 
 The gateway container runs the control plane / orchestration service.
 
@@ -93,7 +93,7 @@ The gateway container runs the control plane / orchestration service.
 - No Docker HEALTHCHECK -- health checks are handled by Kubernetes liveness/readiness probes (`tcpSocket` on the gRPC port).
 - Entrypoint: `navigator-server`, default args: `--port 8080`.
 
-### Cluster Image (`navigator/cluster`)
+### Cluster Image (`openshell/cluster`)
 
 A k3s image with bundled Helm charts and Kubernetes manifests for single-container deployment. Component images (sandbox, gateway) are **pulled at runtime** from the distribution registry -- they are not bundled as tarballs in this image.
 
@@ -317,7 +317,6 @@ All builds use mise tasks defined in `tasks/*.toml` (included from `mise.toml`).
 | Task | Description |
 |---|---|
 | `mise run cluster` | Bootstrap or incremental deploy: creates cluster if needed, rebuilds changed components |
-| `mise run cluster:build:full` | Full build + deploy path (advanced/CI) |
 
 ### Other Tasks
 
@@ -425,11 +424,8 @@ In CI pipelines:
 # Bootstrap or incremental deploy (creates cluster if needed, rebuilds changed components)
 mise run cluster
 
-# Full build + deploy path (advanced/CI)
-mise run cluster:build:full
-
 # Run sandbox container interactively (for testing sandbox code)
-mise run cluster:sandbox
+mise run sandbox
 ```
 
 ### Multi-Arch Publishing
diff --git a/architecture/cluster-single-node.md b/architecture/gateway-single-node.md
similarity index 84%
rename from architecture/cluster-single-node.md
rename to architecture/gateway-single-node.md
index bda6f829e..45f9eed9d 100644
--- a/architecture/cluster-single-node.md
+++ b/architecture/gateway-single-node.md
@@ -1,15 +1,15 @@
-# Cluster Bootstrap Architecture
+# Gateway Bootstrap Architecture
 
-This document describes how OpenShell bootstraps a single-node k3s cluster inside a Docker container, for both local and remote (SSH) targets.
+This document describes how OpenShell bootstraps a single-node k3s gateway inside a Docker container, for both local and remote (SSH) targets.
 
 ## Goals and Scope
 
-- Provide a single bootstrap flow through `navigator-bootstrap` for local and remote cluster lifecycle.
+- Provide a single bootstrap flow through `navigator-bootstrap` for local and remote gateway lifecycle.
 - Keep Docker as the only runtime dependency for provisioning and lifecycle operations.
-- Package the OpenShell cluster as one container image, transferred to the target host via registry pull.
+- Package the OpenShell gateway as one container image, transferred to the target host via registry pull.
 - Support idempotent `deploy` behavior (safe to re-run).
-- Persist cluster access artifacts (kubeconfig, metadata, mTLS certs) in the local XDG config directory.
-- Track the active cluster so most CLI commands resolve their target automatically.
+- Persist gateway access artifacts (kubeconfig, metadata, mTLS certs) in the local XDG config directory.
+- Track the active gateway so most CLI commands resolve their target automatically.
 
 Out of scope:
 
@@ -19,13 +19,13 @@ Out of scope:
 
 - `crates/navigator-cli/src/main.rs`: CLI entry point; `clap`-based command parsing.
 - `crates/navigator-cli/src/run.rs`: CLI command implementations (`gateway_start`, `gateway_stop`, `gateway_destroy`, `gateway_info`, `gateway_tunnel`).
-- `crates/navigator-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a cluster when one is unreachable).
-- `crates/navigator-bootstrap/src/lib.rs`: Cluster lifecycle orchestration (`deploy_cluster`, `deploy_cluster_with_logs`, `cluster_handle`, `check_existing_deployment`).
+- `crates/navigator-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a gateway when one is unreachable).
+- `crates/navigator-bootstrap/src/lib.rs`: Gateway lifecycle orchestration (`deploy_gateway`, `deploy_gateway_with_logs`, `gateway_handle`, `check_existing_deployment`).
 - `crates/navigator-bootstrap/src/docker.rs`: Docker API wrappers (network, volume, container, image operations).
 - `crates/navigator-bootstrap/src/image.rs`: Remote image registry pull with XOR-obfuscated distribution credentials.
 - `crates/navigator-bootstrap/src/runtime.rs`: In-container operations via `docker exec` (kubeconfig readiness, health polling, stale node cleanup, deployment restart).
 - `crates/navigator-bootstrap/src/kubeconfig.rs`: Kubeconfig rewriting, storage, and merge into local `~/.kube/config`.
-- `crates/navigator-bootstrap/src/metadata.rs`: Cluster metadata creation, storage, and active cluster tracking.
+- `crates/navigator-bootstrap/src/metadata.rs`: Gateway metadata creation, storage, and active gateway tracking.
 - `crates/navigator-bootstrap/src/mtls.rs`: Gateway TLS detection and CLI mTLS bundle extraction.
 - `crates/navigator-bootstrap/src/push.rs`: Local development image push into k3s containerd.
 - `crates/navigator-bootstrap/src/paths.rs`: XDG path resolution.
@@ -39,20 +39,20 @@ Out of scope:
 
 ## CLI Commands
 
-All cluster lifecycle commands live under `openshell gateway`:
+All gateway lifecycle commands live under `openshell gateway`:
 
 | Command | Description |
 |---|---|
-| `openshell gateway start [--name NAME] [--remote user@host] [--ssh-key PATH]` | Provision or update a cluster |
+| `openshell gateway start [--name NAME] [--remote user@host] [--ssh-key PATH]` | Provision or update a gateway |
 | `openshell gateway stop [--name NAME] [--remote user@host]` | Stop the container (preserves state) |
 | `openshell gateway destroy [--name NAME] [--remote user@host]` | Destroy container, attached volumes, kubeconfig directory, metadata, and network |
 | `openshell gateway info [--name NAME]` | Show deployment details (endpoint, kubeconfig path, SSH host) |
 | `openshell gateway tunnel [--name NAME] [--remote user@host] [--print-command]` | Start or print SSH tunnel for kubectl access |
 | `openshell status` | Show gateway health via gRPC/HTTP |
-| `openshell gateway select <name>` | Set the active cluster |
-| `openshell gateway select` | List all clusters with metadata |
+| `openshell gateway select <name>` | Set the active gateway |
+| `openshell gateway select` | List all gateways with metadata |
 
-The `--name` flag defaults to `"openshell"`. When omitted on commands that accept it, the CLI resolves the active cluster via: `--gateway` flag, then `OPENSHELL_CLUSTER` env, then `~/.config/openshell/active_cluster` file.
+The `--name` flag defaults to `"openshell"`. When omitted on commands that accept it, the CLI resolves the active gateway via: `--gateway` flag, then `OPENSHELL_GATEWAY` env, then `~/.config/openshell/active_gateway` file.
 
 For remote dev/test deploys from a local checkout, `scripts/remote-deploy.sh`
 wraps a different workflow: it rsyncs the repository to a remote host, builds
@@ -66,10 +66,9 @@ Development task entrypoints split bootstrap behavior:
 
 | Task | Behavior |
 |---|---|
-| `mise run cluster` | Bootstrap or incremental deploy: creates cluster if needed (fast recreate), then detects changed files and rebuilds/pushes only impacted components |
-| `mise run cluster:build:full` | Full build path: builds cluster/server/sandbox images, pushes components, then deploys (preferred in CI) |
+| `mise run cluster` | Bootstrap or incremental deploy: creates gateway if needed (fast recreate), then detects changed files and rebuilds/pushes only impacted components |
 
-For `mise run cluster`, `.env` acts as local source-of-truth for `CLUSTER_NAME`, `GATEWAY_PORT`, and `OPENSHELL_CLUSTER`. Missing keys are appended; existing values are preserved. If `GATEWAY_PORT` is missing, the task selects a free local port and persists it.
+For `mise run cluster`, `.env` acts as local source-of-truth for `GATEWAY_NAME`, `GATEWAY_PORT`, and `OPENSHELL_GATEWAY`. Missing keys are appended; existing values are preserved. If `GATEWAY_PORT` is missing, the task selects a free local port and persists it.
 Fast mode ensures a local registry (`127.0.0.1:5000`) is running and configures k3s to mirror pulls via `host.docker.internal:5000`, so the cluster task can push/pull local component images consistently.
 
 ## Bootstrap Sequence Diagram
@@ -83,7 +82,7 @@ sequenceDiagram
   participant R as Remote Docker daemon (SSH)
 
   U->>C: openshell gateway start --remote user@host
-  C->>B: deploy_cluster(DeployOptions)
+  C->>B: deploy_gateway(DeployOptions)
 
   B->>B: create_ssh_docker_client (ssh://, 600s timeout)
   B->>R: pull_remote_image (registry auth, platform-aware)
@@ -99,13 +98,13 @@ sequenceDiagram
   B->>R: wait_for_kubeconfig (30 attempts, 2s apart)
   B->>B: rewrite and store kubeconfig
   B->>R: clean_stale_nodes (kubectl delete node)
-  B->>R: wait_for_cluster_ready (180 attempts, 2s apart)
+  B->>R: wait_for_gateway_ready (180 attempts, 2s apart)
   B->>R: poll for secret navigator-cli-client (90 attempts, 2s apart)
   R-->>B: ca.crt, tls.crt, tls.key
   B->>B: atomically store mTLS bundle
-  B->>B: create and persist cluster metadata JSON
-  B->>B: save_active_cluster
-  B-->>C: ClusterHandle (kubeconfig_path, metadata, docker client)
+  B->>B: create and persist gateway metadata JSON
+  B->>B: save_active_gateway
+  B-->>C: GatewayHandle (kubeconfig_path, metadata, docker client)
 ```
 
 ## End-State Connectivity Diagram
@@ -115,14 +114,14 @@ flowchart LR
   subgraph WS[User workstation]
     NAV[navigator-cli]
     KUBECTL[kubectl]
-    KC[stored kubeconfig ~/.config/openshell/clusters/NAME/kubeconfig]
+    KC[stored kubeconfig ~/.config/openshell/gateways/NAME/kubeconfig]
     MTLS[mTLS bundle ca.crt, tls.crt, tls.key]
     TUN[ssh -L 6443:127.0.0.1:6443 user@host]
   end
 
   subgraph HOST[Target host]
     DOCKER[Docker daemon]
-    K3S[navigator-cluster-NAME single k3s container]
+    K3S[openshell-cluster-NAME single k3s container]
     KAPI[Kubernetes API :6443]
     G8080[Gateway :port -> :30051 mTLS default 8080]
     SBX[Sandbox runtime]
@@ -146,41 +145,41 @@ flowchart LR
 
 ### 1) Entry and client selection
 
-`deploy_cluster(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode:
+`deploy_gateway(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode:
 
 - `DeployOptions` fields: `name: String`, `image_ref: Option<String>`, `remote: Option<RemoteOptions>`, `port: u16` (default 8080).
 - `RemoteOptions` fields: `destination: String`, `ssh_key: Option<String>`.
 - **Local deploy**: Create one Docker client with `Docker::connect_with_local_defaults()`.
 - **Remote deploy**: Create SSH Docker client via `Docker::connect_with_ssh()` with a 600-second timeout (for large image transfers). The destination is prefixed with `ssh://` if not already present.
 
-The `deploy_cluster_with_logs` variant accepts an `FnMut(String)` callback for progress reporting. The CLI wraps this in a `ClusterDeployLogPanel` for interactive terminals.
+The `deploy_gateway_with_logs` variant accepts an `FnMut(String)` callback for progress reporting. The CLI wraps this in a `GatewayDeployLogPanel` for interactive terminals.
 
-**Pre-deploy check** (CLI layer in `gateway_start`): In interactive terminals, `check_existing_deployment` inspects whether a container or volume already exists. If found, the user is prompted to destroy and recreate or reuse the existing cluster.
+**Pre-deploy check** (CLI layer in `gateway_start`): In interactive terminals, `check_existing_deployment` inspects whether a container or volume already exists. If found, the user is prompted to destroy and recreate or reuse the existing gateway.
 
 ### 2) Image readiness
 
-Image ref resolution in `default_cluster_image_ref()`:
+Image ref resolution in `default_gateway_image_ref()`:
 
 1. If `OPENSHELL_CLUSTER_IMAGE` is set and non-empty, use it verbatim.
-2. Otherwise, use the published distribution image base (`<distribution-registry>/navigator/cluster`) with its default tag behavior.
+2. Otherwise, use the published distribution image base (`<distribution-registry>/openshell/cluster`) with its default tag behavior.
 
 - **Local deploy**: `ensure_image()` inspects the image on the local daemon and pulls from the configured registry if missing (using built-in distribution credentials when pulling from the default distribution host).
-- **Remote deploy**: `pull_remote_image()` queries the remote daemon's architecture via `Docker::version()`, pulls the matching platform variant from the distribution registry (with XOR-decoded credentials), and tags the pulled image to the expected local ref (for example `navigator/cluster:dev` when an explicit local tag is requested).
+- **Remote deploy**: `pull_remote_image()` queries the remote daemon's architecture via `Docker::version()`, pulls the matching platform variant from the distribution registry (with XOR-decoded credentials), and tags the pulled image to the expected local ref (for example `openshell/cluster:dev` when an explicit local tag is requested).
 
 ### 3) Runtime infrastructure
 
 For the target daemon (local or remote):
 
-1. **Ensure bridge network** `navigator-cluster` (attachable, bridge driver) via `ensure_network()`.
-2. **Ensure volume** `navigator-cluster-{name}` via `ensure_volume()`.
+1. **Ensure bridge network** `openshell-cluster` (attachable, bridge driver) via `ensure_network()`.
+2. **Ensure volume** `openshell-cluster-{name}` via `ensure_volume()`.
 3. **Compute extra TLS SANs**:
    - For **local deploys**: Check `DOCKER_HOST` for a non-loopback `tcp://` endpoint (e.g., `tcp://docker:2375` in CI). If found, extract the host as an extra SAN. The function `local_gateway_host_from_docker_host()` skips `localhost`, `127.0.0.1`, and `::1`.
    - For **remote deploys**: Extract the host from the SSH destination (handles `user@host`, `ssh://user@host`), resolve via `ssh -G` to get the canonical hostname/IP. Include both the resolved host and original SSH host (if different) as extra SANs.
-4. **Ensure container** `navigator-cluster-{name}` via `ensure_container()`:
+4. **Ensure container** `openshell-cluster-{name}` via `ensure_container()`:
    - k3s server command: `server --disable=traefik --tls-san=127.0.0.1 --tls-san=localhost --tls-san=host.docker.internal` plus computed extra SANs.
    - Privileged mode.
-   - Volume bind mount: `navigator-cluster-{name}:/var/lib/rancher/k3s`.
-   - Network: `navigator-cluster`.
+   - Volume bind mount: `openshell-cluster-{name}:/var/lib/rancher/k3s`.
+   - Network: `openshell-cluster`.
    - Extra host: `host.docker.internal:host-gateway`.
    - Port mappings:
 
@@ -198,11 +197,11 @@ For the target daemon (local or remote):
 After the container starts:
 
 1. **Poll kubeconfig**: `wait_for_kubeconfig()` runs `cat /etc/rancher/k3s/k3s.yaml` via `docker exec` up to 30 times, 2 seconds apart (60s total). Each attempt first checks that the container is still running. Validates the output contains `apiVersion:` and `clusters:`.
-2. **Rewrite kubeconfig**: Replace the server address with `https://127.0.0.1:6443`. Rename `default` entries (cluster, context, user) to the cluster name. Remote mode appends `-remote` suffix (e.g., `{name}-remote`) to avoid collisions with local contexts.
-3. **Store kubeconfig** at `~/.config/openshell/clusters/{name}/kubeconfig` (or `$XDG_CONFIG_HOME/openshell/clusters/{name}/kubeconfig`).
+2. **Rewrite kubeconfig**: Replace the server address with `https://127.0.0.1:6443`. Rename `default` entries (cluster, context, user) to the gateway name. Remote mode appends `-remote` suffix (e.g., `{name}-remote`) to avoid collisions with local contexts.
+3. **Store kubeconfig** at `~/.config/openshell/gateways/{name}/kubeconfig` (or `$XDG_CONFIG_HOME/openshell/gateways/{name}/kubeconfig`).
 4. **Clean stale nodes**: `clean_stale_nodes()` finds `NotReady` nodes via `kubectl get nodes` and deletes them. This is needed when a container is recreated but reuses the persistent volume -- k3s registers a new node (using the container ID as hostname) while old node entries persist in etcd. Non-fatal on error; returns the count of removed nodes.
 5. **Push local images** (optional, local deploy only): If `OPENSHELL_PUSH_IMAGES` is set, the comma-separated image refs are exported from the local Docker daemon as a single tar, uploaded into the container via `docker put_archive`, and imported into containerd via `ctr images import` in the `k8s.io` namespace. After import, `kubectl rollout restart deployment/navigator -n navigator` is run, followed by `kubectl rollout status --timeout=180s` to wait for completion. See `crates/navigator-bootstrap/src/push.rs`.
-6. **Wait for cluster health**: `wait_for_cluster_ready()` polls the Docker HEALTHCHECK status up to 180 times, 2 seconds apart (6 min total). A background task streams container logs during this wait. Failure modes:
+6. **Wait for gateway health**: `wait_for_gateway_ready()` polls the Docker HEALTHCHECK status up to 180 times, 2 seconds apart (6 min total). A background task streams container logs during this wait. Failure modes:
    - Container exits during polling: error includes recent log lines.
    - Container has no HEALTHCHECK instruction: fails immediately.
    - HEALTHCHECK reports unhealthy on final attempt: error includes recent logs.
@@ -211,13 +210,13 @@ After the container starts:
 
 TLS is always required. `fetch_and_store_cli_mtls()` polls for Kubernetes secret `navigator-cli-client` in namespace `navigator` (90 attempts, 2 seconds apart, 3 min total). Each attempt checks the container is still running. The secret's base64-encoded `ca.crt`, `tls.crt`, and `tls.key` fields are decoded and stored.
 
-Storage location: `~/.config/openshell/clusters/{name}/mtls/`
+Storage location: `~/.config/openshell/gateways/{name}/mtls/`
 
 Write is atomic: write to `.tmp` directory, validate all three files are non-empty, rename existing directory to `.bak`, rename `.tmp` to final path, then remove `.bak`.
 
 ### 6) Metadata persistence
 
-`create_cluster_metadata()` produces a `ClusterMetadata` struct:
+`create_gateway_metadata()` produces a `GatewayMetadata` struct:
 
 - **Local**: endpoint `https://127.0.0.1:{port}` by default, or `https://{docker_host}:{port}` when `DOCKER_HOST` is a non-loopback `tcp://` endpoint. `is_remote=false`.
 - **Remote**: endpoint `https://{resolved_host}:{port}`, `is_remote=true`, plus SSH destination and resolved host.
@@ -226,22 +225,22 @@ Metadata fields:
 
 | Field | Type | Description |
 |---|---|---|
-| `name` | `String` | Cluster name |
+| `name` | `String` | Gateway name |
 | `gateway_endpoint` | `String` | HTTPS endpoint with port (e.g., `https://127.0.0.1:8080`) |
-| `is_remote` | `bool` | Whether cluster is remote |
+| `is_remote` | `bool` | Whether gateway is remote |
 | `gateway_port` | `u16` | Host port mapped to the gateway NodePort |
 | `remote_host` | `Option<String>` | SSH destination (e.g., `user@host`) |
 | `resolved_host` | `Option<String>` | Resolved hostname/IP from `ssh -G` |
 
-Metadata location: `~/.config/openshell/clusters/{name}_metadata.json`
+Metadata location: `~/.config/openshell/gateways/{name}_metadata.json`
 
-Note: metadata is stored at the `clusters/` level (not nested inside `{name}/` like kubeconfig and mTLS).
+Note: metadata is stored at the `gateways/` level (not nested inside `{name}/` like kubeconfig and mTLS).
 
-After deploy, the CLI calls `save_active_cluster(name)`, writing the cluster name to `~/.config/openshell/active_cluster`. Subsequent commands that don't specify `--gateway` or `OPENSHELL_CLUSTER` resolve to this active cluster.
+After deploy, the CLI calls `save_active_gateway(name)`, writing the gateway name to `~/.config/openshell/active_gateway`. Subsequent commands that don't specify `--gateway` or `OPENSHELL_GATEWAY` resolve to this active gateway.
 
 ## Container Image
 
-The cluster image is defined in `deploy/docker/Dockerfile.cluster`:
+The gateway image is defined in `deploy/docker/Dockerfile.cluster`:
 
 ```
 Base:  rancher/k3s:v1.35.2-k3s1
@@ -313,7 +312,7 @@ flowchart LR
   RP --> Auth[Authenticate with distribution registry]
   Auth --> Pull[Pull platform-specific image layers]
   Pull --> Tag[Tag to local image ref]
-  Tag --> OK[Image available as navigator/cluster:TAG]
+  Tag --> OK[Image available as openshell/cluster:TAG]
 ```
 
 - Remote platform is queried via `Docker::version()` and normalized (e.g., `x86_64` -> `amd64`, `aarch64` -> `arm64`).
@@ -325,7 +324,7 @@ flowchart LR
 ### Kubernetes API access
 
 - Kubeconfig always targets `https://127.0.0.1:6443`.
-- For remote clusters, the user must open an SSH local-forward tunnel:
+- For remote gateways, the user must open an SSH local-forward tunnel:
 
 ```bash
 ssh -L 6443:127.0.0.1:6443 -N user@host
@@ -337,7 +336,7 @@ CLI helper:
 openshell gateway tunnel --name <name>
 ```
 
-The `--remote` flag is optional; the CLI resolves the SSH destination from stored cluster metadata. Pass `--print-command` to print the SSH command without executing it.
+The `--remote` flag is optional; the CLI resolves the SSH destination from stored gateway metadata. Pass `--print-command` to print the SSH command without executing it.
 
 ### Gateway endpoint exposure
 
@@ -349,11 +348,11 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store
 
 ### stop
 
-`ClusterHandle::stop()` calls `stop_container()`, which tolerates 404 (not found) and 409 (already stopped).
+`GatewayHandle::stop()` calls `stop_container()`, which tolerates 404 (not found) and 409 (already stopped).
 
 ### destroy
 
-**Bootstrap layer** (`ClusterHandle::destroy()` -> `destroy_cluster_resources()`):
+**Bootstrap layer** (`GatewayHandle::destroy()` -> `destroy_gateway_resources()`):
 
 1. Stop the container.
 2. Remove the container (`force=true`). Tolerates 404.
@@ -363,8 +362,8 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store
 
 **CLI layer** (`gateway_destroy()` in `run.rs` additionally):
 
-6. Remove the metadata JSON file via `remove_cluster_metadata()`.
-7. Clear the active cluster reference if it matches the destroyed cluster.
+6. Remove the metadata JSON file via `remove_gateway_metadata()`.
+7. Clear the active gateway reference if it matches the destroyed gateway.
 
 ## Idempotency and Error Behavior
 
@@ -372,7 +371,7 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store
   - Existing network/volume are reused (inspect before create).
   - If a container exists with the same image ID, it is reused; if the image changed, the container is recreated.
   - `start_container` tolerates already-running state (409).
-- In interactive terminals, the CLI prompts the user to optionally destroy and recreate an existing cluster before redeploying.
+- In interactive terminals, the CLI prompts the user to optionally destroy and recreate an existing gateway before redeploying.
 - Error handling surfaces:
   - Docker API failures from inspect/create/start/remove.
   - SSH connection failures when creating the remote Docker client.
@@ -384,11 +383,11 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store
 
 ## Auto-Bootstrap from `sandbox create`
 
-When `openshell sandbox create` cannot connect to a cluster (connection refused, DNS error, missing default TLS certs), the CLI offers to bootstrap one automatically:
+When `openshell sandbox create` cannot connect to a gateway (connection refused, DNS error, missing default TLS certs), the CLI offers to bootstrap one automatically:
 
 1. `should_attempt_bootstrap()` in `crates/navigator-cli/src/bootstrap.rs` checks the error type. It returns `true` for connectivity errors and missing default TLS materials, but `false` for TLS handshake/auth errors.
 2. If running in a terminal, the user is prompted to confirm.
-3. `run_bootstrap()` deploys a cluster named `"openshell"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs.
+3. `run_bootstrap()` deploys a gateway named `"openshell"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs.
 
 ## Container Environment Variables
 
@@ -422,7 +421,7 @@ Environment variables that affect bootstrap behavior when set on the host:
 | `XDG_CONFIG_HOME` | Base config directory (default: `$HOME/.config`) |
 | `KUBECONFIG` | Target kubeconfig path for merge (first colon-separated path; default: `$HOME/.kube/config`) |
 | `DOCKER_HOST` | When `tcp://` and non-loopback, the host is added as a TLS SAN and used as the gateway endpoint |
-| `OPENSHELL_PUSH_IMAGES` | Comma-separated image refs to push into the cluster's containerd (local deploy only) |
+| `OPENSHELL_PUSH_IMAGES` | Comma-separated image refs to push into the gateway's containerd (local deploy only) |
 | `OPENSHELL_REGISTRY_HOST` | Override the distribution registry host |
 | `OPENSHELL_REGISTRY_NAMESPACE` | Override the registry namespace (default: `"navigator"`) |
 | `IMAGE_REPO_BASE` / `OPENSHELL_IMAGE_REPO_BASE` | Override the image repository base path |
@@ -430,7 +429,7 @@ Environment variables that affect bootstrap behavior when set on the host:
 | `OPENSHELL_REGISTRY_ENDPOINT` | Custom registry mirror endpoint |
 | `OPENSHELL_REGISTRY_USERNAME` | Override registry auth username |
 | `OPENSHELL_REGISTRY_PASSWORD` | Override registry auth password |
-| `OPENSHELL_CLUSTER` | Set the active cluster name for CLI commands |
+| `OPENSHELL_GATEWAY` | Set the active gateway name for CLI commands |
 
 ## File System Layout
 
@@ -438,9 +437,9 @@ Artifacts stored under `$XDG_CONFIG_HOME/openshell/` (default `~/.config/openshe
 
 ```
 openshell/
-  active_cluster                           # plain text: active cluster name
-  clusters/
-    {name}_metadata.json                   # ClusterMetadata JSON
+  active_gateway                           # plain text: active gateway name
+  gateways/
+    {name}_metadata.json                   # GatewayMetadata JSON
     {name}/
       kubeconfig                           # rewritten kubeconfig
       mtls/                                # mTLS bundle (when TLS enabled)
@@ -456,7 +455,7 @@ openshell/
 - `crates/navigator-bootstrap/src/image.rs` -- registry pull, XOR credentials
 - `crates/navigator-bootstrap/src/runtime.rs` -- exec, health polling, stale node cleanup
 - `crates/navigator-bootstrap/src/kubeconfig.rs` -- kubeconfig rewrite and merge
-- `crates/navigator-bootstrap/src/metadata.rs` -- metadata CRUD, active cluster, SSH resolution
+- `crates/navigator-bootstrap/src/metadata.rs` -- metadata CRUD, active gateway, SSH resolution
 - `crates/navigator-bootstrap/src/mtls.rs` -- TLS detection, secret extraction, atomic write
 - `crates/navigator-bootstrap/src/push.rs` -- local image push into k3s containerd
 - `crates/navigator-bootstrap/src/constants.rs` -- naming conventions
diff --git a/crates/navigator-bootstrap/src/build.rs b/crates/navigator-bootstrap/src/build.rs
index 91967a1fe..9624e01b3 100644
--- a/crates/navigator-bootstrap/src/build.rs
+++ b/crates/navigator-bootstrap/src/build.rs
@@ -1,11 +1,11 @@
 // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 // SPDX-License-Identifier: Apache-2.0
 
-//! Build and push container images into a k3s cluster.
+//! Build and push container images into a k3s gateway.
 //!
 //! This module wraps bollard's `build_image()` API to build a container image
 //! from a Dockerfile and build context, then reuses the existing push pipeline
-//! to import the image into the cluster's containerd runtime.
+//! to import the image into the gateway's containerd runtime.
 
 use std::collections::HashMap;
 use std::io::Read;
@@ -19,19 +19,19 @@ use miette::{IntoDiagnostic, Result, WrapErr};
 use crate::constants::container_name;
 use crate::push::push_local_images;
 
-/// Build a container image from a Dockerfile and push it into the cluster.
+/// Build a container image from a Dockerfile and push it into the gateway.
 ///
 /// This is used by `openshell sandbox create --from <Dockerfile>`. It:
 /// 1. Creates a tar archive of the build context directory.
 /// 2. Sends it to the local Docker daemon via `build_image()`.
-/// 3. Pushes the resulting image into the cluster's containerd via the
+/// 3. Pushes the resulting image into the gateway's containerd via the
 ///    existing `push_local_images()` pipeline.
 #[allow(clippy::implicit_hasher)]
 pub async fn build_and_push_image(
     dockerfile_path: &Path,
     tag: &str,
     context_dir: &Path,
-    cluster_name: &str,
+    gateway_name: &str,
     build_args: &HashMap<String, String>,
     on_log: &mut impl FnMut(String),
 ) -> Result<()> {
@@ -43,18 +43,18 @@ pub async fn build_and_push_image(
     build_image(dockerfile_path, tag, context_dir, build_args, on_log).await?;
     on_log(format!("Built image {tag}"));
 
-    // 2. Push into the cluster.
+    // 2. Push into the gateway.
     on_log(format!(
-        "Pushing image {tag} into cluster \"{cluster_name}\""
+        "Pushing image {tag} into gateway \"{gateway_name}\""
     ));
     let local_docker = Docker::connect_with_local_defaults()
         .into_diagnostic()
         .wrap_err("failed to connect to local Docker daemon")?;
-    let container = container_name(cluster_name);
+    let container = container_name(gateway_name);
     let images: Vec<&str> = vec![tag];
     push_local_images(&local_docker, &local_docker, &container, &images, on_log).await?;
 
-    on_log(format!("Image {tag} is available in the cluster."));
+    on_log(format!("Image {tag} is available in the gateway."));
     Ok(())
 }
 
diff --git a/crates/navigator-bootstrap/src/constants.rs b/crates/navigator-bootstrap/src/constants.rs
index 47641dae8..e174e516e 100644
--- a/crates/navigator-bootstrap/src/constants.rs
+++ b/crates/navigator-bootstrap/src/constants.rs
@@ -1,20 +1,20 @@
 // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 // SPDX-License-Identifier: Apache-2.0
 
-pub const NETWORK_NAME: &str = "navigator-cluster";
+pub const NETWORK_NAME: &str = "openshell-cluster";
 pub const KUBECONFIG_PATH: &str = "/etc/rancher/k3s/k3s.yaml";
 
 /// K8s secret holding the server's TLS certificate and private key.
-pub const SERVER_TLS_SECRET_NAME: &str = "navigator-server-tls";
+pub const SERVER_TLS_SECRET_NAME: &str = "openshell-server-tls";
 /// K8s secret holding the CA certificate used to verify client certificates.
-pub const SERVER_CLIENT_CA_SECRET_NAME: &str = "navigator-server-client-ca";
+pub const SERVER_CLIENT_CA_SECRET_NAME: &str = "openshell-server-client-ca";
 /// K8s secret holding the client TLS certificate, key, and CA cert (shared by CLI and sandboxes).
-pub const CLIENT_TLS_SECRET_NAME: &str = "navigator-client-tls";
+pub const CLIENT_TLS_SECRET_NAME: &str = "openshell-client-tls";
 
 pub fn container_name(name: &str) -> String {
-    format!("navigator-cluster-{name}")
+    format!("openshell-cluster-{name}")
 }
 
 pub fn volume_name(name: &str) -> String {
-    format!("navigator-cluster-{name}")
+    format!("openshell-cluster-{name}")
 }
diff --git a/crates/navigator-bootstrap/src/docker.rs b/crates/navigator-bootstrap/src/docker.rs
index 907f34d14..a3e5ef729 100644
--- a/crates/navigator-bootstrap/src/docker.rs
+++ b/crates/navigator-bootstrap/src/docker.rs
@@ -22,7 +22,7 @@ use miette::{IntoDiagnostic, Result, WrapErr};
 use std::collections::HashMap;
 use std::path::Path;
 
-const REGISTRY_NAMESPACE_DEFAULT: &str = "navigator";
+const REGISTRY_NAMESPACE_DEFAULT: &str = "openshell";
 
 const REGISTRY_MODE_EXTERNAL: &str = "external";
 
@@ -202,7 +202,7 @@ pub async fn ensure_image(
     if image::is_local_image_ref(image_ref) {
         return Err(miette::miette!(
             "Image '{}' not found locally. This looks like a locally-built image \
-             (no registry prefix). Build it first with `mise run docker:build:cluster`.",
+             (no registry prefix). Build it first with `mise run docker:build:gateway`.",
             image_ref,
         ));
     }
@@ -476,7 +476,7 @@ pub async fn ensure_container(
         )
         .await
         .into_diagnostic()
-        .wrap_err("failed to create cluster container")?;
+        .wrap_err("failed to create gateway container")?;
     Ok(())
 }
 
@@ -504,7 +504,7 @@ pub async fn start_container(docker: &Docker, name: &str) -> Result<()> {
             Err(err) => {
                 return Err(err)
                     .into_diagnostic()
-                    .wrap_err("failed to start cluster container");
+                    .wrap_err("failed to start gateway container");
             }
         }
     }
@@ -521,7 +521,7 @@ pub async fn stop_container(docker: &Docker, container_name: &str) -> Result<()>
     }
 }
 
-pub async fn destroy_cluster_resources(
+pub async fn destroy_gateway_resources(
     docker: &Docker,
     name: &str,
     kubeconfig_path: &Path,
@@ -569,12 +569,12 @@ pub async fn destroy_cluster_resources(
         return Err(err).into_diagnostic();
     }
 
-    // Remove the cluster image so the next deploy always pulls the latest
+    // Remove the gateway image so the next deploy always pulls the latest
     // version from the registry instead of reusing a stale local copy.
     // Docker may briefly report the container as still running after a
     // force-remove, so retry a few times on conflict (409) errors.
     if let Some(ref image_id) = container_image {
-        tracing::debug!("Removing cluster image: {}", image_id);
+        tracing::debug!("Removing gateway image: {}", image_id);
         let mut last_err = None;
         for attempt in 0..5 {
             if attempt > 0 {
@@ -610,7 +610,7 @@ pub async fn destroy_cluster_resources(
             }
         }
         if let Some(err) = last_err {
-            tracing::warn!("Failed to remove cluster image {}: {}", image_id, err);
+            tracing::warn!("Failed to remove gateway image {}: {}", image_id, err);
         }
     }
 
@@ -632,7 +632,7 @@ pub async fn destroy_cluster_resources(
     Ok(())
 }
 
-/// Forcefully remove the cluster network, disconnecting any remaining
+/// Forcefully remove the gateway network, disconnecting any remaining
 /// containers first.  This ensures that stale Docker network endpoints
 /// cannot prevent port bindings from being released.
 async fn force_remove_network(docker: &Docker) -> Result<()> {
@@ -723,9 +723,9 @@ fn truncate_id(id: &str) -> &str {
     }
 }
 
-/// Information about an existing cluster deployment.
+/// Information about an existing gateway deployment.
 #[derive(Debug, Clone)]
-pub struct ExistingClusterInfo {
+pub struct ExistingGatewayInfo {
     /// Whether the container exists.
     pub container_exists: bool,
     /// Whether the container is currently running.
@@ -736,14 +736,14 @@ pub struct ExistingClusterInfo {
     pub container_image: Option<String>,
 }
 
-/// Check whether a cluster with the given name already exists.
+/// Check whether a gateway with the given name already exists.
 ///
-/// Returns `None` if no cluster resources exist, or `Some(info)` with
+/// Returns `None` if no gateway resources exist, or `Some(info)` with
 /// details about the existing deployment.
-pub async fn check_existing_cluster(
+pub async fn check_existing_gateway(
     docker: &Docker,
     name: &str,
-) -> Result<Option<ExistingClusterInfo>> {
+) -> Result<Option<ExistingGatewayInfo>> {
     let container_name = container_name(name);
     let vol_name = volume_name(name);
 
@@ -770,7 +770,7 @@ pub async fn check_existing_cluster(
         return Ok(None);
     }
 
-    Ok(Some(ExistingClusterInfo {
+    Ok(Some(ExistingGatewayInfo {
         container_exists,
         container_running,
         volume_exists,
diff --git a/crates/navigator-bootstrap/src/edge_token.rs b/crates/navigator-bootstrap/src/edge_token.rs
index 1ca1ea2d6..12b294d39 100644
--- a/crates/navigator-bootstrap/src/edge_token.rs
+++ b/crates/navigator-bootstrap/src/edge_token.rs
@@ -4,26 +4,26 @@
 //! Edge authentication token storage.
 //!
 //! Stores the bearer token for edge-authenticated gateways at
-//! `$XDG_CONFIG_HOME/openshell/clusters/<name>/edge_token`.
+//! `$XDG_CONFIG_HOME/openshell/gateways/<name>/edge_token`.
 //! The token is a plain-text JWT string with `0600` permissions.
 
-use crate::paths::clusters_dir;
+use crate::paths::gateways_dir;
 use miette::{IntoDiagnostic, Result, WrapErr};
 use std::path::PathBuf;
 
-/// Path to the stored edge auth token for a cluster.
-pub fn edge_token_path(cluster_name: &str) -> Result<PathBuf> {
-    Ok(clusters_dir()?.join(cluster_name).join("edge_token"))
+/// Path to the stored edge auth token for a gateway.
+pub fn edge_token_path(gateway_name: &str) -> Result<PathBuf> {
+    Ok(gateways_dir()?.join(gateway_name).join("edge_token"))
 }
 
 /// Legacy path used before the rename to `edge_token`.
-fn legacy_token_path(cluster_name: &str) -> Result<PathBuf> {
-    Ok(clusters_dir()?.join(cluster_name).join("cf_token"))
+fn legacy_token_path(gateway_name: &str) -> Result<PathBuf> {
+    Ok(gateways_dir()?.join(gateway_name).join("cf_token"))
 }
 
-/// Store an edge authentication token for a cluster.
-pub fn store_edge_token(cluster_name: &str, token: &str) -> Result<()> {
-    let path = edge_token_path(cluster_name)?;
+/// Store an edge authentication token for a gateway.
+pub fn store_edge_token(gateway_name: &str, token: &str) -> Result<()> {
+    let path = edge_token_path(gateway_name)?;
     if let Some(parent) = path.parent() {
         std::fs::create_dir_all(parent)
             .into_diagnostic()
@@ -43,27 +43,27 @@ pub fn store_edge_token(cluster_name: &str, token: &str) -> Result<()> {
     Ok(())
 }
 
-/// Load a stored edge authentication token for a cluster.
+/// Load a stored edge authentication token for a gateway.
 ///
 /// Returns `None` if no token file exists or the file is empty.
 /// Falls back to the legacy `cf_token` path for backwards compatibility.
-pub fn load_edge_token(cluster_name: &str) -> Option<String> {
+pub fn load_edge_token(gateway_name: &str) -> Option<String> {
     // Try the new path first, then fall back to legacy.
-    let path = edge_token_path(cluster_name)
+    let path = edge_token_path(gateway_name)
         .ok()
         .filter(|p| p.exists())
-        .or_else(|| legacy_token_path(cluster_name).ok().filter(|p| p.exists()))?;
+        .or_else(|| legacy_token_path(gateway_name).ok().filter(|p| p.exists()))?;
     let contents = std::fs::read_to_string(&path).ok()?;
     let token = contents.trim().to_string();
     if token.is_empty() { None } else { Some(token) }
 }
 
 /// Remove a stored edge authentication token.
-pub fn remove_edge_token(cluster_name: &str) -> Result<()> {
+pub fn remove_edge_token(gateway_name: &str) -> Result<()> {
     // Remove both new and legacy paths.
     for path in [
-        edge_token_path(cluster_name)?,
-        legacy_token_path(cluster_name)?,
+        edge_token_path(gateway_name)?,
+        legacy_token_path(gateway_name)?,
     ] {
         if path.exists() {
             std::fs::remove_file(&path)
@@ -102,9 +102,9 @@ mod tests {
     fn store_and_load_edge_token_roundtrip() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            store_edge_token("test-cluster", "eyJhbGciOiJSUzI1NiJ9.test.sig").unwrap();
+            store_edge_token("test-gateway", "eyJhbGciOiJSUzI1NiJ9.test.sig").unwrap();
             assert_eq!(
-                load_edge_token("test-cluster"),
+                load_edge_token("test-gateway"),
                 Some("eyJhbGciOiJSUzI1NiJ9.test.sig".to_string())
             );
         });
@@ -114,7 +114,7 @@ mod tests {
     fn load_edge_token_returns_none_when_not_set() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            assert_eq!(load_edge_token("no-such-cluster"), None);
+            assert_eq!(load_edge_token("no-such-gateway"), None);
         });
     }
 
@@ -153,10 +153,10 @@ mod tests {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
             // Write manually with whitespace.
-            let path = edge_token_path("ws-cluster").unwrap();
+            let path = edge_token_path("ws-gateway").unwrap();
             std::fs::create_dir_all(path.parent().unwrap()).unwrap();
             std::fs::write(&path, "  my-token \n").unwrap();
-            assert_eq!(load_edge_token("ws-cluster"), Some("my-token".to_string()));
+            assert_eq!(load_edge_token("ws-gateway"), Some("my-token".to_string()));
         });
     }
 
@@ -164,10 +164,10 @@ mod tests {
     fn load_edge_token_returns_none_for_empty_file() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            let path = edge_token_path("empty-cluster").unwrap();
+            let path = edge_token_path("empty-gateway").unwrap();
             std::fs::create_dir_all(path.parent().unwrap()).unwrap();
             std::fs::write(&path, "   \n").unwrap();
-            assert_eq!(load_edge_token("empty-cluster"), None);
+            assert_eq!(load_edge_token("empty-gateway"), None);
         });
     }
 
@@ -176,11 +176,11 @@ mod tests {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
             // Write to the legacy cf_token path.
-            let path = legacy_token_path("legacy-cluster").unwrap();
+            let path = legacy_token_path("legacy-gateway").unwrap();
             std::fs::create_dir_all(path.parent().unwrap()).unwrap();
             std::fs::write(&path, "legacy-jwt").unwrap();
             assert_eq!(
-                load_edge_token("legacy-cluster"),
+                load_edge_token("legacy-gateway"),
                 Some("legacy-jwt".to_string())
             );
         });
diff --git a/crates/navigator-bootstrap/src/image.rs b/crates/navigator-bootstrap/src/image.rs
index 3955bb546..57736a1c2 100644
--- a/crates/navigator-bootstrap/src/image.rs
+++ b/crates/navigator-bootstrap/src/image.rs
@@ -24,8 +24,8 @@ pub const DEFAULT_REGISTRY: &str = "ghcr.io";
 /// Default image repository base on GHCR (without component name or tag).
 pub const DEFAULT_IMAGE_REPO_BASE: &str = "ghcr.io/nvidia/nemoclaw";
 
-/// Default full cluster image path on GHCR (without tag).
-pub const DEFAULT_CLUSTER_IMAGE: &str = "ghcr.io/nvidia/nemoclaw/cluster";
+/// Default full gateway image path on GHCR (without tag).
+pub const DEFAULT_GATEWAY_IMAGE: &str = "ghcr.io/nvidia/nemoclaw/cluster";
 
 /// Default username for token-based GHCR authentication.
 ///
@@ -156,11 +156,11 @@ pub(crate) fn ghcr_credentials(token: Option<&str>) -> Option<DockerCredentials>
     })
 }
 
-/// Pull the cluster image directly on a remote Docker daemon from ghcr.io,
+/// Pull the gateway image directly on a remote Docker daemon from ghcr.io,
 /// authenticating with the provided registry token.
 ///
 /// After pulling, the image is tagged to the expected local image ref (e.g.,
-/// `navigator/cluster:dev`) so that all downstream container creation logic works
+/// `openshell/cluster:dev`) so that all downstream container creation logic works
 /// without changes.
 ///
 /// The remote host's platform is queried so the correct architecture variant is
@@ -186,7 +186,7 @@ pub async fn pull_remote_image(
     // and already points at a registry image, honour its tag.  Otherwise use
     // the distribution registry default tag — the local build tag (e.g. "dev")
     // is a build-time convention that doesn't exist in the registry.
-    let registry_image_base = DEFAULT_CLUSTER_IMAGE.to_string();
+    let registry_image_base = DEFAULT_GATEWAY_IMAGE.to_string();
 
     let tag = if is_local_image_ref(image_ref) {
         PULL_REGISTRY_DEFAULT_TAG.to_string()
@@ -201,7 +201,7 @@ pub async fn pull_remote_image(
         registry_image, DEFAULT_REGISTRY
     );
     on_progress(format!(
-        "[status] Pulling navigator/cluster:{tag} ({platform_str}) on remote host"
+        "[status] Pulling openshell/cluster:{tag} ({platform_str}) on remote host"
     ));
 
     let credentials = ghcr_credentials(registry_token);
@@ -234,7 +234,7 @@ pub async fn pull_remote_image(
 
     // Tag the pulled image to the expected local image ref so downstream code
     // (container creation, image ID checks) works unchanged.
-    // e.g., tag "ghcr.io/nvidia/nemoclaw/cluster:latest" as "navigator/cluster:dev"
+    // e.g., tag "ghcr.io/nvidia/nemoclaw/cluster:latest" as "openshell/cluster:dev"
     let (target_repo, target_tag) = parse_image_ref(image_ref);
     info!(
         "Tagging {} as {}:{}",
@@ -382,8 +382,8 @@ mod tests {
     #[test]
     fn default_constants_are_consistent() {
         assert!(
-            DEFAULT_CLUSTER_IMAGE.starts_with(DEFAULT_IMAGE_REPO_BASE),
-            "cluster image should be under the default repo base"
+            DEFAULT_GATEWAY_IMAGE.starts_with(DEFAULT_IMAGE_REPO_BASE),
+            "gateway image should be under the default repo base"
         );
         assert!(
             DEFAULT_IMAGE_REPO_BASE.starts_with(DEFAULT_REGISTRY),
diff --git a/crates/navigator-bootstrap/src/kubeconfig.rs b/crates/navigator-bootstrap/src/kubeconfig.rs
index c877739fe..01db49bf6 100644
--- a/crates/navigator-bootstrap/src/kubeconfig.rs
+++ b/crates/navigator-bootstrap/src/kubeconfig.rs
@@ -11,7 +11,7 @@ pub fn stored_kubeconfig_path(name: &str) -> Result<PathBuf> {
     let base = xdg_config_dir()?;
     Ok(base
         .join("openshell")
-        .join("clusters")
+        .join("gateways")
         .join(name)
         .join("kubeconfig"))
 }
@@ -100,14 +100,14 @@ pub fn store_kubeconfig(path: &Path, contents: &str) -> Result<()> {
 /// The kubeconfig points to `127.0.0.1:<kube_port>`, which works with an SSH tunnel:
 /// `ssh -L <kube_port>:127.0.0.1:6443 user@host`
 ///
-/// The cluster name includes "-remote" suffix to distinguish from local clusters.
+/// The gateway name includes "-remote" suffix to distinguish from local gateways.
 pub fn rewrite_kubeconfig_remote(
     contents: &str,
-    cluster_name: &str,
+    gateway_name: &str,
     _destination: &str,
     kube_port: Option<u16>,
 ) -> String {
-    let remote_name = format!("{cluster_name}-remote");
+    let remote_name = format!("{gateway_name}-remote");
     rewrite_kubeconfig(contents, &remote_name, kube_port)
 }
 
@@ -116,7 +116,7 @@ pub fn rewrite_kubeconfig_remote(
 /// When `kube_port` is `Some`, the server URL is rewritten to
 /// `https://127.0.0.1:<kube_port>`. When `None`, the original server URL
 /// is left intact (the control plane is not exposed on the host).
-pub fn rewrite_kubeconfig(contents: &str, cluster_name: &str, kube_port: Option<u16>) -> String {
+pub fn rewrite_kubeconfig(contents: &str, gateway_name: &str, kube_port: Option<u16>) -> String {
     let mut replaced = Vec::new();
     for line in contents.lines() {
         let trimmed = line.trim_start();
@@ -128,24 +128,24 @@ pub fn rewrite_kubeconfig(contents: &str, cluster_name: &str, kube_port: Option<
             replaced.push(format!("{indent}server: https://127.0.0.1:{kp}"));
             continue;
         }
-        // Rename default cluster/context/user to the cluster name
+        // Rename default cluster/context/user to the gateway name
         // Handle both "name: default" and "- name: default" (YAML list item)
         if trimmed == "name: default" || trimmed == "- name: default" {
             let indent_len = line.len() - trimmed.len();
             let indent = &line[..indent_len];
             let prefix = if trimmed.starts_with("- ") { "- " } else { "" };
-            replaced.push(format!("{indent}{prefix}name: {cluster_name}"));
+            replaced.push(format!("{indent}{prefix}name: {gateway_name}"));
             continue;
         }
         if trimmed == "cluster: default" || trimmed == "user: default" {
             let indent_len = line.len() - trimmed.len();
             let indent = &line[..indent_len];
             let key = trimmed.split(':').next().unwrap_or("cluster");
-            replaced.push(format!("{indent}{key}: {cluster_name}"));
+            replaced.push(format!("{indent}{key}: {gateway_name}"));
             continue;
         }
         if trimmed == "current-context: default" {
-            replaced.push(format!("current-context: {cluster_name}"));
+            replaced.push(format!("current-context: {gateway_name}"));
             continue;
         }
         replaced.push(line.to_string());
diff --git a/crates/navigator-bootstrap/src/lib.rs b/crates/navigator-bootstrap/src/lib.rs
index d1a0f7cd8..214178c74 100644
--- a/crates/navigator-bootstrap/src/lib.rs
+++ b/crates/navigator-bootstrap/src/lib.rs
@@ -31,30 +31,30 @@ use crate::constants::{
     volume_name,
 };
 use crate::docker::{
-    check_existing_cluster, create_ssh_docker_client, destroy_cluster_resources, ensure_container,
+    check_existing_gateway, create_ssh_docker_client, destroy_gateway_resources, ensure_container,
     ensure_image, ensure_network, ensure_volume, start_container, stop_container,
 };
 use crate::kubeconfig::{rewrite_kubeconfig, rewrite_kubeconfig_remote, store_kubeconfig};
 use crate::metadata::{
-    create_cluster_metadata, create_cluster_metadata_with_host, extract_host_from_ssh_destination,
+    create_gateway_metadata, create_gateway_metadata_with_host, extract_host_from_ssh_destination,
     local_gateway_host, resolve_ssh_hostname,
 };
 use crate::mtls::store_pki_bundle;
 use crate::pki::generate_pki;
 use crate::runtime::{
     clean_stale_nodes, exec_capture_with_exit, fetch_recent_logs, navigator_workload_exists,
-    restart_navigator_deployment, wait_for_cluster_ready, wait_for_kubeconfig,
+    restart_navigator_deployment, wait_for_gateway_ready, wait_for_kubeconfig,
 };
 
-pub use crate::docker::ExistingClusterInfo;
+pub use crate::docker::ExistingGatewayInfo;
 pub use crate::kubeconfig::{
     default_local_kubeconfig_path, print_kubeconfig, stored_kubeconfig_path,
     update_local_kubeconfig,
 };
 pub use crate::metadata::{
-    ClusterMetadata, clear_active_cluster, get_cluster_metadata, list_clusters,
-    load_active_cluster, load_cluster_metadata, load_last_sandbox, remove_cluster_metadata,
-    save_active_cluster, save_last_sandbox, store_cluster_metadata,
+    GatewayMetadata, clear_active_gateway, get_gateway_metadata, list_gateways,
+    load_active_gateway, load_gateway_metadata, load_last_sandbox, remove_gateway_metadata,
+    save_active_gateway, save_last_sandbox, store_gateway_metadata,
 };
 
 /// Options for remote SSH deployment.
@@ -191,20 +191,20 @@ impl DeployOptions {
 }
 
 #[derive(Debug, Clone)]
-pub struct ClusterHandle {
+pub struct GatewayHandle {
     name: String,
     kubeconfig_path: PathBuf,
-    metadata: ClusterMetadata,
+    metadata: GatewayMetadata,
     docker: Docker,
 }
 
-impl ClusterHandle {
+impl GatewayHandle {
     pub fn kubeconfig_path(&self) -> &Path {
         &self.kubeconfig_path
     }
 
-    /// Get the cluster metadata.
-    pub fn metadata(&self) -> &ClusterMetadata {
+    /// Get the gateway metadata.
+    pub fn metadata(&self) -> &GatewayMetadata {
         &self.metadata
     }
 
@@ -218,35 +218,35 @@ impl ClusterHandle {
     }
 
     pub async fn destroy(&self) -> Result<()> {
-        destroy_cluster_resources(&self.docker, &self.name, &self.kubeconfig_path).await
+        destroy_gateway_resources(&self.docker, &self.name, &self.kubeconfig_path).await
     }
 }
 
-/// Check whether a cluster with the given name already has resources deployed.
+/// Check whether a gateway with the given name already has resources deployed.
 ///
-/// Returns `None` if no existing cluster resources are found, or
-/// `Some(ExistingClusterInfo)` with details about what exists.
+/// Returns `None` if no existing gateway resources are found, or
+/// `Some(ExistingGatewayInfo)` with details about what exists.
 pub async fn check_existing_deployment(
     name: &str,
     remote: Option<&RemoteOptions>,
-) -> Result<Option<ExistingClusterInfo>> {
+) -> Result<Option<ExistingGatewayInfo>> {
     let docker = match remote {
         Some(remote_opts) => create_ssh_docker_client(remote_opts).await?,
         None => Docker::connect_with_local_defaults().into_diagnostic()?,
     };
-    check_existing_cluster(&docker, name).await
+    check_existing_gateway(&docker, name).await
 }
 
-pub async fn deploy_cluster(options: DeployOptions) -> Result<ClusterHandle> {
-    deploy_cluster_with_logs(options, |_| {}).await
+pub async fn deploy_gateway(options: DeployOptions) -> Result<GatewayHandle> {
+    deploy_gateway_with_logs(options, |_| {}).await
 }
 
-pub async fn deploy_cluster_with_logs<F>(options: DeployOptions, on_log: F) -> Result<ClusterHandle>
+pub async fn deploy_gateway_with_logs<F>(options: DeployOptions, on_log: F) -> Result<GatewayHandle>
 where
     F: FnMut(String) + Send + 'static,
 {
     let name = options.name;
-    let image_ref = options.image_ref.unwrap_or_else(default_cluster_image_ref);
+    let image_ref = options.image_ref.unwrap_or_else(default_gateway_image_ref);
     let port = options.port;
     let gateway_host = options.gateway_host;
     let kube_port = options.kube_port;
@@ -450,17 +450,17 @@ where
         // Create a short-lived closure that locks on each call rather than holding
         // the MutexGuard across await points.
         let on_log_ref = Arc::clone(&on_log);
-        let mut cluster_log = move |msg: String| {
+        let mut gateway_log = move |msg: String| {
             if let Ok(mut f) = on_log_ref.lock() {
                 f(msg);
             }
         };
-        wait_for_cluster_ready(&target_docker, &name, &mut cluster_log).await?;
+        wait_for_gateway_ready(&target_docker, &name, &mut gateway_log).await?;
     }
 
-    // Create and store cluster metadata.
+    // Create and store gateway metadata.
     log("[progress] Persisting gateway metadata".to_string());
-    let metadata = create_cluster_metadata_with_host(
+    let metadata = create_gateway_metadata_with_host(
         &name,
         remote_opts.as_ref(),
         port,
@@ -468,9 +468,9 @@ where
         ssh_gateway_host.as_deref(),
         disable_tls,
     );
-    store_cluster_metadata(&name, &metadata)?;
+    store_gateway_metadata(&name, &metadata)?;
 
-    Ok(ClusterHandle {
+    Ok(GatewayHandle {
         name,
         kubeconfig_path,
         metadata,
@@ -478,11 +478,11 @@ where
     })
 }
 
-/// Get a handle to an existing cluster.
+/// Get a handle to an existing gateway.
 ///
-/// For local clusters, pass `None` for remote options.
-/// For remote clusters, pass the same `RemoteOptions` used during deployment.
-pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Result<ClusterHandle> {
+/// For local gateways, pass `None` for remote options.
+/// For remote gateways, pass the same `RemoteOptions` used during deployment.
+pub async fn gateway_handle(name: &str, remote: Option<&RemoteOptions>) -> Result<GatewayHandle> {
     let docker = match remote {
         Some(remote_opts) => create_ssh_docker_client(remote_opts).await?,
         None => Docker::connect_with_local_defaults().into_diagnostic()?,
@@ -490,9 +490,9 @@ pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Resul
     let kubeconfig_path = stored_kubeconfig_path(name)?;
     // Try to load existing metadata, fall back to creating new metadata
     // with the default ports (the actual ports are only known at deploy time).
-    let metadata = load_cluster_metadata(name)
-        .unwrap_or_else(|_| create_cluster_metadata(name, remote, DEFAULT_GATEWAY_PORT, None));
-    Ok(ClusterHandle {
+    let metadata = load_gateway_metadata(name)
+        .unwrap_or_else(|_| create_gateway_metadata(name, remote, DEFAULT_GATEWAY_PORT, None));
+    Ok(GatewayHandle {
         name: name.to_string(),
         kubeconfig_path,
         metadata,
@@ -500,28 +500,28 @@ pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Resul
     })
 }
 
-pub async fn ensure_cluster_image(version: &str, registry_token: Option<&str>) -> Result<String> {
+pub async fn ensure_gateway_image(version: &str, registry_token: Option<&str>) -> Result<String> {
     let docker = Docker::connect_with_local_defaults().into_diagnostic()?;
-    let image_ref = format!("{}:{version}", image::DEFAULT_CLUSTER_IMAGE);
+    let image_ref = format!("{}:{version}", image::DEFAULT_GATEWAY_IMAGE);
     ensure_image(&docker, &image_ref, registry_token).await?;
     Ok(image_ref)
 }
 
-fn default_cluster_image_ref() -> String {
+fn default_gateway_image_ref() -> String {
     if let Ok(image) = std::env::var("OPENSHELL_CLUSTER_IMAGE")
         && !image.trim().is_empty()
     {
         return image;
     }
-    image::DEFAULT_CLUSTER_IMAGE.to_string()
+    image::DEFAULT_GATEWAY_IMAGE.to_string()
 }
 
 /// Create the three TLS K8s secrets required by the `OpenShell` server and sandbox pods.
 ///
 /// Secrets are created via `kubectl` exec'd inside the cluster container:
-/// - `navigator-server-tls` (kubernetes.io/tls): server cert + key
-/// - `navigator-server-client-ca` (Opaque): CA cert for verifying client certs
-/// - `navigator-client-tls` (Opaque): client cert + key + CA cert (shared by CLI & sandboxes)
+/// - `openshell-server-tls` (kubernetes.io/tls): server cert + key
+/// - `openshell-server-client-ca` (Opaque): CA cert for verifying client certs
+/// - `openshell-client-tls` (Opaque): client cert + key + CA cert (shared by CLI & sandboxes)
 async fn create_k8s_tls_secrets(
     docker: &Docker,
     name: &str,
@@ -560,13 +560,13 @@ async fn create_k8s_tls_secrets(
         }
     };
 
-    // 1. navigator-server-tls (kubernetes.io/tls)
+    // 1. openshell-server-tls (kubernetes.io/tls)
     let server_tls_manifest = serde_json::json!({
         "apiVersion": "v1",
         "kind": "Secret",
         "metadata": {
             "name": SERVER_TLS_SECRET_NAME,
-            "namespace": "navigator"
+            "namespace": "openshell"
         },
         "type": "kubernetes.io/tls",
         "data": {
@@ -576,15 +576,15 @@ async fn create_k8s_tls_secrets(
     });
     apply_secret(server_tls_manifest.to_string())
         .await
-        .wrap_err("failed to create navigator-server-tls secret")?;
+        .wrap_err("failed to create openshell-server-tls secret")?;
 
-    // 2. navigator-server-client-ca (Opaque)
+    // 2. openshell-server-client-ca (Opaque)
     let client_ca_manifest = serde_json::json!({
         "apiVersion": "v1",
         "kind": "Secret",
         "metadata": {
             "name": SERVER_CLIENT_CA_SECRET_NAME,
-            "namespace": "navigator"
+            "namespace": "openshell"
         },
         "type": "Opaque",
         "data": {
@@ -593,15 +593,15 @@ async fn create_k8s_tls_secrets(
     });
     apply_secret(client_ca_manifest.to_string())
         .await
-        .wrap_err("failed to create navigator-server-client-ca secret")?;
+        .wrap_err("failed to create openshell-server-client-ca secret")?;
 
-    // 3. navigator-client-tls (Opaque) — shared by CLI and sandbox pods
+    // 3. openshell-client-tls (Opaque) — shared by CLI and sandbox pods
     let client_tls_manifest = serde_json::json!({
         "apiVersion": "v1",
         "kind": "Secret",
         "metadata": {
             "name": CLIENT_TLS_SECRET_NAME,
-            "namespace": "navigator"
+            "namespace": "openshell"
         },
         "type": "Opaque",
         "data": {
@@ -612,15 +612,15 @@ async fn create_k8s_tls_secrets(
     });
     apply_secret(client_tls_manifest.to_string())
         .await
-        .wrap_err("failed to create navigator-client-tls secret")?;
+        .wrap_err("failed to create openshell-client-tls secret")?;
 
     Ok(())
 }
 
-/// Reconcile cluster TLS secrets: reuse existing PKI if valid, generate new if needed.
+/// Reconcile gateway TLS secrets: reuse existing PKI if valid, generate new if needed.
 ///
 /// Returns `(bundle, rotated)` where `rotated` is true if new PKI was generated
-/// and applied to the cluster (meaning the server needs a restart to pick it up).
+/// and applied to the gateway (meaning the server needs a restart to pick it up).
 async fn reconcile_pki<F>(
     docker: &Docker,
     name: &str,
@@ -651,8 +651,8 @@ where
     // Generate fresh PKI and apply to cluster.
     // Namespace may still be creating on first bootstrap, so wait here only
     // when rotation is actually needed.
-    log("[progress] Waiting for navigator namespace".to_string());
-    wait_for_namespace(docker, &cname, kubeconfig, "navigator").await?;
+    log("[progress] Waiting for openshell namespace".to_string());
+    wait_for_namespace(docker, &cname, kubeconfig, "openshell").await?;
     log("[progress] Generating TLS certificates".to_string());
     let bundle = generate_pki(extra_sans)?;
     log("[progress] Applying TLS secrets to gateway".to_string());
@@ -776,7 +776,7 @@ async fn wait_for_namespace(
                 {
                     let logs = fetch_recent_logs(docker, container_name, 40).await;
                     return Err(miette::miette!(
-                        "cluster container is not running while waiting for namespace '{namespace}': {status_err}\n{logs}"
+                        "gateway container is not running while waiting for namespace '{namespace}': {status_err}\n{logs}"
                     ))
                     .wrap_err("K8s namespace not ready");
                 }
diff --git a/crates/navigator-bootstrap/src/metadata.rs b/crates/navigator-bootstrap/src/metadata.rs
index daf96ea08..62d9da0ca 100644
--- a/crates/navigator-bootstrap/src/metadata.rs
+++ b/crates/navigator-bootstrap/src/metadata.rs
@@ -2,19 +2,19 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::RemoteOptions;
-use crate::paths::{active_cluster_path, clusters_dir, last_sandbox_path, xdg_config_dir};
+use crate::paths::{active_gateway_path, gateways_dir, last_sandbox_path};
 use miette::{IntoDiagnostic, Result, WrapErr};
 use serde::{Deserialize, Serialize};
 use std::path::PathBuf;
 
-/// Cluster metadata stored alongside the kubeconfig.
+/// Gateway metadata stored alongside the kubeconfig.
 #[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ClusterMetadata {
-    /// The cluster name.
+pub struct GatewayMetadata {
+    /// The gateway name.
     pub name: String,
     /// Gateway endpoint URL (e.g., `https://127.0.0.1:8080`).
     pub gateway_endpoint: String,
-    /// Whether this is a remote cluster.
+    /// Whether this is a remote gateway.
     pub is_remote: bool,
     /// Host port mapped to the gateway `NodePort`.
     pub gateway_port: u16,
@@ -23,10 +23,10 @@ pub struct ClusterMetadata {
     /// Old metadata files without this field are deserialized as `None`.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub kube_port: Option<u16>,
-    /// For remote clusters, the SSH destination (e.g., `user@hostname`).
+    /// For remote gateways, the SSH destination (e.g., `user@hostname`).
     #[serde(skip_serializing_if = "Option::is_none")]
     pub remote_host: Option<String>,
-    /// For remote clusters, the resolved hostname/IP from SSH config.
+    /// For remote gateways, the resolved hostname/IP from SSH config.
     #[serde(skip_serializing_if = "Option::is_none", default)]
     pub resolved_host: Option<String>,
 
@@ -51,32 +51,32 @@ pub struct ClusterMetadata {
     pub edge_auth_url: Option<String>,
 }
 
-pub fn create_cluster_metadata(
+pub fn create_gateway_metadata(
     name: &str,
     remote: Option<&RemoteOptions>,
     port: u16,
     kube_port: Option<u16>,
-) -> ClusterMetadata {
-    create_cluster_metadata_with_host(name, remote, port, kube_port, None, false)
+) -> GatewayMetadata {
+    create_gateway_metadata_with_host(name, remote, port, kube_port, None, false)
 }
 
-/// Create cluster metadata, optionally overriding the gateway host.
+/// Create gateway metadata, optionally overriding the gateway host.
 ///
 /// When `gateway_host` is `Some`, that value is used as the host portion of
-/// `gateway_endpoint` instead of the default (`127.0.0.1` for local clusters,
-/// or the resolved SSH host for remote clusters).
+/// `gateway_endpoint` instead of the default (`127.0.0.1` for local gateways,
+/// or the resolved SSH host for remote gateways).
 ///
 /// When `disable_tls` is `true`, the gateway endpoint uses the `http://`
 /// scheme instead of `https://`.  This must match the server configuration
 /// so that the CLI connects with the correct protocol.
-pub fn create_cluster_metadata_with_host(
+pub fn create_gateway_metadata_with_host(
     name: &str,
     remote: Option<&RemoteOptions>,
     port: u16,
     kube_port: Option<u16>,
     gateway_host: Option<&str>,
     disable_tls: bool,
-) -> ClusterMetadata {
+) -> GatewayMetadata {
     let scheme = if disable_tls { "http" } else { "https" };
 
     let (gateway_endpoint, is_remote, remote_host, resolved_host) = remote.map_or_else(
@@ -103,7 +103,7 @@ pub fn create_cluster_metadata_with_host(
         },
     );
 
-    ClusterMetadata {
+    GatewayMetadata {
         name: name.to_string(),
         gateway_endpoint,
         is_remote,
@@ -146,11 +146,7 @@ pub fn local_gateway_host_from_docker_host(docker_host: &str) -> Option<String>
 }
 
 fn stored_metadata_path(name: &str) -> Result<PathBuf> {
-    let base = xdg_config_dir()?;
-    Ok(base
-        .join("openshell")
-        .join("clusters")
-        .join(format!("{name}_metadata.json")))
+    Ok(gateways_dir()?.join(name).join("metadata.json"))
 }
 
 /// Extract the hostname from an SSH destination string.
@@ -215,7 +211,7 @@ pub fn resolve_ssh_hostname(host: &str) -> String {
     }
 }
 
-pub fn store_cluster_metadata(name: &str, metadata: &ClusterMetadata) -> Result<()> {
+pub fn store_gateway_metadata(name: &str, metadata: &GatewayMetadata) -> Result<()> {
     let path = stored_metadata_path(name)?;
     if let Some(parent) = path.parent() {
         std::fs::create_dir_all(parent)
@@ -224,31 +220,31 @@ pub fn store_cluster_metadata(name: &str, metadata: &ClusterMetadata) -> Result<
     }
     let contents = serde_json::to_string_pretty(metadata)
         .into_diagnostic()
-        .wrap_err("failed to serialize cluster metadata")?;
+        .wrap_err("failed to serialize gateway metadata")?;
     std::fs::write(&path, contents)
         .into_diagnostic()
         .wrap_err_with(|| format!("failed to write metadata to {}", path.display()))?;
     Ok(())
 }
 
-pub fn load_cluster_metadata(name: &str) -> Result<ClusterMetadata> {
+pub fn load_gateway_metadata(name: &str) -> Result<GatewayMetadata> {
     let path = stored_metadata_path(name)?;
     let contents = std::fs::read_to_string(&path)
         .into_diagnostic()
         .wrap_err_with(|| format!("failed to read metadata from {}", path.display()))?;
     serde_json::from_str(&contents)
         .into_diagnostic()
-        .wrap_err("failed to parse cluster metadata")
+        .wrap_err("failed to parse gateway metadata")
 }
 
-/// Load cluster metadata if available.
-pub fn get_cluster_metadata(name: &str) -> Option<ClusterMetadata> {
-    load_cluster_metadata(name).ok()
+/// Load gateway metadata if available.
+pub fn get_gateway_metadata(name: &str) -> Option<GatewayMetadata> {
+    load_gateway_metadata(name).ok()
 }
 
-/// Save the active cluster name to persistent storage.
-pub fn save_active_cluster(name: &str) -> Result<()> {
-    let path = active_cluster_path()?;
+/// Save the active gateway name to persistent storage.
+pub fn save_active_gateway(name: &str) -> Result<()> {
+    let path = active_gateway_path()?;
     if let Some(parent) = path.parent() {
         std::fs::create_dir_all(parent)
             .into_diagnostic()
@@ -256,23 +252,23 @@ pub fn save_active_cluster(name: &str) -> Result<()> {
     }
     std::fs::write(&path, name)
         .into_diagnostic()
-        .wrap_err_with(|| format!("failed to write active cluster to {}", path.display()))?;
+        .wrap_err_with(|| format!("failed to write active gateway to {}", path.display()))?;
     Ok(())
 }
 
-/// Load the active cluster name from persistent storage.
+/// Load the active gateway name from persistent storage.
 ///
-/// Returns `None` if no active cluster has been set.
-pub fn load_active_cluster() -> Option<String> {
-    let path = active_cluster_path().ok()?;
+/// Returns `None` if no active gateway has been set.
+pub fn load_active_gateway() -> Option<String> {
+    let path = active_gateway_path().ok()?;
     let contents = std::fs::read_to_string(&path).ok()?;
     let name = contents.trim().to_string();
     if name.is_empty() { None } else { Some(name) }
 }
 
-/// Save the last-used sandbox name for a cluster to persistent storage.
-pub fn save_last_sandbox(cluster: &str, sandbox: &str) -> Result<()> {
-    let path = last_sandbox_path(cluster)?;
+/// Save the last-used sandbox name for a gateway to persistent storage.
+pub fn save_last_sandbox(gateway: &str, sandbox: &str) -> Result<()> {
+    let path = last_sandbox_path(gateway)?;
     if let Some(parent) = path.parent() {
         std::fs::create_dir_all(parent)
             .into_diagnostic()
@@ -284,50 +280,51 @@ pub fn save_last_sandbox(cluster: &str, sandbox: &str) -> Result<()> {
     Ok(())
 }
 
-/// Load the last-used sandbox name for a cluster from persistent storage.
+/// Load the last-used sandbox name for a gateway from persistent storage.
 ///
 /// Returns `None` if no last sandbox has been set.
-pub fn load_last_sandbox(cluster: &str) -> Option<String> {
-    let path = last_sandbox_path(cluster).ok()?;
+pub fn load_last_sandbox(gateway: &str) -> Option<String> {
+    let path = last_sandbox_path(gateway).ok()?;
     let contents = std::fs::read_to_string(&path).ok()?;
     let name = contents.trim().to_string();
     if name.is_empty() { None } else { Some(name) }
 }
 
-/// List all clusters that have stored metadata.
+/// List all gateways that have stored metadata.
 ///
-/// Scans `$XDG_CONFIG_HOME/navigator/clusters/` for `*_metadata.json` files
-/// and returns the parsed metadata for each.
-pub fn list_clusters() -> Result<Vec<ClusterMetadata>> {
-    let dir = clusters_dir()?;
+/// Scans `$XDG_CONFIG_HOME/openshell/gateways/` for subdirectories containing
+/// `metadata.json` and returns the parsed metadata for each.
+pub fn list_gateways() -> Result<Vec<GatewayMetadata>> {
+    let dir = gateways_dir()?;
     if !dir.exists() {
         return Ok(Vec::new());
     }
 
-    let mut clusters = Vec::new();
+    let mut gateways = Vec::new();
     let entries = std::fs::read_dir(&dir)
         .into_diagnostic()
         .wrap_err_with(|| format!("failed to read directory {}", dir.display()))?;
 
     for entry in entries {
         let entry = entry.into_diagnostic()?;
-        let file_name = entry.file_name();
-        let name_str = file_name.to_string_lossy();
-        if let Some(cluster_name) = name_str.strip_suffix("_metadata.json")
-            && let Ok(metadata) = load_cluster_metadata(cluster_name)
-        {
-            clusters.push(metadata);
+        let path = entry.path();
+        // Only consider directories that contain a metadata.json file
+        if path.is_dir() {
+            let gateway_name = entry.file_name().to_string_lossy().to_string();
+            if let Ok(metadata) = load_gateway_metadata(&gateway_name) {
+                gateways.push(metadata);
+            }
         }
     }
 
     // Sort by name for stable output
-    clusters.sort_by(|a, b| a.name.cmp(&b.name));
-    Ok(clusters)
+    gateways.sort_by(|a, b| a.name.cmp(&b.name));
+    Ok(gateways)
 }
 
-/// Remove the active cluster file (used when destroying the active cluster).
-pub fn clear_active_cluster() -> Result<()> {
-    let path = active_cluster_path()?;
+/// Remove the active gateway file (used when destroying the active gateway).
+pub fn clear_active_gateway() -> Result<()> {
+    let path = active_gateway_path()?;
     if path.exists() {
         std::fs::remove_file(&path)
             .into_diagnostic()
@@ -336,8 +333,8 @@ pub fn clear_active_cluster() -> Result<()> {
     Ok(())
 }
 
-/// Remove cluster metadata file.
-pub fn remove_cluster_metadata(name: &str) -> Result<()> {
+/// Remove gateway metadata file.
+pub fn remove_gateway_metadata(name: &str) -> Result<()> {
     let path = stored_metadata_path(name)?;
     if path.exists() {
         std::fs::remove_file(&path)
@@ -381,8 +378,8 @@ mod tests {
     }
 
     #[test]
-    fn local_cluster_metadata() {
-        let meta = create_cluster_metadata("test", None, 8080, None);
+    fn local_gateway_metadata() {
+        let meta = create_gateway_metadata("test", None, 8080, None);
         assert_eq!(meta.name, "test");
         assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:8080");
         assert_eq!(meta.gateway_port, 8080);
@@ -393,21 +390,21 @@ mod tests {
     }
 
     #[test]
-    fn local_cluster_metadata_custom_port() {
-        let meta = create_cluster_metadata("test", None, 9090, None);
+    fn local_gateway_metadata_custom_port() {
+        let meta = create_gateway_metadata("test", None, 9090, None);
         assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:9090");
         assert_eq!(meta.gateway_port, 9090);
     }
 
     #[test]
-    fn local_cluster_metadata_with_kube_port() {
-        let meta = create_cluster_metadata("test", None, 8080, Some(7443));
+    fn local_gateway_metadata_with_kube_port() {
+        let meta = create_gateway_metadata("test", None, 8080, Some(7443));
         assert_eq!(meta.kube_port, Some(7443));
     }
 
     #[test]
-    fn local_cluster_metadata_without_kube_port() {
-        let meta = create_cluster_metadata("test", None, 8080, None);
+    fn local_gateway_metadata_without_kube_port() {
+        let meta = create_gateway_metadata("test", None, 8080, None);
         assert!(meta.kube_port.is_none());
     }
 
@@ -430,9 +427,9 @@ mod tests {
     }
 
     #[test]
-    fn remote_cluster_metadata_has_resolved_host() {
+    fn remote_gateway_metadata_has_resolved_host() {
         let opts = RemoteOptions::new("user@10.0.0.5");
-        let meta = create_cluster_metadata("test", Some(&opts), 8080, Some(6443));
+        let meta = create_gateway_metadata("test", Some(&opts), 8080, Some(6443));
         assert!(meta.is_remote);
         assert_eq!(meta.remote_host.as_deref(), Some("user@10.0.0.5"));
         // When the host is a plain IP, ssh -G should resolve it to itself
@@ -447,7 +444,7 @@ mod tests {
 
     #[test]
     fn metadata_roundtrip_with_kube_port() {
-        let meta = ClusterMetadata {
+        let meta = GatewayMetadata {
             name: "test".to_string(),
             gateway_endpoint: "https://10.0.0.5:8080".to_string(),
             is_remote: true,
@@ -460,7 +457,7 @@ mod tests {
             edge_auth_url: None,
         };
         let json = serde_json::to_string(&meta).unwrap();
-        let parsed: ClusterMetadata = serde_json::from_str(&json).unwrap();
+        let parsed: GatewayMetadata = serde_json::from_str(&json).unwrap();
         assert_eq!(parsed.resolved_host.as_deref(), Some("10.0.0.5"));
         assert_eq!(parsed.gateway_endpoint, "https://10.0.0.5:8080");
         assert_eq!(parsed.gateway_port, 8080);
@@ -469,7 +466,7 @@ mod tests {
 
     #[test]
     fn metadata_roundtrip_without_kube_port() {
-        let meta = ClusterMetadata {
+        let meta = GatewayMetadata {
             name: "test".to_string(),
             gateway_endpoint: "https://10.0.0.5:8080".to_string(),
             is_remote: true,
@@ -486,7 +483,7 @@ mod tests {
             !json.contains("kube_port"),
             "None should be omitted from JSON"
         );
-        let parsed: ClusterMetadata = serde_json::from_str(&json).unwrap();
+        let parsed: GatewayMetadata = serde_json::from_str(&json).unwrap();
         assert!(parsed.kube_port.is_none());
     }
 
@@ -501,15 +498,15 @@ mod tests {
             "gateway_port": 8080,
             "remote_host": "user@myserver"
         }"#;
-        let parsed: ClusterMetadata = serde_json::from_str(json).unwrap();
+        let parsed: GatewayMetadata = serde_json::from_str(json).unwrap();
         assert!(parsed.resolved_host.is_none());
         // kube_port should default to None when not present in JSON
         assert!(parsed.kube_port.is_none());
     }
 
     #[test]
-    fn local_cluster_metadata_with_gateway_host_override() {
-        let meta = create_cluster_metadata_with_host(
+    fn local_gateway_metadata_with_gateway_host_override() {
+        let meta = create_gateway_metadata_with_host(
             "test",
             None,
             8080,
@@ -526,21 +523,21 @@ mod tests {
     }
 
     #[test]
-    fn local_cluster_metadata_with_no_gateway_host_override() {
-        // When gateway_host is None, behaviour matches create_cluster_metadata.
-        let meta = create_cluster_metadata_with_host("test", None, 8080, None, None, false);
+    fn local_gateway_metadata_with_no_gateway_host_override() {
+        // When gateway_host is None, behaviour matches create_gateway_metadata.
+        let meta = create_gateway_metadata_with_host("test", None, 8080, None, None, false);
         assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:8080");
     }
 
     #[test]
-    fn local_cluster_metadata_with_tls_disabled() {
-        let meta = create_cluster_metadata_with_host("test", None, 8080, None, None, true);
+    fn local_gateway_metadata_with_tls_disabled() {
+        let meta = create_gateway_metadata_with_host("test", None, 8080, None, None, true);
         assert_eq!(meta.gateway_endpoint, "http://127.0.0.1:8080");
     }
 
     #[test]
-    fn local_cluster_metadata_with_tls_disabled_and_gateway_host() {
-        let meta = create_cluster_metadata_with_host(
+    fn local_gateway_metadata_with_tls_disabled_and_gateway_host() {
+        let meta = create_gateway_metadata_with_host(
             "test",
             None,
             8080,
@@ -552,10 +549,10 @@ mod tests {
     }
 
     #[test]
-    fn remote_cluster_metadata_with_tls_disabled() {
+    fn remote_gateway_metadata_with_tls_disabled() {
         let opts = RemoteOptions::new("user@10.0.0.5");
         let meta =
-            create_cluster_metadata_with_host("test", Some(&opts), 8080, Some(6443), None, true);
+            create_gateway_metadata_with_host("test", Some(&opts), 8080, Some(6443), None, true);
         assert!(meta.is_remote);
         assert!(meta.gateway_endpoint.starts_with("http://"));
         assert!(!meta.gateway_endpoint.starts_with("https://"));
@@ -587,8 +584,8 @@ mod tests {
     fn save_and_load_last_sandbox_roundtrip() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            save_last_sandbox("mycluster", "dev-box").unwrap();
-            assert_eq!(load_last_sandbox("mycluster"), Some("dev-box".to_string()));
+            save_last_sandbox("mygateway", "dev-box").unwrap();
+            assert_eq!(load_last_sandbox("mygateway"), Some("dev-box".to_string()));
         });
     }
 
@@ -596,7 +593,7 @@ mod tests {
     fn load_last_sandbox_returns_none_when_not_set() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            assert_eq!(load_last_sandbox("no-such-cluster"), None);
+            assert_eq!(load_last_sandbox("no-such-gateway"), None);
         });
     }
 
@@ -604,9 +601,9 @@ mod tests {
     fn save_last_sandbox_overwrites_previous() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            save_last_sandbox("c1", "first").unwrap();
-            save_last_sandbox("c1", "second").unwrap();
-            assert_eq!(load_last_sandbox("c1"), Some("second".to_string()));
+            save_last_sandbox("g1", "first").unwrap();
+            save_last_sandbox("g1", "second").unwrap();
+            assert_eq!(load_last_sandbox("g1"), Some("second".to_string()));
         });
     }
 
@@ -614,10 +611,10 @@ mod tests {
     fn save_last_sandbox_creates_parent_dirs() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            // The cluster subdir doesn't exist yet — save should create it.
-            save_last_sandbox("brand-new-cluster", "sb1").unwrap();
+            // The gateway subdir doesn't exist yet — save should create it.
+            save_last_sandbox("brand-new-gateway", "sb1").unwrap();
             assert_eq!(
-                load_last_sandbox("brand-new-cluster"),
+                load_last_sandbox("brand-new-gateway"),
                 Some("sb1".to_string())
             );
         });
@@ -628,10 +625,10 @@ mod tests {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
             // Write the file manually with surrounding whitespace.
-            let path = last_sandbox_path("ws-cluster").unwrap();
+            let path = last_sandbox_path("ws-gateway").unwrap();
             std::fs::create_dir_all(path.parent().unwrap()).unwrap();
             std::fs::write(&path, "  my-sb \n").unwrap();
-            assert_eq!(load_last_sandbox("ws-cluster"), Some("my-sb".to_string()));
+            assert_eq!(load_last_sandbox("ws-gateway"), Some("my-sb".to_string()));
         });
     }
 
@@ -639,25 +636,25 @@ mod tests {
     fn load_last_sandbox_returns_none_for_empty_file() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            let path = last_sandbox_path("empty-cluster").unwrap();
+            let path = last_sandbox_path("empty-gateway").unwrap();
             std::fs::create_dir_all(path.parent().unwrap()).unwrap();
             std::fs::write(&path, "   \n").unwrap();
-            assert_eq!(load_last_sandbox("empty-cluster"), None);
+            assert_eq!(load_last_sandbox("empty-gateway"), None);
         });
     }
 
     #[test]
-    fn last_sandbox_is_per_cluster() {
+    fn last_sandbox_is_per_gateway() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            save_last_sandbox("cluster-a", "sandbox-a").unwrap();
-            save_last_sandbox("cluster-b", "sandbox-b").unwrap();
+            save_last_sandbox("gateway-a", "sandbox-a").unwrap();
+            save_last_sandbox("gateway-b", "sandbox-b").unwrap();
             assert_eq!(
-                load_last_sandbox("cluster-a"),
+                load_last_sandbox("gateway-a"),
                 Some("sandbox-a".to_string())
             );
             assert_eq!(
-                load_last_sandbox("cluster-b"),
+                load_last_sandbox("gateway-b"),
                 Some("sandbox-b".to_string())
             );
         });
diff --git a/crates/navigator-bootstrap/src/mtls.rs b/crates/navigator-bootstrap/src/mtls.rs
index 84b801d57..3767a639a 100644
--- a/crates/navigator-bootstrap/src/mtls.rs
+++ b/crates/navigator-bootstrap/src/mtls.rs
@@ -72,7 +72,7 @@ pub fn store_pki_bundle(name: &str, bundle: &PkiBundle) -> Result<()> {
 fn cli_mtls_dir(name: &str) -> Result<PathBuf> {
     Ok(xdg_config_dir()?
         .join("openshell")
-        .join("clusters")
+        .join("gateways")
         .join(name)
         .join("mtls"))
 }
diff --git a/crates/navigator-bootstrap/src/paths.rs b/crates/navigator-bootstrap/src/paths.rs
index 22ef36190..84bd06e30 100644
--- a/crates/navigator-bootstrap/src/paths.rs
+++ b/crates/navigator-bootstrap/src/paths.rs
@@ -14,25 +14,25 @@ pub fn xdg_config_dir() -> Result<PathBuf> {
     Ok(PathBuf::from(home).join(".config"))
 }
 
-/// Path to the file that stores the active cluster name.
+/// Path to the file that stores the active gateway name.
 ///
-/// Location: `$XDG_CONFIG_HOME/openshell/active_cluster`
-pub fn active_cluster_path() -> Result<PathBuf> {
-    Ok(xdg_config_dir()?.join("openshell").join("active_cluster"))
+/// Location: `$XDG_CONFIG_HOME/openshell/active_gateway`
+pub fn active_gateway_path() -> Result<PathBuf> {
+    Ok(xdg_config_dir()?.join("openshell").join("active_gateway"))
 }
 
-/// Base directory for all cluster metadata files.
+/// Base directory for all gateway metadata files.
 ///
-/// Location: `$XDG_CONFIG_HOME/openshell/clusters/`
-pub fn clusters_dir() -> Result<PathBuf> {
-    Ok(xdg_config_dir()?.join("openshell").join("clusters"))
+/// Location: `$XDG_CONFIG_HOME/openshell/gateways/`
+pub fn gateways_dir() -> Result<PathBuf> {
+    Ok(xdg_config_dir()?.join("openshell").join("gateways"))
 }
 
-/// Path to the file that stores the last-used sandbox name for a cluster.
+/// Path to the file that stores the last-used sandbox name for a gateway.
 ///
-/// Location: `$XDG_CONFIG_HOME/openshell/clusters/<cluster>/last_sandbox`
-pub fn last_sandbox_path(cluster: &str) -> Result<PathBuf> {
-    Ok(clusters_dir()?.join(cluster).join("last_sandbox"))
+/// Location: `$XDG_CONFIG_HOME/openshell/gateways/<gateway>/last_sandbox`
+pub fn last_sandbox_path(gateway: &str) -> Result<PathBuf> {
+    Ok(gateways_dir()?.join(gateway).join("last_sandbox"))
 }
 
 #[cfg(test)]
@@ -50,9 +50,9 @@ mod tests {
         unsafe {
             std::env::set_var("XDG_CONFIG_HOME", tmp.path());
         }
-        let path = last_sandbox_path("my-cluster").unwrap();
+        let path = last_sandbox_path("my-gateway").unwrap();
         assert!(
-            path.ends_with("openshell/clusters/my-cluster/last_sandbox"),
+            path.ends_with("openshell/gateways/my-gateway/last_sandbox"),
             "unexpected path: {path:?}"
         );
         unsafe {
diff --git a/crates/navigator-bootstrap/src/pki.rs b/crates/navigator-bootstrap/src/pki.rs
index 3abd23e2b..537030af4 100644
--- a/crates/navigator-bootstrap/src/pki.rs
+++ b/crates/navigator-bootstrap/src/pki.rs
@@ -19,9 +19,9 @@ pub struct PkiBundle {
 
 /// Default SANs always included on the server certificate.
 const DEFAULT_SERVER_SANS: &[&str] = &[
-    "navigator",
-    "navigator.navigator.svc",
-    "navigator.navigator.svc.cluster.local",
+    "openshell",
+    "openshell.openshell.svc",
+    "openshell.openshell.svc.cluster.local",
     "localhost",
     "host.docker.internal",
     "127.0.0.1",
@@ -46,10 +46,10 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
     ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
     ca_params
         .distinguished_name
-        .push(DnType::OrganizationName, "navigator");
+        .push(DnType::OrganizationName, "openshell");
     ca_params
         .distinguished_name
-        .push(DnType::CommonName, "navigator-ca");
+        .push(DnType::CommonName, "openshell-ca");
 
     let ca_cert = ca_params
         .self_signed(&ca_key)
@@ -67,7 +67,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
     server_params.subject_alt_names = server_sans;
     server_params
         .distinguished_name
-        .push(DnType::CommonName, "navigator-server");
+        .push(DnType::CommonName, "openshell-server");
 
     let server_cert = server_params
         .signed_by(&server_key, &ca_cert, &ca_key)
diff --git a/crates/navigator-bootstrap/src/push.rs b/crates/navigator-bootstrap/src/push.rs
index 0e3f39ba3..8fafc33fe 100644
--- a/crates/navigator-bootstrap/src/push.rs
+++ b/crates/navigator-bootstrap/src/push.rs
@@ -1,11 +1,11 @@
 // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 // SPDX-License-Identifier: Apache-2.0
 
-//! Push locally-built images into a k3s cluster's containerd runtime.
+//! Push locally-built images into a k3s gateway's containerd runtime.
 //!
 //! This module implements the "push" path for local development: images are
 //! exported from the local Docker daemon (equivalent to `docker save`),
-//! uploaded into the cluster container as a tar file via the Docker
+//! uploaded into the gateway container as a tar file via the Docker
 //! `put_archive` API, and then imported into containerd via `ctr images import`.
 //!
 //! The standalone `ctr` binary is used (not `k3s ctr` which may not work in
@@ -26,14 +26,14 @@ const CONTAINERD_SOCK: &str = "/run/k3s/containerd/containerd.sock";
 /// Path inside the container where the image tar is staged.
 const IMPORT_TAR_PATH: &str = "/tmp/navigator-images.tar";
 
-/// Push a list of images from the local Docker daemon into a k3s cluster's
+/// Push a list of images from the local Docker daemon into a k3s gateway's
 /// containerd runtime.
 ///
 /// All images are exported as a single tar (shared layers are deduplicated),
 /// uploaded to the container filesystem, and imported into containerd.
 pub async fn push_local_images(
     local_docker: &Docker,
-    cluster_docker: &Docker,
+    gateway_docker: &Docker,
     container_name: &str,
     images: &[&str],
     on_log: &mut impl FnMut(String),
@@ -43,7 +43,7 @@ pub async fn push_local_images(
     }
 
     on_log(format!(
-        "[status] Importing {} component image(s) into cluster",
+        "[status] Importing {} component image(s) into gateway",
         images.len()
     ));
     for img in images {
@@ -60,14 +60,14 @@ pub async fn push_local_images(
 
     // 2. Wrap the image tar as a file inside an outer tar archive and upload
     //    it into the container filesystem via the Docker put_archive API.
-    on_log("[status] Uploading images into cluster container".to_string());
+    on_log("[status] Uploading images into gateway container".to_string());
     let outer_tar = wrap_in_tar(IMPORT_TAR_PATH, &image_tar)?;
-    upload_archive(cluster_docker, container_name, &outer_tar).await?;
+    upload_archive(gateway_docker, container_name, &outer_tar).await?;
 
     // 3. Import the tar into containerd via ctr.
     on_log("[status] Running ctr images import".to_string());
     let (output, exit_code) = exec_capture_with_exit(
-        cluster_docker,
+        gateway_docker,
         container_name,
         vec![
             "ctr".to_string(),
@@ -90,7 +90,7 @@ pub async fn push_local_images(
 
     // 4. Clean up the staged tar file.
     let _ = exec_capture_with_exit(
-        cluster_docker,
+        gateway_docker,
         container_name,
         vec![
             "rm".to_string(),
@@ -100,7 +100,7 @@ pub async fn push_local_images(
     )
     .await;
 
-    on_log("[status] All component images imported into cluster".to_string());
+    on_log("[status] All component images imported into gateway".to_string());
     Ok(())
 }
 
diff --git a/crates/navigator-bootstrap/src/runtime.rs b/crates/navigator-bootstrap/src/runtime.rs
index b80c0866c..fdb010cfe 100644
--- a/crates/navigator-bootstrap/src/runtime.rs
+++ b/crates/navigator-bootstrap/src/runtime.rs
@@ -23,7 +23,7 @@ pub async fn wait_for_kubeconfig(docker: &Docker, name: &str) -> Result<String>
         {
             let logs = fetch_recent_logs(docker, &container_name, 20).await;
             return Err(miette::miette!(
-                "cluster container is not running while waiting for kubeconfig: {status_err}\n{logs}"
+                "gateway container is not running while waiting for kubeconfig: {status_err}\n{logs}"
             ));
         }
 
@@ -51,7 +51,7 @@ pub async fn wait_for_kubeconfig(docker: &Docker, name: &str) -> Result<String>
     Err(miette::miette!("timed out waiting for kubeconfig\n{logs}"))
 }
 
-pub async fn wait_for_cluster_ready<F>(docker: &Docker, name: &str, mut on_log: F) -> Result<()>
+pub async fn wait_for_gateway_ready<F>(docker: &Docker, name: &str, mut on_log: F) -> Result<()>
 where
     F: FnMut(String) + Send,
 {
@@ -94,7 +94,7 @@ where
                 .and_then(|s| s.error.as_deref())
                 .unwrap_or("");
             let mut detail =
-                format!("cluster container exited unexpectedly (exit_code={exit_code})");
+                format!("gateway container exited unexpectedly (exit_code={exit_code})");
             if !error_msg.is_empty() {
                 use std::fmt::Write;
                 let _ = write!(detail, ", error={error_msg}");
@@ -118,14 +118,14 @@ where
             }
             Some(HealthStatusEnum::UNHEALTHY) if attempt + 1 == attempts => {
                 result = Some(Err(miette::miette!(
-                    "cluster health check reported unhealthy\n{}",
+                    "gateway health check reported unhealthy\n{}",
                     format_recent_logs(&recent_logs)
                 )));
                 break;
             }
             Some(HealthStatusEnum::NONE | HealthStatusEnum::EMPTY) | None if attempt == 0 => {
                 result = Some(Err(miette::miette!(
-                    "cluster container does not expose a health check\n{}",
+                    "gateway container does not expose a health check\n{}",
                     format_recent_logs(&recent_logs)
                 )));
                 break;
@@ -139,14 +139,14 @@ where
     if result.is_none() {
         drain_logs(&mut log_rx, &mut recent_logs, &mut on_log);
         result = Some(Err(miette::miette!(
-            "timed out waiting for cluster health check\n{}",
+            "timed out waiting for gateway health check\n{}",
             format_recent_logs(&recent_logs)
         )));
     }
 
     log_task.abort();
 
-    result.unwrap_or_else(|| Err(miette::miette!("cluster health status unavailable")))
+    result.unwrap_or_else(|| Err(miette::miette!("gateway health status unavailable")))
 }
 
 async fn stream_container_logs(
diff --git a/crates/navigator-cli/src/bootstrap.rs b/crates/navigator-cli/src/bootstrap.rs
index 7bf84c160..8667ce2d5 100644
--- a/crates/navigator-cli/src/bootstrap.rs
+++ b/crates/navigator-cli/src/bootstrap.rs
@@ -3,8 +3,8 @@
 
 //! Auto-bootstrap helpers for sandbox creation.
 //!
-//! When `sandbox create` cannot reach a cluster, these helpers determine whether
-//! to offer cluster bootstrap, prompt the user for confirmation, and execute the
+//! When `sandbox create` cannot reach a gateway, these helpers determine whether
+//! to offer gateway bootstrap, prompt the user for confirmation, and execute the
 //! local or remote bootstrap flow.
 
 use crate::tls::TlsOptions;
@@ -13,28 +13,28 @@ use miette::Result;
 use owo_colors::OwoColorize;
 use std::io::IsTerminal;
 
-use crate::run::{deploy_cluster_with_panel, print_deploy_summary};
+use crate::run::{deploy_gateway_with_panel, print_deploy_summary};
 
-/// Default cluster name used during auto-bootstrap.
-const DEFAULT_CLUSTER_NAME: &str = "openshell";
+/// Default gateway name used during auto-bootstrap.
+const DEFAULT_GATEWAY_NAME: &str = "openshell";
 
-/// Determines if a gRPC connection error indicates the cluster is unreachable
+/// Determines if a gRPC connection error indicates the gateway is unreachable
 /// and bootstrap should be offered.
 ///
 /// Returns `true` for connectivity errors (connection refused, timeout, DNS failure)
-/// and for missing default TLS materials (which implies no cluster has been deployed).
+/// and for missing default TLS materials (which implies no gateway has been deployed).
 ///
 /// Returns `false` for explicit TLS configuration errors, auth failures, and other
 /// non-connectivity issues.
 pub fn should_attempt_bootstrap(error: &miette::Report, tls: &TlsOptions) -> bool {
     // If TLS paths were explicitly provided (e.g. in tests) and they failed,
-    // that's a configuration error, not a missing-cluster situation.
+    // that's a configuration error, not a missing-gateway situation.
     if tls.has_any() {
         return is_connectivity_error(error);
     }
 
     // With no explicit TLS options, missing default cert files strongly implies
-    // no cluster has been bootstrapped yet.
+    // no gateway has been bootstrapped yet.
     let msg = format!("{error:?}");
     if is_missing_tls_material(&msg) {
         return true;
@@ -95,7 +95,7 @@ fn is_connectivity_error(error: &miette::Report) -> bool {
     connectivity_patterns.iter().any(|p| lower.contains(p))
 }
 
-/// Prompt the user to confirm cluster bootstrap.
+/// Prompt the user to confirm gateway bootstrap.
 ///
 /// When `override_value` is `Some(true)` or `Some(false)`, the decision is
 /// made immediately (from `--bootstrap` / `--no-bootstrap`). Otherwise,
@@ -126,7 +126,7 @@ pub fn confirm_bootstrap(override_value: Option<bool>) -> Result<bool> {
     Ok(confirmed)
 }
 
-/// Bootstrap a local cluster and return refreshed TLS options that pick up the
+/// Bootstrap a local gateway and return refreshed TLS options that pick up the
 /// newly-written mTLS certificates.
 pub async fn run_bootstrap(
     remote: Option<&str>,
@@ -134,7 +134,7 @@ pub async fn run_bootstrap(
 ) -> Result<(TlsOptions, String)> {
     let location = if remote.is_some() { "remote" } else { "local" };
 
-    let mut options = navigator_bootstrap::DeployOptions::new(DEFAULT_CLUSTER_NAME);
+    let mut options = navigator_bootstrap::DeployOptions::new(DEFAULT_GATEWAY_NAME);
     if let Some(dest) = remote {
         let mut remote_opts = navigator_bootstrap::RemoteOptions::new(dest);
         if let Some(key) = ssh_key {
@@ -143,7 +143,7 @@ pub async fn run_bootstrap(
         options = options.with_remote(remote_opts);
     }
     // Read registry token from environment for the auto-bootstrap path.
-    // The explicit `--registry-token` flag is only on `cluster admin deploy`;
+    // The explicit `--registry-token` flag is only on `gateway start`;
     // when bootstrapping via `sandbox create`, the env var is the mechanism.
     if let Ok(token) = std::env::var("NEMOCLAW_REGISTRY_TOKEN")
         && !token.trim().is_empty()
@@ -151,20 +151,20 @@ pub async fn run_bootstrap(
         options = options.with_registry_token(token);
     }
 
-    let handle = deploy_cluster_with_panel(options, DEFAULT_CLUSTER_NAME, location).await?;
+    let handle = deploy_gateway_with_panel(options, DEFAULT_GATEWAY_NAME, location).await?;
     let server = handle.gateway_endpoint().to_string();
 
-    print_deploy_summary(DEFAULT_CLUSTER_NAME, &handle);
+    print_deploy_summary(DEFAULT_GATEWAY_NAME, &handle);
 
-    // Auto-activate the bootstrapped cluster.
-    if let Err(err) = navigator_bootstrap::save_active_cluster(DEFAULT_CLUSTER_NAME) {
-        tracing::debug!("failed to set active cluster after bootstrap: {err}");
+    // Auto-activate the bootstrapped gateway.
+    if let Err(err) = navigator_bootstrap::save_active_gateway(DEFAULT_GATEWAY_NAME) {
+        tracing::debug!("failed to set active gateway after bootstrap: {err}");
     }
 
     // Build fresh TLS options that resolve the newly-written mTLS certs from
-    // the default XDG path for this cluster, using the cluster name directly.
+    // the default XDG path for this gateway, using the gateway name directly.
     let tls = TlsOptions::default()
-        .with_cluster_name(DEFAULT_CLUSTER_NAME)
+        .with_gateway_name(DEFAULT_GATEWAY_NAME)
         .with_default_paths(&server);
 
     Ok((tls, server))
diff --git a/crates/navigator-cli/src/completers.rs b/crates/navigator-cli/src/completers.rs
index fbdd8ad2b..6fa4e1d20 100644
--- a/crates/navigator-cli/src/completers.rs
+++ b/crates/navigator-cli/src/completers.rs
@@ -6,29 +6,29 @@ use std::future::Future;
 use std::time::Duration;
 
 use clap_complete::engine::CompletionCandidate;
-use navigator_bootstrap::{list_clusters, load_active_cluster, load_cluster_metadata};
+use navigator_bootstrap::{list_gateways, load_active_gateway, load_gateway_metadata};
 use navigator_core::proto::navigator_client::NavigatorClient;
 use navigator_core::proto::{ListProvidersRequest, ListSandboxesRequest};
 use tonic::transport::{Channel, Endpoint};
 
 use crate::tls::{TlsOptions, build_tonic_tls_config, require_tls_materials};
 
-/// Complete cluster names from local metadata files (no network call).
-pub fn complete_cluster_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
-    let Ok(clusters) = list_clusters() else {
+/// Complete gateway names from local metadata files (no network call).
+pub fn complete_gateway_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
+    let Ok(gateways) = list_gateways() else {
         return Vec::new();
     };
-    clusters
+    gateways
         .into_iter()
-        .map(|c| CompletionCandidate::new(c.name))
+        .map(|g| CompletionCandidate::new(g.name))
         .collect()
 }
 
-/// Complete sandbox names by querying the active cluster's gateway.
+/// Complete sandbox names by querying the active gateway.
 pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
     blocking_complete(async {
-        let (endpoint, cluster_name) = resolve_active_cluster()?;
-        let mut client = completion_grpc_client(&endpoint, &cluster_name).await?;
+        let (endpoint, gateway_name) = resolve_active_gateway()?;
+        let mut client = completion_grpc_client(&endpoint, &gateway_name).await?;
         let response = client
             .list_sandboxes(ListSandboxesRequest {
                 limit: 200,
@@ -47,11 +47,11 @@ pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
     })
 }
 
-/// Complete provider names by querying the active cluster's gateway.
+/// Complete provider names by querying the active gateway.
 pub fn complete_provider_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
     blocking_complete(async {
-        let (endpoint, cluster_name) = resolve_active_cluster()?;
-        let mut client = completion_grpc_client(&endpoint, &cluster_name).await?;
+        let (endpoint, gateway_name) = resolve_active_gateway()?;
+        let mut client = completion_grpc_client(&endpoint, &gateway_name).await?;
         let response = client
             .list_providers(ListProvidersRequest {
                 limit: 200,
@@ -70,20 +70,20 @@ pub fn complete_provider_names(_prefix: &OsStr) -> Vec<CompletionCandidate> {
     })
 }
 
-fn resolve_active_cluster() -> Option<(String, String)> {
-    let name = std::env::var("OPENSHELL_CLUSTER")
+fn resolve_active_gateway() -> Option<(String, String)> {
+    let name = std::env::var("OPENSHELL_GATEWAY")
         .ok()
         .filter(|v| !v.trim().is_empty())
-        .or_else(load_active_cluster)?;
-    let metadata = load_cluster_metadata(&name).ok()?;
+        .or_else(load_active_gateway)?;
+    let metadata = load_gateway_metadata(&name).ok()?;
     Some((metadata.gateway_endpoint, name))
 }
 
 async fn completion_grpc_client(
     server: &str,
-    cluster_name: &str,
+    gateway_name: &str,
 ) -> Option<NavigatorClient<Channel>> {
-    let tls_opts = TlsOptions::default().with_cluster_name(cluster_name);
+    let tls_opts = TlsOptions::default().with_gateway_name(gateway_name);
     let materials = require_tls_materials(server, &tls_opts).ok()?;
     let tls_config = build_tonic_tls_config(&materials);
     let endpoint = Endpoint::from_shared(server.to_string())
@@ -131,23 +131,23 @@ mod tests {
         with_vars(
             [
                 ("XDG_CONFIG_HOME", Some(tmp.as_str())),
-                ("OPENSHELL_CLUSTER", None::<&str>),
+                ("OPENSHELL_GATEWAY", None::<&str>),
             ],
             f,
         );
     }
 
     #[test]
-    fn cluster_completer_returns_empty_when_no_config() {
+    fn gateway_completer_returns_empty_when_no_config() {
         let temp = tempfile::tempdir().unwrap();
         with_isolated_cli_env(temp.path(), || {
-            let result = complete_cluster_names(OsStr::new(""));
+            let result = complete_gateway_names(OsStr::new(""));
             assert!(result.is_empty());
         });
     }
 
     #[test]
-    fn sandbox_completer_returns_empty_when_no_active_cluster() {
+    fn sandbox_completer_returns_empty_when_no_active_gateway() {
         let temp = tempfile::tempdir().unwrap();
         with_isolated_cli_env(temp.path(), || {
             let result = complete_sandbox_names(OsStr::new(""));
@@ -156,7 +156,7 @@ mod tests {
     }
 
     #[test]
-    fn provider_completer_returns_empty_when_no_active_cluster() {
+    fn provider_completer_returns_empty_when_no_active_gateway() {
         let temp = tempfile::tempdir().unwrap();
         with_isolated_cli_env(temp.path(), || {
             let result = complete_provider_names(OsStr::new(""));
diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs
index 823b4c57c..b1f1ae88c 100644
--- a/crates/navigator-cli/src/main.rs
+++ b/crates/navigator-cli/src/main.rs
@@ -11,31 +11,31 @@ use owo_colors::OwoColorize;
 use std::io::Write;
 
 use navigator_bootstrap::{
-    edge_token::load_edge_token, get_cluster_metadata, list_clusters, load_active_cluster,
-    load_cluster_metadata, load_last_sandbox, save_last_sandbox,
+    edge_token::load_edge_token, get_gateway_metadata, list_gateways, load_active_gateway,
+    load_gateway_metadata, load_last_sandbox, save_last_sandbox,
 };
 use navigator_cli::completers;
 use navigator_cli::run;
 use navigator_cli::tls::TlsOptions;
 
-/// Resolved cluster context: name + gateway endpoint.
+/// Resolved gateway context: name + gateway endpoint.
 struct GatewayContext {
-    /// The cluster name (used for TLS cert directory, metadata lookup, etc.).
+    /// The gateway name (used for TLS cert directory, metadata lookup, etc.).
     name: String,
     /// The gateway endpoint URL (e.g., `https://127.0.0.1` or `https://10.0.0.5`).
     endpoint: String,
 }
 
-/// Resolve the cluster name to a [`GatewayContext`] with the gateway endpoint.
+/// Resolve the gateway name to a [`GatewayContext`] with the gateway endpoint.
 ///
 /// Resolution priority:
 /// 1. `--gateway-endpoint` flag (direct URL, preserving metadata when available)
-/// 2. `--cluster` flag (explicit name)
-/// 3. `OPENSHELL_CLUSTER` environment variable
-/// 4. Active cluster from `~/.config/openshell/active_cluster`
+/// 2. `--gateway` flag (explicit name)
+/// 3. `OPENSHELL_GATEWAY` environment variable
+/// 4. Active gateway from `~/.config/openshell/active_gateway`
 ///
 /// When `--gateway-endpoint` is provided, it is used directly as the endpoint.
-/// If stored metadata can still identify the gateway, the stored cluster name
+/// If stored metadata can still identify the gateway, the stored gateway name
 /// is preserved so auth and TLS materials continue to resolve correctly.
 fn normalize_gateway_endpoint(endpoint: &str) -> &str {
     endpoint.trim_end_matches('/')
@@ -44,27 +44,27 @@ fn normalize_gateway_endpoint(endpoint: &str) -> &str {
 fn find_gateway_by_endpoint(endpoint: &str) -> Option<String> {
     let endpoint = normalize_gateway_endpoint(endpoint);
 
-    if let Some(active_name) = load_active_cluster()
-        && let Ok(metadata) = load_cluster_metadata(&active_name)
+    if let Some(active_name) = load_active_gateway()
+        && let Ok(metadata) = load_gateway_metadata(&active_name)
         && normalize_gateway_endpoint(&metadata.gateway_endpoint) == endpoint
     {
         return Some(metadata.name);
     }
 
-    list_clusters().ok()?.into_iter().find_map(|metadata| {
+    list_gateways().ok()?.into_iter().find_map(|metadata| {
         (normalize_gateway_endpoint(&metadata.gateway_endpoint) == endpoint)
             .then_some(metadata.name)
     })
 }
 
 fn resolve_gateway(
-    cluster_flag: &Option<String>,
+    gateway_flag: &Option<String>,
     gateway_endpoint: &Option<String>,
 ) -> Result<GatewayContext> {
     if let Some(endpoint) = gateway_endpoint {
-        let name = cluster_flag
+        let name = gateway_flag
             .clone()
-            .filter(|name| get_cluster_metadata(name).is_some())
+            .filter(|name| get_gateway_metadata(name).is_some())
             .or_else(|| find_gateway_by_endpoint(endpoint))
             .unwrap_or_else(|| endpoint.clone());
         return Ok(GatewayContext {
@@ -73,14 +73,14 @@ fn resolve_gateway(
         });
     }
 
-    let name = cluster_flag
+    let name = gateway_flag
         .clone()
         .or_else(|| {
-            std::env::var("OPENSHELL_CLUSTER")
+            std::env::var("OPENSHELL_GATEWAY")
                 .ok()
                 .filter(|v| !v.trim().is_empty())
         })
-        .or_else(load_active_cluster)
+        .or_else(load_active_gateway)
         .ok_or_else(|| {
             miette::miette!(
                 "No active gateway.\n\
@@ -89,7 +89,7 @@ fn resolve_gateway(
             )
         })?;
 
-    let metadata = load_cluster_metadata(&name).map_err(|_| {
+    let metadata = load_gateway_metadata(&name).map_err(|_| {
         miette::miette!(
             "Unknown gateway '{name}'.\n\
              Deploy it first: openshell gateway start --name {name}\n\
@@ -103,46 +103,46 @@ fn resolve_gateway(
     })
 }
 
-/// Resolve only the cluster name (without requiring metadata to exist).
+/// Resolve only the gateway name (without requiring metadata to exist).
 ///
-/// Used by gateway commands that operate on a cluster by name but may not need
-/// the gateway endpoint (e.g., `gateway start` creates the cluster).
-fn resolve_gateway_name(cluster_flag: &Option<String>) -> Option<String> {
-    cluster_flag
+/// Used by gateway commands that operate on a gateway by name but may not need
+/// the gateway endpoint (e.g., `gateway start` creates the gateway).
+fn resolve_gateway_name(gateway_flag: &Option<String>) -> Option<String> {
+    gateway_flag
         .clone()
         .or_else(|| {
-            std::env::var("OPENSHELL_CLUSTER")
+            std::env::var("OPENSHELL_GATEWAY")
                 .ok()
                 .filter(|v| !v.trim().is_empty())
         })
-        .or_else(load_active_cluster)
+        .or_else(load_active_gateway)
 }
 
-/// Apply edge authentication token from local storage when the cluster uses edge auth.
+/// Apply edge authentication token from local storage when the gateway uses edge auth.
 ///
-/// When the resolved cluster has `auth_mode == "cloudflare_jwt"`, loads the
+/// When the resolved gateway has `auth_mode == "cloudflare_jwt"`, loads the
 /// stored edge token from disk and sets it on the `TlsOptions`. The token is
-/// always read from cluster metadata rather than supplied via a CLI flag.
-fn apply_edge_auth(tls: &mut TlsOptions, cluster_name: &str) {
-    if let Some(meta) = get_cluster_metadata(cluster_name) {
+/// always read from gateway metadata rather than supplied via a CLI flag.
+fn apply_edge_auth(tls: &mut TlsOptions, gateway_name: &str) {
+    if let Some(meta) = get_gateway_metadata(gateway_name) {
         if meta.auth_mode.as_deref() == Some("cloudflare_jwt") {
-            if let Some(token) = load_edge_token(cluster_name) {
+            if let Some(token) = load_edge_token(gateway_name) {
                 tls.edge_token = Some(token);
             }
         }
     }
 }
 
-/// Resolve a sandbox name, falling back to the last-used sandbox for the cluster.
+/// Resolve a sandbox name, falling back to the last-used sandbox for the gateway.
 ///
 /// When `name` is `None`, looks up the last sandbox recorded for the active
-/// cluster. Prints a hint when falling back so the user knows which sandbox
+/// gateway. Prints a hint when falling back so the user knows which sandbox
 /// was chosen.
-fn resolve_sandbox_name(name: Option<String>, cluster: &str) -> Result<String> {
+fn resolve_sandbox_name(name: Option<String>, gateway: &str) -> Result<String> {
     if let Some(n) = name {
         return Ok(n);
     }
-    let last = load_last_sandbox(cluster).ok_or_else(|| {
+    let last = load_last_sandbox(gateway).ok_or_else(|| {
         miette::miette!(
             "No sandbox name provided and no last-used sandbox.\n\
              Specify a sandbox name or connect to one first: nav sandbox connect <name>"
@@ -278,12 +278,12 @@ struct Cli {
     #[arg(short, long, action = clap::ArgAction::Count, global = true)]
     verbose: u8,
 
-    /// Cluster name to operate on (resolved from stored metadata).
-    #[arg(long, short, global = true, env = "OPENSHELL_CLUSTER")]
-    cluster: Option<String>,
+    /// Gateway name to operate on (resolved from stored metadata).
+    #[arg(long, short = 'g', global = true, env = "OPENSHELL_GATEWAY")]
+    gateway: Option<String>,
 
     /// Gateway endpoint URL (e.g. https://gateway.example.com).
-    /// Connects directly without looking up cluster metadata.
+    /// Connects directly without looking up gateway metadata.
     #[arg(long, global = true, env = "OPENSHELL_GATEWAY_ENDPOINT")]
     gateway_endpoint: Option<String>,
 
@@ -370,7 +370,7 @@ enum Commands {
     #[command(hide = true, after_help = INFERENCE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)]
     Inference {
         #[command(subcommand)]
-        command: Option<ClusterInferenceCommands>,
+        command: Option<InferenceCommands>,
     },
 
     // ===================================================================
@@ -395,12 +395,12 @@ enum Commands {
     ///   `openshell ssh-proxy --gateway <url> --sandbox-id <id> --token <token>`
     ///
     /// **Name mode** (for use in `~/.ssh/config`):
-    ///   `openshell ssh-proxy --cluster <name> --name <sandbox-name>`
+    ///   `openshell ssh-proxy --gateway <name> --name <sandbox-name>`
     #[command(hide = true)]
     SshProxy {
         /// Gateway URL (e.g., <https://gw.example.com:443/proxy/connect>).
-        /// Required in token mode.
-        #[arg(long)]
+        /// Required in token mode. In name mode, can be a gateway name.
+        #[arg(long, short = 'g')]
         gateway: Option<String>,
 
         /// Sandbox id. Required in token mode.
@@ -411,25 +411,18 @@ enum Commands {
         #[arg(long)]
         token: Option<String>,
 
-        /// Cluster endpoint URL. Used in name mode. Deprecated: prefer --cluster.
+        /// Gateway endpoint URL. Used in name mode. Deprecated: prefer --gateway.
         #[arg(long)]
         server: Option<String>,
 
-        /// Cluster name (resolves endpoint from stored metadata). Used in name mode.
-        #[arg(long, short)]
-        cluster: Option<String>,
+        /// Gateway name. Used with --name to resolve gateway from metadata.
+        #[arg(long)]
+        gateway_name: Option<String>,
 
         /// Sandbox name. Used in name mode.
         #[arg(long)]
         name: Option<String>,
     },
-
-    /// Manage cluster (deprecated: use `gateway`).
-    #[command(hide = true)]
-    Cluster {
-        #[command(subcommand)]
-        command: ClusterCommands,
-    },
 }
 
 #[derive(Clone, Debug, ValueEnum)]
@@ -619,7 +612,7 @@ enum GatewayCommands {
     /// Deploy/start the gateway.
     Start {
         /// Gateway name.
-        #[arg(long, default_value = "openshell", env = "OPENSHELL_CLUSTER")]
+        #[arg(long, default_value = "openshell", env = "OPENSHELL_GATEWAY")]
         name: String,
 
         /// Write stored kubeconfig into local kubeconfig.
@@ -694,14 +687,14 @@ enum GatewayCommands {
     /// Stop the gateway (preserves state).
     Stop {
         /// Gateway name (defaults to active gateway).
-        #[arg(long, env = "OPENSHELL_CLUSTER")]
+        #[arg(long, env = "OPENSHELL_GATEWAY")]
         name: Option<String>,
 
-        /// Override SSH destination (auto-resolved from cluster metadata).
+        /// Override SSH destination (auto-resolved from gateway metadata).
         #[arg(long)]
         remote: Option<String>,
 
-        /// Path to SSH private key for remote cluster.
+        /// Path to SSH private key for remote gateway.
         #[arg(long, value_hint = ValueHint::FilePath)]
         ssh_key: Option<String>,
     },
@@ -709,14 +702,14 @@ enum GatewayCommands {
     /// Destroy the gateway and its state.
     Destroy {
         /// Gateway name (defaults to active gateway).
-        #[arg(long, env = "OPENSHELL_CLUSTER")]
+        #[arg(long, env = "OPENSHELL_GATEWAY")]
         name: Option<String>,
 
-        /// Override SSH destination (auto-resolved from cluster metadata).
+        /// Override SSH destination (auto-resolved from gateway metadata).
         #[arg(long)]
         remote: Option<String>,
 
-        /// Path to SSH private key for remote cluster.
+        /// Path to SSH private key for remote gateway.
         #[arg(long, value_hint = ValueHint::FilePath)]
         ssh_key: Option<String>,
     },
@@ -747,7 +740,7 @@ enum GatewayCommands {
     /// or to authenticate a gateway added with `--no-auth`.
     Login {
         /// Gateway name (defaults to the active gateway).
-        #[arg(add = ArgValueCompleter::new(completers::complete_cluster_names))]
+        #[arg(add = ArgValueCompleter::new(completers::complete_gateway_names))]
         name: Option<String>,
     },
 
@@ -756,24 +749,24 @@ enum GatewayCommands {
     /// When called without a name, lists available gateways to choose from.
     Select {
         /// Gateway name (omit to list available gateways).
-        #[arg(add = ArgValueCompleter::new(completers::complete_cluster_names))]
+        #[arg(add = ArgValueCompleter::new(completers::complete_gateway_names))]
         name: Option<String>,
     },
 
     /// Show gateway deployment details.
     Info {
         /// Gateway name (defaults to active gateway).
-        #[arg(long, env = "OPENSHELL_CLUSTER")]
+        #[arg(long, env = "OPENSHELL_GATEWAY")]
         name: Option<String>,
     },
 
     /// Print or start an SSH tunnel for kubectl access to a remote gateway.
     Tunnel {
         /// Gateway name (defaults to active gateway).
-        #[arg(long, env = "OPENSHELL_CLUSTER")]
+        #[arg(long, env = "OPENSHELL_GATEWAY")]
         name: Option<String>,
 
-        /// Override SSH destination (auto-resolved from cluster metadata).
+        /// Override SSH destination (auto-resolved from gateway metadata).
         #[arg(long)]
         remote: Option<String>,
 
@@ -788,29 +781,12 @@ enum GatewayCommands {
 }
 
 // -----------------------------------------------------------------------
-// Hidden backwards-compat: `cluster admin deploy` → `gateway start`
+// Inference commands
 // -----------------------------------------------------------------------
 
 #[derive(Subcommand, Debug)]
-enum ClusterCommands {
-    /// Deprecated: use `gateway start`.
-    #[command(hide = true)]
-    Admin {
-        #[command(subcommand)]
-        command: ClusterAdminCommands,
-    },
-
-    /// Manage cluster-level inference configuration.
-    #[command(hide = true)]
-    Inference {
-        #[command(subcommand)]
-        command: ClusterInferenceCommands,
-    },
-}
-
-#[derive(Subcommand, Debug)]
-enum ClusterInferenceCommands {
-    /// Set cluster-level inference provider and model.
+enum InferenceCommands {
+    /// Set gateway-level inference provider and model.
     Set {
         /// Provider name.
         #[arg(long, add = ArgValueCompleter::new(completers::complete_provider_names))]
@@ -821,7 +797,7 @@ enum ClusterInferenceCommands {
         model: String,
     },
 
-    /// Update cluster-level inference configuration (partial update).
+    /// Update gateway-level inference configuration (partial update).
     Update {
         /// Provider name (unchanged if omitted).
         #[arg(long, add = ArgValueCompleter::new(completers::complete_provider_names))]
@@ -832,56 +808,10 @@ enum ClusterInferenceCommands {
         model: Option<String>,
     },
 
-    /// Get cluster-level inference provider and model.
+    /// Get gateway-level inference provider and model.
     Get,
 }
 
-#[derive(Subcommand, Debug)]
-enum ClusterAdminCommands {
-    /// Deprecated: use `gateway start`.
-    Deploy {
-        /// Cluster name.
-        #[arg(long, default_value = "openshell", env = "OPENSHELL_CLUSTER")]
-        name: String,
-
-        /// Write stored kubeconfig into local kubeconfig.
-        #[arg(long)]
-        update_kube_config: bool,
-
-        /// Print stored kubeconfig to stdout.
-        #[arg(long)]
-        get_kubeconfig: bool,
-
-        /// SSH destination for remote deployment (e.g., user@hostname).
-        #[arg(long)]
-        remote: Option<String>,
-
-        /// Path to SSH private key for remote deployment.
-        #[arg(long, value_hint = ValueHint::FilePath)]
-        ssh_key: Option<String>,
-
-        /// Host port to map to the gateway (default: 8080).
-        #[arg(long, default_value_t = navigator_bootstrap::DEFAULT_GATEWAY_PORT)]
-        port: u16,
-
-        /// Override the gateway host written into cluster metadata.
-        #[arg(long)]
-        gateway_host: Option<String>,
-
-        /// Expose the Kubernetes control plane on a host port for kubectl access.
-        #[arg(long, num_args = 0..=1, default_missing_value = "0")]
-        kube_port: Option<u16>,
-
-        /// Destroy and recreate from scratch if a cluster already exists.
-        #[arg(long)]
-        recreate: bool,
-
-        /// Authentication token for pulling container images from ghcr.io.
-        #[arg(long, env = "OPENSHELL_REGISTRY_TOKEN")]
-        registry_token: Option<String>,
-    },
-}
-
 #[derive(Subcommand, Debug)]
 enum SandboxCommands {
     /// Create a sandbox.
@@ -1190,7 +1120,7 @@ async fn main() -> Result<()> {
                 disable_gateway_auth,
                 registry_token,
             } => {
-                run::cluster_admin_deploy(
+                run::gateway_admin_deploy(
                     &name,
                     update_kube_config,
                     get_kubeconfig,
@@ -1212,9 +1142,9 @@ async fn main() -> Result<()> {
                 ssh_key,
             } => {
                 let name = name
-                    .or_else(|| resolve_gateway_name(&cli.cluster))
+                    .or_else(|| resolve_gateway_name(&cli.gateway))
                     .unwrap_or_else(|| "openshell".to_string());
-                run::cluster_admin_stop(&name, remote.as_deref(), ssh_key.as_deref()).await?;
+                run::gateway_admin_stop(&name, remote.as_deref(), ssh_key.as_deref()).await?;
             }
             GatewayCommands::Destroy {
                 name,
@@ -1222,9 +1152,9 @@ async fn main() -> Result<()> {
                 ssh_key,
             } => {
                 let name = name
-                    .or_else(|| resolve_gateway_name(&cli.cluster))
+                    .or_else(|| resolve_gateway_name(&cli.gateway))
                     .unwrap_or_else(|| "openshell".to_string());
-                run::cluster_admin_destroy(&name, remote.as_deref(), ssh_key.as_deref()).await?;
+                run::gateway_admin_destroy(&name, remote.as_deref(), ssh_key.as_deref()).await?;
             }
             GatewayCommands::Add {
                 endpoint,
@@ -1235,7 +1165,7 @@ async fn main() -> Result<()> {
             }
             GatewayCommands::Login { name } => {
                 let name = name
-                    .or_else(|| resolve_gateway_name(&cli.cluster))
+                    .or_else(|| resolve_gateway_name(&cli.gateway))
                     .ok_or_else(|| {
                         miette::miette!(
                             "No active gateway.\n\
@@ -1247,10 +1177,10 @@ async fn main() -> Result<()> {
             }
             GatewayCommands::Select { name } => {
                 if let Some(name) = name {
-                    run::cluster_use(&name)?;
+                    run::gateway_use(&name)?;
                 } else {
                     // No name provided — show available gateways.
-                    run::cluster_list(&cli.cluster)?;
+                    run::gateway_list(&cli.gateway)?;
                     eprintln!();
                     eprintln!(
                         "Select a gateway with: {}",
@@ -1260,9 +1190,9 @@ async fn main() -> Result<()> {
             }
             GatewayCommands::Info { name } => {
                 let name = name
-                    .or_else(|| resolve_gateway_name(&cli.cluster))
+                    .or_else(|| resolve_gateway_name(&cli.gateway))
                     .unwrap_or_else(|| "openshell".to_string());
-                run::cluster_admin_info(&name)?;
+                run::gateway_admin_info(&name)?;
             }
             GatewayCommands::Tunnel {
                 name,
@@ -1271,9 +1201,9 @@ async fn main() -> Result<()> {
                 print_command,
             } => {
                 let name = name
-                    .or_else(|| resolve_gateway_name(&cli.cluster))
+                    .or_else(|| resolve_gateway_name(&cli.gateway))
                     .unwrap_or_else(|| "openshell".to_string());
-                run::cluster_admin_tunnel(
+                run::gateway_admin_tunnel(
                     &name,
                     remote.as_deref(),
                     ssh_key.as_deref(),
@@ -1283,13 +1213,13 @@ async fn main() -> Result<()> {
         },
 
         // -----------------------------------------------------------
-        // Top-level status (was `cluster status`)
+        // Top-level status
         // -----------------------------------------------------------
         Some(Commands::Status) => {
-            if let Ok(ctx) = resolve_gateway(&cli.cluster, &cli.gateway_endpoint) {
-                let mut tls = tls.with_cluster_name(&ctx.name);
+            if let Ok(ctx) = resolve_gateway(&cli.gateway, &cli.gateway_endpoint) {
+                let mut tls = tls.with_gateway_name(&ctx.name);
                 apply_edge_auth(&mut tls, &ctx.name);
-                run::cluster_status(&ctx.name, &ctx.endpoint, &tls).await?;
+                run::gateway_status(&ctx.name, &ctx.endpoint, &tls).await?;
             } else {
                 println!("{}", "Gateway Status".cyan().bold());
                 println!();
@@ -1309,8 +1239,8 @@ async fn main() -> Result<()> {
             command: Some(fwd_cmd),
         }) => match fwd_cmd {
             ForwardCommands::Stop { port, name } => {
-                let cluster_name = resolve_gateway_name(&cli.cluster).unwrap_or_default();
-                let name = resolve_sandbox_name(name, &cluster_name)?;
+                let gateway_name = resolve_gateway_name(&cli.gateway).unwrap_or_default();
+                let name = resolve_sandbox_name(name, &gateway_name)?;
                 if run::stop_forward(&name, port)? {
                     eprintln!(
                         "{} Stopped forward of port {port} for sandbox {name}",
@@ -1363,8 +1293,8 @@ async fn main() -> Result<()> {
                 name,
                 background,
             } => {
-                let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-                let mut tls = tls.with_cluster_name(&ctx.name);
+                let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+                let mut tls = tls.with_gateway_name(&ctx.name);
                 apply_edge_auth(&mut tls, &ctx.name);
                 let name = resolve_sandbox_name(name, &ctx.name)?;
                 run::sandbox_forward(&ctx.endpoint, &name, port, background, &tls).await?;
@@ -1390,8 +1320,8 @@ async fn main() -> Result<()> {
             source,
             level,
         }) => {
-            let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-            let mut tls = tls.with_cluster_name(&ctx.name);
+            let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+            let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
             let name = resolve_sandbox_name(name, &ctx.name)?;
             run::sandbox_logs(
@@ -1413,8 +1343,8 @@ async fn main() -> Result<()> {
         Some(Commands::Policy {
             command: Some(policy_cmd),
         }) => {
-            let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-            let mut tls = tls.with_cluster_name(&ctx.name);
+            let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+            let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
             match policy_cmd {
                 PolicyCommands::Set {
@@ -1444,16 +1374,16 @@ async fn main() -> Result<()> {
         Some(Commands::Inference {
             command: Some(command),
         }) => {
-            let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
+            let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
             let endpoint = &ctx.endpoint;
-            let mut tls = tls.with_cluster_name(&ctx.name);
+            let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
             match command {
-                ClusterInferenceCommands::Set { provider, model } => {
-                    run::cluster_inference_set(endpoint, &provider, &model, &tls).await?;
+                InferenceCommands::Set { provider, model } => {
+                    run::gateway_inference_set(endpoint, &provider, &model, &tls).await?;
                 }
-                ClusterInferenceCommands::Update { provider, model } => {
-                    run::cluster_inference_update(
+                InferenceCommands::Update { provider, model } => {
+                    run::gateway_inference_update(
                         endpoint,
                         provider.as_deref(),
                         model.as_deref(),
@@ -1461,8 +1391,8 @@ async fn main() -> Result<()> {
                     )
                     .await?;
                 }
-                ClusterInferenceCommands::Get => {
-                    run::cluster_inference_get(endpoint, &tls).await?;
+                InferenceCommands::Get => {
+                    run::gateway_inference_get(endpoint, &tls).await?;
                 }
             }
         }
@@ -1528,7 +1458,7 @@ async fn main() -> Result<()> {
 
                     // For `sandbox create`, a missing cluster is not fatal — the
                     // bootstrap flow inside `sandbox_create` can deploy one.
-                    match resolve_gateway(&cli.cluster, &cli.gateway_endpoint) {
+                    match resolve_gateway(&cli.gateway, &cli.gateway_endpoint) {
                         Ok(ctx) => {
                             if remote.is_some() {
                                 eprintln!(
@@ -1540,7 +1470,7 @@ async fn main() -> Result<()> {
                                 return Ok(());
                             }
                             let endpoint = &ctx.endpoint;
-                            let mut tls = tls.with_cluster_name(&ctx.name);
+                            let mut tls = tls.with_gateway_name(&ctx.name);
                             apply_edge_auth(&mut tls, &ctx.name);
                             Box::pin(run::sandbox_create(
                                 endpoint,
@@ -1563,7 +1493,7 @@ async fn main() -> Result<()> {
                             .await?;
                         }
                         Err(_) => {
-                            // No cluster configured — go straight to bootstrap.
+                            // No gateway configured — go straight to bootstrap.
                             Box::pin(run::sandbox_create_with_bootstrap(
                                 name.as_deref(),
                                 from.as_deref(),
@@ -1589,8 +1519,8 @@ async fn main() -> Result<()> {
                     dest,
                     no_git_ignore,
                 } => {
-                    let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-                    let mut tls = tls.with_cluster_name(&ctx.name);
+                    let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+                    let mut tls = tls.with_gateway_name(&ctx.name);
                     apply_edge_auth(&mut tls, &ctx.name);
                     let sandbox_dest = dest.as_deref().unwrap_or("/sandbox");
                     let local = std::path::Path::new(&local_path);
@@ -1623,8 +1553,8 @@ async fn main() -> Result<()> {
                     sandbox_path,
                     dest,
                 } => {
-                    let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-                    let mut tls = tls.with_cluster_name(&ctx.name);
+                    let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+                    let mut tls = tls.with_gateway_name(&ctx.name);
                     apply_edge_auth(&mut tls, &ctx.name);
                     let local_dest = std::path::Path::new(dest.as_deref().unwrap_or("."));
                     eprintln!(
@@ -1637,9 +1567,9 @@ async fn main() -> Result<()> {
                     eprintln!("{} Download complete", "✓".green().bold());
                 }
                 other => {
-                    let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
+                    let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
                     let endpoint = &ctx.endpoint;
-                    let mut tls = tls.with_cluster_name(&ctx.name);
+                    let mut tls = tls.with_gateway_name(&ctx.name);
                     apply_edge_auth(&mut tls, &ctx.name);
                     match other {
                         SandboxCommands::Create { .. }
@@ -1678,9 +1608,9 @@ async fn main() -> Result<()> {
         Some(Commands::Provider {
             command: Some(command),
         }) => {
-            let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
+            let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
             let endpoint = &ctx.endpoint;
-            let mut tls = tls.with_cluster_name(&ctx.name);
+            let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
 
             match command {
@@ -1734,8 +1664,8 @@ async fn main() -> Result<()> {
             }
         }
         Some(Commands::Term) => {
-            let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-            let mut tls = tls.with_cluster_name(&ctx.name);
+            let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
+            let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
             let channel = navigator_cli::tls::build_channel(&ctx.endpoint, &tls).await?;
             navigator_tui::run(channel, &ctx.name, &ctx.endpoint).await?;
@@ -1756,115 +1686,51 @@ async fn main() -> Result<()> {
             sandbox_id,
             token,
             server,
-            cluster,
+            gateway_name,
             name,
         }) => {
-            match (gateway, sandbox_id, token, server, cluster, name) {
+            match (gateway, sandbox_id, token, server, gateway_name, name) {
                 // Token mode (existing behavior): pre-created session credentials.
-                (Some(gw), Some(sid), Some(tok), _, cluster_opt, _) => {
-                    let mut effective_tls = match cluster_opt {
-                        Some(ref c) => tls.with_cluster_name(c),
+                (Some(gw), Some(sid), Some(tok), _, gateway_name_opt, _) => {
+                    let mut effective_tls = match gateway_name_opt {
+                        Some(ref g) => tls.with_gateway_name(g),
                         None => tls,
                     };
-                    if let Some(ref c) = cluster_opt {
-                        apply_edge_auth(&mut effective_tls, c);
+                    if let Some(ref g) = gateway_name_opt {
+                        apply_edge_auth(&mut effective_tls, g);
                     }
                     run::sandbox_ssh_proxy(&gw, &sid, &tok, &effective_tls).await?;
                 }
-                // Name mode with --cluster: resolve endpoint from metadata.
-                (_, _, _, server_override, Some(c), Some(n)) => {
+                // Name mode with --gateway-name: resolve endpoint from metadata.
+                (_, _, _, server_override, Some(g), Some(n)) => {
                     let endpoint = if let Some(srv) = server_override {
                         srv
                     } else {
-                        let meta = load_cluster_metadata(&c).map_err(|_| {
+                        let meta = load_gateway_metadata(&g).map_err(|_| {
                             miette::miette!(
-                                "Unknown gateway '{c}'.\n\
-                                  Deploy it first: openshell gateway start --name {c}\n\
+                                "Unknown gateway '{g}'.\n\
+                                  Deploy it first: openshell gateway start --name {g}\n\
                                   Or list available gateways: openshell gateway select"
                             )
                         })?;
                         meta.gateway_endpoint
                     };
-                    let mut tls = tls.with_cluster_name(&c);
-                    apply_edge_auth(&mut tls, &c);
+                    let mut tls = tls.with_gateway_name(&g);
+                    apply_edge_auth(&mut tls, &g);
                     run::sandbox_ssh_proxy_by_name(&endpoint, &n, &tls).await?;
                 }
-                // Legacy name mode with --server only (no --cluster).
+                // Legacy name mode with --server only (no --gateway-name).
                 (_, _, _, Some(srv), None, Some(n)) => {
                     run::sandbox_ssh_proxy_by_name(&srv, &n, &tls).await?;
                 }
                 _ => {
                     return Err(miette::miette!(
-                        "provide either --gateway/--sandbox-id/--token or --cluster/--name (or --server/--name)"
+                        "provide either --gateway/--sandbox-id/--token or --gateway-name/--name (or --server/--name)"
                     ));
                 }
             }
         }
 
-        // -----------------------------------------------------------
-        // Hidden backwards-compat: `cluster admin deploy`
-        // -----------------------------------------------------------
-        Some(Commands::Cluster { command }) => match command {
-            ClusterCommands::Admin { command } => match command {
-                ClusterAdminCommands::Deploy {
-                    name,
-                    update_kube_config,
-                    get_kubeconfig,
-                    remote,
-                    ssh_key,
-                    port,
-                    gateway_host,
-                    kube_port,
-                    recreate,
-                    registry_token,
-                } => {
-                    eprintln!(
-                        "{} `openshell cluster admin deploy` is deprecated. \
-                         Use `openshell gateway start` instead.",
-                        "warning:".yellow().bold(),
-                    );
-                    run::cluster_admin_deploy(
-                        &name,
-                        update_kube_config,
-                        get_kubeconfig,
-                        remote.as_deref(),
-                        ssh_key.as_deref(),
-                        port,
-                        gateway_host.as_deref(),
-                        kube_port,
-                        recreate,
-                        false, // disable_tls
-                        false, // disable_gateway_auth
-                        registry_token.as_deref(),
-                    )
-                    .await?;
-                }
-            },
-            ClusterCommands::Inference { command } => {
-                let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?;
-                let endpoint = &ctx.endpoint;
-                let mut tls = tls.with_cluster_name(&ctx.name);
-                apply_edge_auth(&mut tls, &ctx.name);
-                match command {
-                    ClusterInferenceCommands::Set { provider, model } => {
-                        run::cluster_inference_set(endpoint, &provider, &model, &tls).await?;
-                    }
-                    ClusterInferenceCommands::Update { provider, model } => {
-                        run::cluster_inference_update(
-                            endpoint,
-                            provider.as_deref(),
-                            model.as_deref(),
-                            &tls,
-                        )
-                        .await?;
-                    }
-                    ClusterInferenceCommands::Get => {
-                        run::cluster_inference_get(endpoint, &tls).await?;
-                    }
-                }
-            }
-        },
-
         // No subcommand provided - print help for the command
         Some(Commands::Sandbox { command: None }) => {
             Cli::command()
@@ -1937,7 +1803,7 @@ fn parse_upload_spec(spec: &str) -> (String, Option<String>) {
 mod tests {
     use super::*;
     use navigator_bootstrap::{
-        ClusterMetadata, edge_token::store_edge_token, store_cluster_metadata,
+        GatewayMetadata, edge_token::store_edge_token, store_gateway_metadata,
     };
     use std::ffi::OsString;
     use std::fs;
@@ -1967,8 +1833,8 @@ mod tests {
         }
     }
 
-    fn edge_metadata(name: &str, endpoint: &str) -> ClusterMetadata {
-        ClusterMetadata {
+    fn edge_metadata(name: &str, endpoint: &str) -> GatewayMetadata {
+        GatewayMetadata {
             name: name.to_string(),
             gateway_endpoint: endpoint.to_string(),
             is_remote: true,
@@ -2156,7 +2022,7 @@ mod tests {
     fn resolve_sandbox_name_returns_explicit_name() {
         // When a name is provided, it should be returned regardless of any
         // stored last-sandbox state.
-        let result = resolve_sandbox_name(Some("explicit".to_string()), "any-cluster");
+        let result = resolve_sandbox_name(Some("explicit".to_string()), "any-gateway");
         assert_eq!(result.unwrap(), "explicit");
     }
 
@@ -2164,8 +2030,8 @@ mod tests {
     fn resolve_sandbox_name_falls_back_to_last_used() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            save_last_sandbox("test-cluster", "remembered-sb").unwrap();
-            let result = resolve_sandbox_name(None, "test-cluster");
+            save_last_sandbox("test-gateway", "remembered-sb").unwrap();
+            let result = resolve_sandbox_name(None, "test-gateway");
             assert_eq!(result.unwrap(), "remembered-sb");
         });
     }
@@ -2174,7 +2040,7 @@ mod tests {
     fn resolve_sandbox_name_errors_without_fallback() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            let err = resolve_sandbox_name(None, "unknown-cluster").unwrap_err();
+            let err = resolve_sandbox_name(None, "unknown-gateway").unwrap_err();
             let msg = err.to_string();
             assert!(
                 msg.contains("nav sandbox connect"),
@@ -2187,7 +2053,7 @@ mod tests {
     fn resolve_gateway_uses_stored_name_for_matching_endpoint() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            store_cluster_metadata(
+            store_gateway_metadata(
                 "edge-gateway",
                 &edge_metadata("edge-gateway", "https://gw.example.com"),
             )
@@ -2200,10 +2066,10 @@ mod tests {
     }
 
     #[test]
-    fn resolve_gateway_prefers_explicit_cluster_for_direct_endpoint() {
+    fn resolve_gateway_prefers_explicit_gateway_for_direct_endpoint() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            store_cluster_metadata(
+            store_gateway_metadata(
                 "named-gateway",
                 &edge_metadata("named-gateway", "https://stored.example.com"),
             )
@@ -2224,7 +2090,7 @@ mod tests {
     fn apply_edge_auth_uses_stored_token() {
         let tmp = tempfile::tempdir().unwrap();
         with_tmp_xdg(tmp.path(), || {
-            store_cluster_metadata(
+            store_gateway_metadata(
                 "edge-gateway",
                 &edge_metadata("edge-gateway", "https://gw.example.com"),
             )
diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs
index 969df8eab..ddce9560e 100644
--- a/crates/navigator-cli/src/run.rs
+++ b/crates/navigator-cli/src/run.rs
@@ -16,10 +16,10 @@ use hyper_util::{client::legacy::Client, rt::TokioExecutor};
 use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
 use miette::{IntoDiagnostic, Result, WrapErr};
 use navigator_bootstrap::{
-    ClusterMetadata, DeployOptions, RemoteOptions, clear_active_cluster,
-    default_local_kubeconfig_path, get_cluster_metadata, list_clusters, load_active_cluster,
-    print_kubeconfig, remove_cluster_metadata, save_active_cluster, save_last_sandbox,
-    store_cluster_metadata, update_local_kubeconfig,
+    DeployOptions, GatewayMetadata, RemoteOptions, clear_active_gateway,
+    default_local_kubeconfig_path, get_gateway_metadata, list_gateways, load_active_gateway,
+    print_kubeconfig, remove_gateway_metadata, save_active_gateway, save_last_sandbox,
+    store_gateway_metadata, update_local_kubeconfig,
 };
 use navigator_core::proto::{
     CreateProviderRequest, CreateSandboxRequest, DeleteProviderRequest, DeleteSandboxRequest,
@@ -378,7 +378,7 @@ fn truncate_to_width(s: &str, max_width: usize) -> String {
     format!("{}…", &s[..end])
 }
 
-struct ClusterDeployLogPanel {
+struct GatewayDeployLogPanel {
     mp: MultiProgress,
     status: String,
     progress: Option<String>,
@@ -391,7 +391,7 @@ struct ClusterDeployLogPanel {
     buffer: VecDeque<String>,
 }
 
-impl ClusterDeployLogPanel {
+impl GatewayDeployLogPanel {
     fn new(_name: &str, _location: &str) -> Self {
         let mp = MultiProgress::new();
 
@@ -584,12 +584,12 @@ fn is_progress_status(status: &str) -> bool {
         || status.starts_with("Extracting:")
 }
 
-/// Show cluster status.
+/// Show gateway status.
 #[allow(clippy::branches_sharing_code)]
-pub async fn cluster_status(cluster_name: &str, server: &str, tls: &TlsOptions) -> Result<()> {
+pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) -> Result<()> {
     println!("{}", "Server Status".cyan().bold());
     println!();
-    println!("  {} {}", "Cluster:".dimmed(), cluster_name);
+    println!("  {} {}", "Gateway:".dimmed(), gateway_name);
     println!("  {} {}", "Server:".dimmed(), server);
     if tls.is_bearer_auth() {
         println!("  {} {}", "Auth:".dimmed(), "Edge (bearer token)");
@@ -641,10 +641,10 @@ pub async fn cluster_status(cluster_name: &str, server: &str, tls: &TlsOptions)
     Ok(())
 }
 
-/// Set the active cluster.
-pub fn cluster_use(name: &str) -> Result<()> {
-    // Verify the cluster exists
-    get_cluster_metadata(name).ok_or_else(|| {
+/// Set the active gateway.
+pub fn gateway_use(name: &str) -> Result<()> {
+    // Verify the gateway exists
+    get_gateway_metadata(name).ok_or_else(|| {
         miette::miette!(
             "No gateway metadata found for '{name}'.\n\
               Deploy a gateway first with: openshell gateway start --name {name}\n\
@@ -652,7 +652,7 @@ pub fn cluster_use(name: &str) -> Result<()> {
         )
     })?;
 
-    save_active_cluster(name)?;
+    save_active_gateway(name)?;
     eprintln!("{} Active gateway set to '{name}'", "✓".green().bold());
     Ok(())
 }
@@ -685,7 +685,7 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R
     };
 
     // Build metadata for an edge-authenticated remote gateway.
-    let metadata = ClusterMetadata {
+    let metadata = GatewayMetadata {
         name: name.to_string(),
         gateway_endpoint: endpoint.clone(),
         is_remote: true,
@@ -698,8 +698,8 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R
         edge_auth_url: None,
     };
 
-    store_cluster_metadata(name, &metadata)?;
-    save_active_cluster(name)?;
+    store_gateway_metadata(name, &metadata)?;
+    save_active_gateway(name)?;
 
     if no_auth {
         eprintln!(
@@ -743,7 +743,7 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R
 ///
 /// Opens a browser for edge proxy login and stores the updated token.
 pub async fn gateway_login(name: &str) -> Result<()> {
-    let metadata = navigator_bootstrap::load_cluster_metadata(name).map_err(|_| {
+    let metadata = navigator_bootstrap::load_gateway_metadata(name).map_err(|_| {
         miette::miette!(
             "Unknown gateway '{name}'.\n\
              List available gateways: openshell gateway select"
@@ -765,12 +765,12 @@ pub async fn gateway_login(name: &str) -> Result<()> {
     Ok(())
 }
 
-/// List all provisioned clusters.
-pub fn cluster_list(gateway_flag: &Option<String>) -> Result<()> {
-    let clusters = list_clusters()?;
-    let active = gateway_flag.clone().or_else(load_active_cluster);
+/// List all provisioned gateways.
+pub fn gateway_list(gateway_flag: &Option<String>) -> Result<()> {
+    let gateways = list_gateways()?;
+    let active = gateway_flag.clone().or_else(load_active_gateway);
 
-    if clusters.is_empty() {
+    if gateways.is_empty() {
         println!("No gateways found.");
         println!();
         println!(
@@ -781,15 +781,15 @@ pub fn cluster_list(gateway_flag: &Option<String>) -> Result<()> {
     }
 
     // Calculate column widths
-    let name_width = clusters
+    let name_width = gateways
         .iter()
-        .map(|c| c.name.len())
+        .map(|g| g.name.len())
         .max()
         .unwrap_or(4)
         .max(4);
-    let endpoint_width = clusters
+    let endpoint_width = gateways
         .iter()
-        .map(|c| c.gateway_endpoint.len())
+        .map(|g| g.gateway_endpoint.len())
         .max()
         .unwrap_or(8)
         .max(8);
@@ -803,17 +803,17 @@ pub fn cluster_list(gateway_flag: &Option<String>) -> Result<()> {
     );
 
     // Print rows
-    for cluster in &clusters {
-        let is_active = active.as_deref() == Some(&cluster.name);
+    for gateway in &gateways {
+        let is_active = active.as_deref() == Some(&gateway.name);
         let marker = if is_active { "*" } else { " " };
-        let cluster_type = match cluster.auth_mode.as_deref() {
+        let gateway_type = match gateway.auth_mode.as_deref() {
             Some("cloudflare_jwt") => "edge",
-            _ if cluster.is_remote => "remote",
+            _ if gateway.is_remote => "remote",
             _ => "local",
         };
         let line = format!(
-            "{marker} {:<name_width$}  {:<endpoint_width$}  {cluster_type}",
-            cluster.name, cluster.gateway_endpoint,
+            "{marker} {:<name_width$}  {:<endpoint_width$}  {gateway_type}",
+            gateway.name, gateway.gateway_endpoint,
         );
         if is_active {
             println!("{}", line.green());
@@ -862,12 +862,12 @@ async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<Stat
     Ok(Some(resp.status()))
 }
 
-/// Prompt the user to choose how to handle an existing cluster deployment.
+/// Prompt the user to choose how to handle an existing gateway deployment.
 ///
 /// Returns `true` to recreate (destroy and start fresh), `false` to reuse.
-fn prompt_existing_cluster(
+fn prompt_existing_gateway(
     name: &str,
-    info: &navigator_bootstrap::ExistingClusterInfo,
+    info: &navigator_bootstrap::ExistingGatewayInfo,
 ) -> Result<bool> {
     let status = if info.container_running {
         "running"
@@ -896,24 +896,24 @@ fn prompt_existing_cluster(
     Ok(choice == "y" || choice == "yes")
 }
 
-/// Deploy a cluster with the rich progress panel (interactive) or simple
-/// logging (non-interactive). Returns the [`ClusterHandle`] on success.
+/// Deploy a gateway with the rich progress panel (interactive) or simple
+/// logging (non-interactive). Returns the [`GatewayHandle`] on success.
 ///
 /// This is the shared deploy UX used by both `gateway start` and
 /// the auto-bootstrap path in `sandbox create`.
-pub(crate) async fn deploy_cluster_with_panel(
+pub(crate) async fn deploy_gateway_with_panel(
     options: DeployOptions,
     name: &str,
     location: &str,
-) -> Result<navigator_bootstrap::ClusterHandle> {
+) -> Result<navigator_bootstrap::GatewayHandle> {
     let interactive = std::io::stderr().is_terminal();
 
     if interactive {
-        let panel = std::sync::Arc::new(std::sync::Mutex::new(ClusterDeployLogPanel::new(
+        let panel = std::sync::Arc::new(std::sync::Mutex::new(GatewayDeployLogPanel::new(
             name, location,
         )));
         let panel_clone = std::sync::Arc::clone(&panel);
-        let result = navigator_bootstrap::deploy_cluster_with_logs(options, move |line| {
+        let result = navigator_bootstrap::deploy_gateway_with_logs(options, move |line| {
             if let Ok(mut p) = panel_clone.lock() {
                 p.push_log(line);
             }
@@ -942,7 +942,7 @@ pub(crate) async fn deploy_cluster_with_panel(
         }
     } else {
         eprintln!("Deploying {location} gateway {name}...");
-        let handle = navigator_bootstrap::deploy_cluster_with_logs(options, |line| {
+        let handle = navigator_bootstrap::deploy_gateway_with_logs(options, |line| {
             if let Some(status) = line.strip_prefix("[status] ") {
                 eprintln!("  {status}");
             } else if line.strip_prefix("[progress] ").is_some() {
@@ -957,8 +957,8 @@ pub(crate) async fn deploy_cluster_with_panel(
     }
 }
 
-/// Print post-deploy summary showing the cluster name and gateway endpoint.
-pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::ClusterHandle) {
+/// Print post-deploy summary showing the gateway name and endpoint.
+pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::GatewayHandle) {
     eprintln!();
     eprintln!("{} {} {name}", "✓".green().bold(), "Gateway ready:".green(),);
     eprintln!(
@@ -969,8 +969,8 @@ pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::Clu
     eprintln!();
 }
 
-/// Provision or start a cluster (local or remote).
-pub async fn cluster_admin_deploy(
+/// Provision or start a gateway (local or remote).
+pub async fn gateway_admin_deploy(
     name: &str,
     update_kube_config: bool,
     get_kubeconfig: bool,
@@ -1014,7 +1014,7 @@ pub async fn cluster_admin_deploy(
 
     let interactive = std::io::stderr().is_terminal();
 
-    // Check for existing cluster and prompt user if found.
+    // Check for existing gateway and prompt user if found.
     // --recreate skips the prompt and always destroys.
     {
         let remote_opts = remote.map(|dest| {
@@ -1030,7 +1030,7 @@ pub async fn cluster_admin_deploy(
             let should_recreate = if recreate {
                 true
             } else if interactive {
-                prompt_existing_cluster(name, &info)?
+                prompt_existing_gateway(name, &info)?
             } else {
                 false // non-interactive without --recreate: silently reuse
             };
@@ -1038,7 +1038,7 @@ pub async fn cluster_admin_deploy(
             if should_recreate {
                 eprintln!("• Destroying existing gateway...");
                 let handle =
-                    navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?;
+                    navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
                 handle.destroy().await?;
                 eprintln!("{} Gateway destroyed, starting fresh.", "✓".green().bold());
                 eprintln!();
@@ -1047,7 +1047,7 @@ pub async fn cluster_admin_deploy(
         }
     }
 
-    let handle = deploy_cluster_with_panel(options, name, location).await?;
+    let handle = deploy_gateway_with_panel(options, name, location).await?;
 
     if update_kube_config {
         let target_path = default_local_kubeconfig_path()?;
@@ -1065,63 +1065,63 @@ pub async fn cluster_admin_deploy(
 
     print_deploy_summary(name, &handle);
 
-    // Auto-activate: set this cluster as the active cluster.
-    save_active_cluster(name)?;
+    // Auto-activate: set this gateway as the active gateway.
+    save_active_gateway(name)?;
     eprintln!("{} Active gateway set to '{name}'", "✓".green().bold());
 
     Ok(())
 }
 
-/// Resolve the remote SSH destination for a cluster.
+/// Resolve the remote SSH destination for a gateway.
 ///
 /// If `remote_override` is provided, use it. Otherwise, look up the remote
-/// host from stored cluster metadata.
-enum ClusterControlTarget {
+/// host from stored gateway metadata.
+enum GatewayControlTarget {
     Local,
     Remote(String),
     ExternalRegistration,
 }
 
-fn resolve_cluster_control_target(
+fn resolve_gateway_control_target(
     name: &str,
     remote_override: Option<&str>,
-) -> ClusterControlTarget {
-    resolve_cluster_control_target_from(get_cluster_metadata(name), remote_override)
+) -> GatewayControlTarget {
+    resolve_gateway_control_target_from(get_gateway_metadata(name), remote_override)
 }
 
-fn resolve_cluster_control_target_from(
-    metadata: Option<ClusterMetadata>,
+fn resolve_gateway_control_target_from(
+    metadata: Option<GatewayMetadata>,
     remote_override: Option<&str>,
-) -> ClusterControlTarget {
+) -> GatewayControlTarget {
     if let Some(r) = remote_override {
-        return ClusterControlTarget::Remote(r.to_string());
+        return GatewayControlTarget::Remote(r.to_string());
     }
 
     match metadata {
         Some(metadata) if metadata.is_remote => metadata
             .remote_host
-            .map(ClusterControlTarget::Remote)
-            .unwrap_or(ClusterControlTarget::ExternalRegistration),
-        _ => ClusterControlTarget::Local,
+            .map(GatewayControlTarget::Remote)
+            .unwrap_or(GatewayControlTarget::ExternalRegistration),
+        _ => GatewayControlTarget::Local,
     }
 }
 
-fn cluster_control_target_options(
+fn gateway_control_target_options(
     name: &str,
     remote_override: Option<&str>,
     ssh_key: Option<&str>,
 ) -> Result<Option<RemoteOptions>> {
-    match resolve_cluster_control_target(name, remote_override) {
-        ClusterControlTarget::Local => Ok(None),
-        ClusterControlTarget::Remote(dest) => {
+    match resolve_gateway_control_target(name, remote_override) {
+        GatewayControlTarget::Local => Ok(None),
+        GatewayControlTarget::Remote(dest) => {
             let mut opts = RemoteOptions::new(&dest);
             if let Some(key) = ssh_key {
                 opts = opts.with_ssh_key(key);
             }
             Ok(Some(opts))
         }
-        ClusterControlTarget::ExternalRegistration => Err(miette::miette!(
-            "Gateway '{name}' is an external registration, not a managed Docker cluster.\n\
+        GatewayControlTarget::ExternalRegistration => Err(miette::miette!(
+            "Gateway '{name}' is an external registration, not a managed Docker gateway.\n\
              `openshell gateway stop` is only supported for local or SSH-managed gateways."
         )),
     }
@@ -1131,60 +1131,60 @@ fn remove_gateway_registration(name: &str) {
     if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) {
         tracing::debug!("failed to remove edge token: {err}");
     }
-    if let Err(err) = remove_cluster_metadata(name) {
-        tracing::debug!("failed to remove cluster metadata: {err}");
+    if let Err(err) = remove_gateway_metadata(name) {
+        tracing::debug!("failed to remove gateway metadata: {err}");
     }
-    if load_active_cluster().as_deref() == Some(name)
-        && let Err(err) = clear_active_cluster()
+    if load_active_gateway().as_deref() == Some(name)
+        && let Err(err) = clear_active_gateway()
     {
-        tracing::debug!("failed to clear active cluster: {err}");
+        tracing::debug!("failed to clear active gateway: {err}");
     }
 }
 
-fn cleanup_cluster_metadata(name: &str) {
+fn cleanup_gateway_metadata(name: &str) {
     if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) {
         tracing::debug!("failed to remove edge token: {err}");
     }
-    if let Err(err) = remove_cluster_metadata(name) {
-        tracing::debug!("failed to remove cluster metadata: {err}");
+    if let Err(err) = remove_gateway_metadata(name) {
+        tracing::debug!("failed to remove gateway metadata: {err}");
     }
-    if load_active_cluster().as_deref() == Some(name)
-        && let Err(err) = clear_active_cluster()
+    if load_active_gateway().as_deref() == Some(name)
+        && let Err(err) = clear_active_gateway()
     {
-        tracing::debug!("failed to clear active cluster: {err}");
+        tracing::debug!("failed to clear active gateway: {err}");
     }
 }
 
 fn resolve_remote(name: &str, remote_override: Option<&str>) -> Option<String> {
-    match resolve_cluster_control_target(name, remote_override) {
-        ClusterControlTarget::Remote(dest) => Some(dest),
-        ClusterControlTarget::Local | ClusterControlTarget::ExternalRegistration => None,
+    match resolve_gateway_control_target(name, remote_override) {
+        GatewayControlTarget::Remote(dest) => Some(dest),
+        GatewayControlTarget::Local | GatewayControlTarget::ExternalRegistration => None,
     }
 }
 
-/// Stop a cluster.
-pub async fn cluster_admin_stop(
+/// Stop a gateway.
+pub async fn gateway_admin_stop(
     name: &str,
     remote: Option<&str>,
     ssh_key: Option<&str>,
 ) -> Result<()> {
-    let remote_opts = cluster_control_target_options(name, remote, ssh_key)?;
+    let remote_opts = gateway_control_target_options(name, remote, ssh_key)?;
 
-    eprintln!("• Stopping cluster {name}...");
-    let handle = navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?;
+    eprintln!("• Stopping gateway {name}...");
+    let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
     handle.stop().await?;
-    eprintln!("{} Cluster {name} stopped.", "✓".green().bold());
+    eprintln!("{} Gateway {name} stopped.", "✓".green().bold());
     Ok(())
 }
 
-/// Destroy a cluster and its state.
-pub async fn cluster_admin_destroy(
+/// Destroy a gateway and its state.
+pub async fn gateway_admin_destroy(
     name: &str,
     remote: Option<&str>,
     ssh_key: Option<&str>,
 ) -> Result<()> {
-    match resolve_cluster_control_target(name, remote) {
-        ClusterControlTarget::ExternalRegistration => {
+    match resolve_gateway_control_target(name, remote) {
+        GatewayControlTarget::ExternalRegistration => {
             eprintln!("• Removing gateway registration {name}...");
             remove_gateway_registration(name);
             eprintln!(
@@ -1193,24 +1193,24 @@ pub async fn cluster_admin_destroy(
             );
             Ok(())
         }
-        ClusterControlTarget::Local | ClusterControlTarget::Remote(_) => {
-            let remote_opts = cluster_control_target_options(name, remote, ssh_key)?;
+        GatewayControlTarget::Local | GatewayControlTarget::Remote(_) => {
+            let remote_opts = gateway_control_target_options(name, remote, ssh_key)?;
 
-            eprintln!("• Destroying cluster {name}...");
-            let handle = navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?;
+            eprintln!("• Destroying gateway {name}...");
+            let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
             handle.destroy().await?;
 
-            cleanup_cluster_metadata(name);
+            cleanup_gateway_metadata(name);
 
-            eprintln!("{} Cluster {name} destroyed.", "✓".green().bold());
+            eprintln!("{} Gateway {name} destroyed.", "✓".green().bold());
             Ok(())
         }
     }
 }
 
-/// Show cluster deployment details.
-pub fn cluster_admin_info(name: &str) -> Result<()> {
-    let metadata = get_cluster_metadata(name).ok_or_else(|| {
+/// Show gateway deployment details.
+pub fn gateway_admin_info(name: &str) -> Result<()> {
+    let metadata = get_gateway_metadata(name).ok_or_else(|| {
         miette::miette!(
             "No gateway metadata found for '{name}'.\n\
               Deploy a gateway first with: openshell gateway start --name {name}"
@@ -1219,9 +1219,9 @@ pub fn cluster_admin_info(name: &str) -> Result<()> {
 
     let kubeconfig_path = navigator_bootstrap::stored_kubeconfig_path(name)?;
 
-    println!("{}", "Cluster Info".cyan().bold());
+    println!("{}", "Gateway Info".cyan().bold());
     println!();
-    println!("  {} {}", "Cluster:".dimmed(), metadata.name);
+    println!("  {} {}", "Gateway:".dimmed(), metadata.name);
     println!(
         "  {} {}",
         "Gateway endpoint:".dimmed(),
@@ -1259,8 +1259,8 @@ pub fn cluster_admin_info(name: &str) -> Result<()> {
     Ok(())
 }
 
-/// Print or start an SSH tunnel for kubectl access to a remote cluster.
-pub fn cluster_admin_tunnel(
+/// Print or start an SSH tunnel for kubectl access to a remote gateway.
+pub fn gateway_admin_tunnel(
     name: &str,
     remote_override: Option<&str>,
     ssh_key: Option<&str>,
@@ -1268,16 +1268,16 @@ pub fn cluster_admin_tunnel(
 ) -> Result<()> {
     let remote = resolve_remote(name, remote_override).ok_or_else(|| {
         miette::miette!(
-            "Cluster '{name}' is not a remote cluster (no SSH destination found).\n\
-             SSH tunnels are only needed for remote clusters."
+            "Gateway '{name}' is not a remote gateway (no SSH destination found).\n\
+             SSH tunnels are only needed for remote gateways."
         )
     })?;
 
-    let kube_port = get_cluster_metadata(name)
+    let kube_port = get_gateway_metadata(name)
         .and_then(|m| m.kube_port)
         .ok_or_else(|| {
             miette::miette!(
-                "Cluster '{name}' was deployed without --kube-port.\n\
+                "Gateway '{name}' was deployed without --kube-port.\n\
                  Redeploy with --kube-port <port> to enable kubectl access via SSH tunnel."
             )
         })?;
@@ -1310,9 +1310,9 @@ pub fn cluster_admin_tunnel(
     Ok(())
 }
 
-/// Create a sandbox when no cluster is configured.
+/// Create a sandbox when no gateway is configured.
 ///
-/// Offers to bootstrap a new cluster first, then delegates to [`sandbox_create`].
+/// Offers to bootstrap a new gateway first, then delegates to [`sandbox_create`].
 #[allow(clippy::too_many_arguments)]
 pub async fn sandbox_create_with_bootstrap(
     name: Option<&str>,
@@ -1337,13 +1337,13 @@ pub async fn sandbox_create_with_bootstrap(
         ));
     }
     let (tls, server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?;
-    // The bootstrap flow always creates a cluster named "openshell".
-    let cluster_name = "openshell";
+    // The bootstrap flow always creates a gateway named "openshell".
+    let gateway_name = "openshell";
     sandbox_create(
         &server,
         name,
         from,
-        cluster_name,
+        gateway_name,
         upload,
         keep,
         remote,
@@ -1366,7 +1366,7 @@ pub async fn sandbox_create(
     server: &str,
     name: Option<&str>,
     from: Option<&str>,
-    cluster_name: &str,
+    gateway_name: &str,
     upload: Option<&(String, Option<String>, bool)>,
     keep: bool,
     remote: Option<&str>,
@@ -1380,7 +1380,7 @@ pub async fn sandbox_create(
     auto_providers_override: Option<bool>,
     tls: &TlsOptions,
 ) -> Result<()> {
-    // Try connecting to the cluster. If it fails due to an unreachable cluster,
+    // Try connecting to the gateway. If it fails due to an unreachable gateway,
     // offer to bootstrap a local one and retry.
     let (mut client, effective_server, effective_tls) = match grpc_client(server, tls).await {
         Ok(c) => (c, server.to_string(), tls.clone()),
@@ -1394,7 +1394,7 @@ pub async fn sandbox_create(
             let (new_tls, new_server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?;
             let c = grpc_client(&new_server, &new_tls)
                 .await
-                .wrap_err("bootstrap succeeded but failed to connect to cluster")?;
+                .wrap_err("bootstrap succeeded but failed to connect to gateway")?;
             (c, new_server, new_tls)
         }
     };
@@ -1410,7 +1410,7 @@ pub async fn sandbox_create(
                     dockerfile,
                     context,
                 } => {
-                    let tag = build_from_dockerfile(&dockerfile, &context, cluster_name).await?;
+                    let tag = build_from_dockerfile(&dockerfile, &context, gateway_name).await?;
                     Some(tag)
                 }
             }
@@ -1464,9 +1464,9 @@ pub async fn sandbox_create(
     let interactive = std::io::stdout().is_terminal();
     let sandbox_name = sandbox.name.clone();
 
-    // Record this sandbox as the last-used for the active cluster.
-    if let Some(cluster) = effective_tls.cluster_name() {
-        let _ = save_last_sandbox(cluster, &sandbox_name);
+    // Record this sandbox as the last-used for the active gateway.
+    if let Some(gateway) = effective_tls.gateway_name() {
+        let _ = save_last_sandbox(gateway, &sandbox_name);
     }
 
     // Set up display — interactive terminals get a step-based checklist with
@@ -1883,20 +1883,20 @@ fn resolve_from(value: &str) -> Result<ResolvedSource> {
     Ok(ResolvedSource::Image(format!("{prefix}/{value}:latest")))
 }
 
-/// Build a Dockerfile and push the resulting image into the cluster.
+/// Build a Dockerfile and push the resulting image into the gateway.
 ///
 /// Returns the image tag that was built so the caller can use it for sandbox
 /// creation.
 async fn build_from_dockerfile(
     dockerfile: &Path,
     context: &Path,
-    cluster_name: &str,
+    gateway_name: &str,
 ) -> Result<String> {
     let timestamp = std::time::SystemTime::now()
         .duration_since(std::time::UNIX_EPOCH)
         .unwrap_or_default()
         .as_secs();
-    let tag = format!("navigator/sandbox-from:{timestamp}");
+    let tag = format!("openshell/sandbox-from:{timestamp}");
 
     eprintln!(
         "Building image {} from {}",
@@ -1904,7 +1904,7 @@ async fn build_from_dockerfile(
         dockerfile.display()
     );
     eprintln!("  {} {}", "Context:".dimmed(), context.display());
-    eprintln!("  {} {}", "Cluster:".dimmed(), cluster_name);
+    eprintln!("  {} {}", "Gateway:".dimmed(), gateway_name);
     eprintln!();
 
     let mut on_log = |msg: String| {
@@ -1915,7 +1915,7 @@ async fn build_from_dockerfile(
         dockerfile,
         &tag,
         context,
-        cluster_name,
+        gateway_name,
         &HashMap::new(),
         &mut on_log,
     )
@@ -1923,7 +1923,7 @@ async fn build_from_dockerfile(
 
     eprintln!();
     eprintln!(
-        "{} Image {} is available in the cluster.",
+        "{} Image {} is available in the gateway.",
         "✓".green().bold(),
         tag.cyan(),
     );
@@ -2785,7 +2785,7 @@ pub async fn provider_delete(server: &str, names: &[String], tls: &TlsOptions) -
     Ok(())
 }
 
-pub async fn cluster_inference_set(
+pub async fn gateway_inference_set(
     server: &str,
     provider_name: &str,
     model_id: &str,
@@ -2801,7 +2801,7 @@ pub async fn cluster_inference_set(
         .into_diagnostic()?;
 
     let configured = response.into_inner();
-    println!("{}", "Cluster inference configured:".cyan().bold());
+    println!("{}", "Gateway inference configured:".cyan().bold());
     println!();
     println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
     println!("  {} {}", "Model:".dimmed(), configured.model_id);
@@ -2809,7 +2809,7 @@ pub async fn cluster_inference_set(
     Ok(())
 }
 
-pub async fn cluster_inference_update(
+pub async fn gateway_inference_update(
     server: &str,
     provider_name: Option<&str>,
     model_id: Option<&str>,
@@ -2842,7 +2842,7 @@ pub async fn cluster_inference_update(
         .into_diagnostic()?;
 
     let configured = response.into_inner();
-    println!("{}", "Cluster inference updated:".cyan().bold());
+    println!("{}", "Gateway inference updated:".cyan().bold());
     println!();
     println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
     println!("  {} {}", "Model:".dimmed(), configured.model_id);
@@ -2850,7 +2850,7 @@ pub async fn cluster_inference_update(
     Ok(())
 }
 
-pub async fn cluster_inference_get(server: &str, tls: &TlsOptions) -> Result<()> {
+pub async fn gateway_inference_get(server: &str, tls: &TlsOptions) -> Result<()> {
     let mut client = grpc_inference_client(server, tls).await?;
     let response = client
         .get_cluster_inference(GetClusterInferenceRequest {})
@@ -2858,7 +2858,7 @@ pub async fn cluster_inference_get(server: &str, tls: &TlsOptions) -> Result<()>
         .into_diagnostic()?;
 
     let configured = response.into_inner();
-    println!("{}", "Cluster inference:".cyan().bold());
+    println!("{}", "Gateway inference:".cyan().bold());
     println!();
     println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
     println!("  {} {}", "Model:".dimmed(), configured.model_id);
@@ -3369,8 +3369,8 @@ fn print_log_line(log: &navigator_core::proto::SandboxLogLine) {
 #[cfg(test)]
 mod tests {
     use super::{
-        ClusterControlTarget, TlsOptions, git_sync_files, http_health_check,
-        inferred_provider_type, parse_credential_pairs, resolve_cluster_control_target_from,
+        GatewayControlTarget, TlsOptions, git_sync_files, http_health_check,
+        inferred_provider_type, parse_credential_pairs, resolve_gateway_control_target_from,
     };
     use hyper::StatusCode;
     use std::fs;
@@ -3380,7 +3380,7 @@ mod tests {
     use std::process::Command;
     use std::thread;
 
-    use navigator_bootstrap::ClusterMetadata;
+    use navigator_bootstrap::GatewayMetadata;
 
     struct EnvVarGuard {
         key: &'static str,
@@ -3421,8 +3421,8 @@ mod tests {
         }
     }
 
-    fn edge_registration(name: &str, endpoint: &str) -> ClusterMetadata {
-        ClusterMetadata {
+    fn edge_registration(name: &str, endpoint: &str) -> GatewayMetadata {
+        GatewayMetadata {
             name: name.to_string(),
             gateway_endpoint: endpoint.to_string(),
             is_remote: true,
@@ -3562,18 +3562,18 @@ mod tests {
     }
 
     #[test]
-    fn resolve_cluster_control_target_marks_edge_registration_unmanaged() {
+    fn resolve_gateway_control_target_marks_edge_registration_unmanaged() {
         let metadata = edge_registration("edge-gateway", "https://gw.example.com");
-        let target = resolve_cluster_control_target_from(Some(metadata), None);
-        assert!(matches!(target, ClusterControlTarget::ExternalRegistration));
+        let target = resolve_gateway_control_target_from(Some(metadata), None);
+        assert!(matches!(target, GatewayControlTarget::ExternalRegistration));
     }
 
     #[test]
-    fn resolve_cluster_control_target_prefers_explicit_remote_override() {
-        let target = resolve_cluster_control_target_from(None, Some("user@host"));
+    fn resolve_gateway_control_target_prefers_explicit_remote_override() {
+        let target = resolve_gateway_control_target_from(None, Some("user@host"));
         match target {
-            ClusterControlTarget::Remote(dest) => assert_eq!(dest, "user@host"),
-            ClusterControlTarget::Local | ClusterControlTarget::ExternalRegistration => {
+            GatewayControlTarget::Remote(dest) => assert_eq!(dest, "user@host"),
+            GatewayControlTarget::Local | GatewayControlTarget::ExternalRegistration => {
                 panic!("expected remote target")
             }
         }
diff --git a/crates/navigator-cli/src/ssh.rs b/crates/navigator-cli/src/ssh.rs
index 06f4f5547..8ccf64adc 100644
--- a/crates/navigator-cli/src/ssh.rs
+++ b/crates/navigator-cli/src/ssh.rs
@@ -78,15 +78,15 @@ async fn ssh_session_config(
             session.gateway_scheme, gateway_host, gateway_port, session.connect_path
         )
     };
-    let cluster_name = tls
-        .cluster_name()
-        .ok_or_else(|| miette::miette!("cluster name is required to build SSH proxy command"))?;
+    let gateway_name = tls
+        .gateway_name()
+        .ok_or_else(|| miette::miette!("gateway name is required to build SSH proxy command"))?;
     let proxy_command = format!(
-        "{exe_command} ssh-proxy --gateway {} --sandbox-id {} --token {} --cluster {}",
+        "{exe_command} ssh-proxy --gateway {} --sandbox-id {} --token {} --gateway-name {}",
         gateway_url,
         session.sandbox_id,
         session.token,
-        shell_escape(cluster_name),
+        shell_escape(gateway_name),
     );
 
     Ok(SshSessionConfig {
@@ -580,14 +580,14 @@ pub async fn sandbox_ssh_proxy_by_name(server: &str, name: &str, tls: &TlsOption
 /// The output is suitable for appending to `~/.ssh/config` so that tools like
 /// `VSCode` Remote-SSH can connect to the sandbox by host alias.
 ///
-/// The `ProxyCommand` uses `--cluster` so that `ssh-proxy` resolves the
+/// The `ProxyCommand` uses `--gateway-name` so that `ssh-proxy` resolves the
 /// gateway endpoint and TLS certificates from the gateway metadata directory
-/// (`~/.config/openshell/clusters/<name>/mtls/`).
-pub fn print_ssh_config(cluster: &str, name: &str) {
+/// (`~/.config/openshell/gateways/<name>/mtls/`).
+pub fn print_ssh_config(gateway: &str, name: &str) {
     let exe = std::env::current_exe().expect("failed to resolve OpenShell executable");
     let exe = shell_escape(&exe.to_string_lossy());
 
-    let proxy_cmd = format!("{exe} ssh-proxy --cluster {cluster} --name {name}");
+    let proxy_cmd = format!("{exe} ssh-proxy --gateway-name {gateway} --name {name}");
 
     println!("Host openshell-{name}");
     println!("    User sandbox");
diff --git a/crates/navigator-cli/src/tls.rs b/crates/navigator-cli/src/tls.rs
index a885161b6..e977b0cc7 100644
--- a/crates/navigator-cli/src/tls.rs
+++ b/crates/navigator-cli/src/tls.rs
@@ -29,8 +29,8 @@ pub struct TlsOptions {
     ca: Option<PathBuf>,
     cert: Option<PathBuf>,
     key: Option<PathBuf>,
-    /// Cluster name for resolving default cert directory.
-    cluster_name: Option<String>,
+    /// Gateway name for resolving default cert directory.
+    gateway_name: Option<String>,
     /// Edge auth bearer token — when set, disables mTLS client certs and
     /// injects authentication headers on every gRPC request instead.
     pub edge_token: Option<String>,
@@ -42,7 +42,7 @@ impl TlsOptions {
             ca,
             cert,
             key,
-            cluster_name: None,
+            gateway_name: None,
             edge_token: None,
         }
     }
@@ -51,16 +51,16 @@ impl TlsOptions {
         self.ca.is_some() || self.cert.is_some() || self.key.is_some()
     }
 
-    /// Return the cluster name, if set.
-    pub fn cluster_name(&self) -> Option<&str> {
-        self.cluster_name.as_deref()
+    /// Return the gateway name, if set.
+    pub fn gateway_name(&self) -> Option<&str> {
+        self.gateway_name.as_deref()
     }
 
-    /// Set the cluster name for cert directory resolution.
+    /// Set the gateway name for cert directory resolution.
     #[must_use]
-    pub fn with_cluster_name(&self, name: &str) -> Self {
+    pub fn with_gateway_name(&self, name: &str) -> Self {
         Self {
-            cluster_name: Some(name.to_string()),
+            gateway_name: Some(name.to_string()),
             ..self.clone()
         }
     }
@@ -68,9 +68,9 @@ impl TlsOptions {
     #[must_use]
     pub fn with_default_paths(&self, server: &str) -> Self {
         let base = self
-            .cluster_name
+            .gateway_name
             .as_deref()
-            .and_then(tls_dir_for_cluster)
+            .and_then(tls_dir_for_gateway)
             .or_else(|| default_tls_dir(server));
         Self {
             ca: self
@@ -85,7 +85,7 @@ impl TlsOptions {
                 .key
                 .clone()
                 .or_else(|| base.as_ref().map(|dir| dir.join("tls.key"))),
-            cluster_name: self.cluster_name.clone(),
+            gateway_name: self.gateway_name.clone(),
             ..self.clone()
         }
     }
@@ -102,18 +102,18 @@ pub struct TlsMaterials {
     key: Vec<u8>,
 }
 
-/// Resolve the TLS cert directory for a known cluster name.
-fn tls_dir_for_cluster(name: &str) -> Option<PathBuf> {
+/// Resolve the TLS cert directory for a known gateway name.
+fn tls_dir_for_gateway(name: &str) -> Option<PathBuf> {
     let safe_name = sanitize_name(name);
-    let base = xdg_config_dir().ok()?.join("openshell").join("clusters");
+    let base = xdg_config_dir().ok()?.join("openshell").join("gateways");
     Some(base.join(safe_name).join("mtls"))
 }
 
 /// Fallback TLS directory resolution from a server URL.
 ///
-/// Used when no cluster name is set (e.g., `SshProxy` which receives a raw URL).
+/// Used when no gateway name is set (e.g., `SshProxy` which receives a raw URL).
 fn default_tls_dir(server: &str) -> Option<PathBuf> {
-    let mut name = std::env::var("OPENSHELL_CLUSTER_NAME")
+    let mut name = std::env::var("OPENSHELL_GATEWAY_NAME")
         .ok()
         .filter(|value| !value.trim().is_empty());
 
@@ -132,7 +132,7 @@ fn default_tls_dir(server: &str) -> Option<PathBuf> {
 
     let name = name.unwrap_or_else(|| "openshell".to_string());
     let safe_name = sanitize_name(&name);
-    let base = xdg_config_dir().ok()?.join("openshell").join("clusters");
+    let base = xdg_config_dir().ok()?.join("openshell").join("gateways");
     Some(base.join(safe_name).join("mtls"))
 }
 
diff --git a/crates/navigator-server/src/sandbox/mod.rs b/crates/navigator-server/src/sandbox/mod.rs
index d4a2dd424..db9dee306 100644
--- a/crates/navigator-server/src/sandbox/mod.rs
+++ b/crates/navigator-server/src/sandbox/mod.rs
@@ -28,9 +28,9 @@ const KUBE_API_TIMEOUT: Duration = Duration::from_secs(30);
 const SANDBOX_GROUP: &str = "agents.x-k8s.io";
 const SANDBOX_VERSION: &str = "v1alpha1";
 pub const SANDBOX_KIND: &str = "Sandbox";
-const SANDBOX_ID_LABEL: &str = "navigator.ai/sandbox-id";
-const SANDBOX_MANAGED_LABEL: &str = "navigator.ai/managed-by";
-const SANDBOX_MANAGED_VALUE: &str = "navigator";
+const SANDBOX_ID_LABEL: &str = "openshell.ai/sandbox-id";
+const SANDBOX_MANAGED_LABEL: &str = "openshell.ai/managed-by";
+const SANDBOX_MANAGED_VALUE: &str = "openshell";
 
 #[derive(Clone)]
 pub struct SandboxClient {
@@ -898,8 +898,8 @@ fn sandbox_template_to_k8s(
         container.insert(
             "volumeMounts".to_string(),
             serde_json::json!([{
-                "name": "navigator-client-tls",
-                "mountPath": "/etc/navigator-tls/client",
+                "name": "openshell-client-tls",
+                "mountPath": "/etc/openshell-tls/client",
                 "readOnly": true
             }]),
         );
@@ -918,7 +918,7 @@ fn sandbox_template_to_k8s(
         spec.insert(
             "volumes".to_string(),
             serde_json::json!([{
-                "name": "navigator-client-tls",
+                "name": "openshell-client-tls",
                 "secret": { "secretName": client_tls_secret_name }
             }]),
         );
@@ -968,7 +968,7 @@ fn inject_pod_template(
             .or_insert_with(|| serde_json::Value::Array(Vec::new()));
         if let Some(volumes_arr) = volumes.as_array_mut() {
             volumes_arr.push(serde_json::json!({
-                "name": "navigator-client-tls",
+                "name": "openshell-client-tls",
                 "secret": { "secretName": client_tls_secret_name }
             }));
         }
@@ -1015,8 +1015,8 @@ fn inject_pod_template(
                 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
             if let Some(mounts_arr) = mounts.as_array_mut() {
                 mounts_arr.push(serde_json::json!({
-                    "name": "navigator-client-tls",
-                    "mountPath": "/etc/navigator-tls/client",
+                    "name": "openshell-client-tls",
+                    "mountPath": "/etc/openshell-tls/client",
                     "readOnly": true
                 }));
             }
diff --git a/crates/navigator-tui/src/app.rs b/crates/navigator-tui/src/app.rs
index 044060ca2..aacf37ce6 100644
--- a/crates/navigator-tui/src/app.rs
+++ b/crates/navigator-tui/src/app.rs
@@ -33,7 +33,7 @@ pub enum InputMode {
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Focus {
     // Dashboard screen
-    Clusters,
+    Gateways,
     Providers,
     Sandboxes,
     // Sandbox screen — metadata pane is always visible (non-interactive);
@@ -84,10 +84,10 @@ impl LogSourceFilter {
 }
 
 // ---------------------------------------------------------------------------
-// Cluster entry
+// Gateway entry
 // ---------------------------------------------------------------------------
 
-pub struct ClusterEntry {
+pub struct GatewayEntry {
     pub name: String,
     pub endpoint: String,
     pub is_remote: bool,
@@ -276,16 +276,16 @@ pub struct App {
     /// When the splash screen was shown (for auto-dismiss timing).
     pub splash_start: Option<Instant>,
 
-    // Active cluster connection
-    pub cluster_name: String,
+    // Active gateway connection
+    pub gateway_name: String,
     pub endpoint: String,
     pub client: NavigatorClient<Channel>,
     pub status_text: String,
 
-    // Cluster list
-    pub clusters: Vec<ClusterEntry>,
-    pub cluster_selected: usize,
-    pub pending_cluster_switch: Option<String>,
+    // Gateway list
+    pub gateways: Vec<GatewayEntry>,
+    pub gateway_selected: usize,
+    pub pending_gateway_switch: Option<String>,
 
     // Provider list
     pub provider_names: Vec<String>,
@@ -356,21 +356,21 @@ pub struct App {
 }
 
 impl App {
-    pub fn new(client: NavigatorClient<Channel>, cluster_name: String, endpoint: String) -> Self {
+    pub fn new(client: NavigatorClient<Channel>, gateway_name: String, endpoint: String) -> Self {
         Self {
             running: true,
             screen: Screen::Splash,
             input_mode: InputMode::Normal,
-            focus: Focus::Clusters,
+            focus: Focus::Gateways,
             command_input: String::new(),
             splash_start: Some(Instant::now()),
-            cluster_name,
+            gateway_name,
             endpoint,
             client,
             status_text: String::from("connecting..."),
-            clusters: Vec::new(),
-            cluster_selected: 0,
-            pending_cluster_switch: None,
+            gateways: Vec::new(),
+            gateway_selected: 0,
+            pending_gateway_switch: None,
             provider_names: Vec::new(),
             provider_types: Vec::new(),
             provider_cred_keys: Vec::new(),
@@ -485,7 +485,7 @@ impl App {
 
     fn handle_normal_key(&mut self, key: KeyEvent) {
         match self.focus {
-            Focus::Clusters => self.handle_clusters_key(key),
+            Focus::Gateways => self.handle_gateways_key(key),
             Focus::Providers => self.handle_providers_key(key),
             Focus::Sandboxes => self.handle_sandboxes_key(key),
             Focus::SandboxPolicy => self.handle_policy_key(key),
@@ -493,7 +493,7 @@ impl App {
         }
     }
 
-    fn handle_clusters_key(&mut self, key: KeyEvent) {
+    fn handle_gateways_key(&mut self, key: KeyEvent) {
         match key.code {
             KeyCode::Char('q') => self.running = false,
             KeyCode::Tab => self.focus = Focus::Providers,
@@ -503,18 +503,18 @@ impl App {
                 self.command_input.clear();
             }
             KeyCode::Char('j') | KeyCode::Down => {
-                if !self.clusters.is_empty() {
-                    self.cluster_selected =
-                        (self.cluster_selected + 1).min(self.clusters.len() - 1);
+                if !self.gateways.is_empty() {
+                    self.gateway_selected =
+                        (self.gateway_selected + 1).min(self.gateways.len() - 1);
                 }
             }
             KeyCode::Char('k') | KeyCode::Up => {
-                self.cluster_selected = self.cluster_selected.saturating_sub(1);
+                self.gateway_selected = self.gateway_selected.saturating_sub(1);
             }
             KeyCode::Enter => {
-                if let Some(entry) = self.clusters.get(self.cluster_selected) {
-                    if entry.name != self.cluster_name {
-                        self.pending_cluster_switch = Some(entry.name.clone());
+                if let Some(entry) = self.gateways.get(self.gateway_selected) {
+                    if entry.name != self.gateway_name {
+                        self.pending_gateway_switch = Some(entry.name.clone());
                     }
                     self.focus = Focus::Providers;
                 }
@@ -541,7 +541,7 @@ impl App {
         match key.code {
             KeyCode::Char('q') => self.running = false,
             KeyCode::Tab => self.focus = Focus::Sandboxes,
-            KeyCode::BackTab => self.focus = Focus::Clusters,
+            KeyCode::BackTab => self.focus = Focus::Gateways,
             KeyCode::Char(':') => {
                 self.input_mode = InputMode::Command;
                 self.command_input.clear();
@@ -582,7 +582,7 @@ impl App {
     fn handle_sandboxes_key(&mut self, key: KeyEvent) {
         match key.code {
             KeyCode::Char('q') => self.running = false,
-            KeyCode::Tab => self.focus = Focus::Clusters,
+            KeyCode::Tab => self.focus = Focus::Gateways,
             KeyCode::BackTab => self.focus = Focus::Providers,
             KeyCode::Char(':') => {
                 self.input_mode = InputMode::Command;
@@ -1308,7 +1308,7 @@ impl App {
         }
     }
 
-    /// Reset sandbox and provider state after switching clusters.
+    /// Reset sandbox and provider state after switching gateways.
     pub fn reset_sandbox_state(&mut self) {
         self.stop_anim();
         self.cancel_log_stream();
diff --git a/crates/navigator-tui/src/lib.rs b/crates/navigator-tui/src/lib.rs
index 89a4dccc1..5181cbdb7 100644
--- a/crates/navigator-tui/src/lib.rs
+++ b/crates/navigator-tui/src/lib.rs
@@ -22,7 +22,7 @@ use ratatui::backend::CrosstermBackend;
 use tokio::sync::mpsc;
 use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
 
-use app::{App, ClusterEntry, Focus, LogLine, Screen};
+use app::{App, Focus, GatewayEntry, LogLine, Screen};
 use event::{Event, EventHandler};
 
 /// Duration to show the splash screen before auto-dismissing.
@@ -31,9 +31,9 @@ const SPLASH_DURATION: Duration = Duration::from_secs(3);
 /// Launch the OpenShell TUI.
 ///
 /// `channel` must be a connected gRPC channel to the OpenShell gateway.
-pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result<()> {
+pub async fn run(channel: Channel, gateway_name: &str, endpoint: &str) -> Result<()> {
     let client = NavigatorClient::new(channel);
-    let mut app = App::new(client, cluster_name.to_string(), endpoint.to_string());
+    let mut app = App::new(client, gateway_name.to_string(), endpoint.to_string());
 
     enable_raw_mode().into_diagnostic()?;
     let mut stdout = io::stdout();
@@ -45,7 +45,7 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result
 
     let mut events = EventHandler::new(Duration::from_secs(2));
 
-    refresh_cluster_list(&mut app);
+    refresh_gateway_list(&mut app);
     refresh_data(&mut app).await;
 
     while app.running {
@@ -57,8 +57,8 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result
             Some(Event::Key(key)) => {
                 app.handle_key(key);
                 // Handle async actions triggered by key presses.
-                if app.pending_cluster_switch.is_some() {
-                    handle_cluster_switch(&mut app).await;
+                if app.pending_gateway_switch.is_some() {
+                    handle_gateway_switch(&mut app).await;
                 }
                 if app.pending_log_fetch {
                     app.pending_log_fetch = false;
@@ -199,7 +199,7 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result
                     }
                 }
 
-                refresh_cluster_list(&mut app);
+                refresh_gateway_list(&mut app);
                 refresh_data(&mut app).await;
 
                 // Auto-refresh the policy view when a new version is detected.
@@ -327,15 +327,15 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result
 }
 
 // ---------------------------------------------------------------------------
-// Cluster discovery and switching
+// Gateway discovery and switching
 // ---------------------------------------------------------------------------
 
-/// Refresh the list of known clusters from disk.
-fn refresh_cluster_list(app: &mut App) {
-    if let Ok(clusters) = navigator_bootstrap::list_clusters() {
-        app.clusters = clusters
+/// Refresh the list of known gateways from disk.
+fn refresh_gateway_list(app: &mut App) {
+    if let Ok(gateways) = navigator_bootstrap::list_gateways() {
+        app.gateways = gateways
             .into_iter()
-            .map(|m| ClusterEntry {
+            .map(|m| GatewayEntry {
                 name: m.name,
                 endpoint: m.gateway_endpoint,
                 is_remote: m.is_remote,
@@ -343,39 +343,39 @@ fn refresh_cluster_list(app: &mut App) {
             .collect();
 
         // Keep selection in bounds.
-        if app.cluster_selected >= app.clusters.len() && !app.clusters.is_empty() {
-            app.cluster_selected = app.clusters.len() - 1;
+        if app.gateway_selected >= app.gateways.len() && !app.gateways.is_empty() {
+            app.gateway_selected = app.gateways.len() - 1;
         }
 
-        // If the active cluster appears in the list, move cursor to it on first load.
-        if let Some(idx) = app.clusters.iter().position(|c| c.name == app.cluster_name) {
+        // If the active gateway appears in the list, move cursor to it on first load.
+        if let Some(idx) = app.gateways.iter().position(|g| g.name == app.gateway_name) {
             // Only snap the cursor when it's still at 0 (initial state).
-            if app.cluster_selected == 0 {
-                app.cluster_selected = idx;
+            if app.gateway_selected == 0 {
+                app.gateway_selected = idx;
             }
         }
     }
 }
 
-/// Handle a pending cluster switch requested by the user.
-async fn handle_cluster_switch(app: &mut App) {
-    let Some(name) = app.pending_cluster_switch.take() else {
+/// Handle a pending gateway switch requested by the user.
+async fn handle_gateway_switch(app: &mut App) {
+    let Some(name) = app.pending_gateway_switch.take() else {
         return;
     };
 
-    // Look up the endpoint from the cluster list.
-    let endpoint = match app.clusters.iter().find(|c| c.name == name) {
-        Some(c) => c.endpoint.clone(),
+    // Look up the endpoint from the gateway list.
+    let endpoint = match app.gateways.iter().find(|g| g.name == name) {
+        Some(g) => g.endpoint.clone(),
         None => return,
     };
 
-    match connect_to_cluster(&name, &endpoint).await {
+    match connect_to_gateway(&name, &endpoint).await {
         Ok(channel) => {
             app.client = NavigatorClient::new(channel);
-            app.cluster_name = name;
+            app.gateway_name = name;
             app.endpoint = endpoint;
             app.reset_sandbox_state();
-            // Immediately refresh data for the new cluster.
+            // Immediately refresh data for the new gateway.
             refresh_data(app).await;
         }
         Err(e) => {
@@ -384,20 +384,20 @@ async fn handle_cluster_switch(app: &mut App) {
     }
 }
 
-/// Build a gRPC channel to a cluster using its mTLS certs on disk.
-async fn connect_to_cluster(name: &str, endpoint: &str) -> Result<Channel> {
-    let mtls_dir = cluster_mtls_dir(name)
-        .ok_or_else(|| miette::miette!("cannot determine config directory for cluster {name}"))?;
+/// Build a gRPC channel to a gateway using its mTLS certs on disk.
+async fn connect_to_gateway(name: &str, endpoint: &str) -> Result<Channel> {
+    let mtls_dir = gateway_mtls_dir(name)
+        .ok_or_else(|| miette::miette!("cannot determine config directory for gateway {name}"))?;
 
     let ca = std::fs::read(mtls_dir.join("ca.crt"))
         .into_diagnostic()
-        .map_err(|_| miette::miette!("missing CA cert for cluster {name}"))?;
+        .map_err(|_| miette::miette!("missing CA cert for gateway {name}"))?;
     let cert = std::fs::read(mtls_dir.join("tls.crt"))
         .into_diagnostic()
-        .map_err(|_| miette::miette!("missing client cert for cluster {name}"))?;
+        .map_err(|_| miette::miette!("missing client cert for gateway {name}"))?;
     let key = std::fs::read(mtls_dir.join("tls.key"))
         .into_diagnostic()
-        .map_err(|_| miette::miette!("missing client key for cluster {name}"))?;
+        .map_err(|_| miette::miette!("missing client key for gateway {name}"))?;
 
     let tls_config = ClientTlsConfig::new()
         .ca_certificate(Certificate::from_pem(ca))
@@ -417,8 +417,8 @@ async fn connect_to_cluster(name: &str, endpoint: &str) -> Result<Channel> {
     Ok(channel)
 }
 
-/// Resolve the mTLS cert directory for a cluster.
-fn cluster_mtls_dir(name: &str) -> Option<PathBuf> {
+/// Resolve the mTLS cert directory for a gateway.
+fn gateway_mtls_dir(name: &str) -> Option<PathBuf> {
     let config_dir = std::env::var("XDG_CONFIG_HOME")
         .map(PathBuf::from)
         .or_else(|_| std::env::var("HOME").map(|h| PathBuf::from(h).join(".config")))
@@ -426,7 +426,7 @@ fn cluster_mtls_dir(name: &str) -> Option<PathBuf> {
     Some(
         config_dir
             .join("openshell")
-            .join("clusters")
+            .join("gateways")
             .join(name)
             .join("mtls"),
     )
@@ -733,9 +733,9 @@ async fn handle_shell_connect(
         }
     };
     let exe_str = shell_escape(&exe.to_string_lossy());
-    let cluster = shell_escape(&app.cluster_name);
+    let gateway = shell_escape(&app.gateway_name);
     let proxy_command = format!(
-        "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway {cluster}",
+        "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}",
         session.sandbox_id, session.token,
     );
     // Step 5: Build the SSH command.
@@ -875,9 +875,9 @@ async fn handle_exec_command(
         }
     };
     let exe_str = shell_escape(&exe.to_string_lossy());
-    let cluster = shell_escape(&app.cluster_name);
+    let gateway = shell_escape(&app.gateway_name);
     let proxy_command = format!(
-        "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway {cluster}",
+        "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}",
         session.sandbox_id, session.token,
     );
 
@@ -1149,7 +1149,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
     app.pending_forward_ports = ports.clone();
 
     let endpoint = app.endpoint.clone();
-    let cluster_name = app.cluster_name.clone();
+    let gateway_name = app.gateway_name.clone();
     let need_ready = !ports.is_empty() || !app.pending_exec_command.is_empty();
 
     tokio::spawn(async move {
@@ -1239,7 +1239,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
                 start_port_forwards(
                     &mut client,
                     &endpoint,
-                    &cluster_name,
+                    &gateway_name,
                     &sandbox_name,
                     &sandbox_id,
                     &ports,
@@ -1259,7 +1259,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
 async fn start_port_forwards(
     client: &mut NavigatorClient<Channel>,
     endpoint: &str,
-    cluster_name: &str,
+    gateway_name: &str,
     sandbox_name: &str,
     sandbox_id: &str,
     ports: &[u16],
@@ -1301,9 +1301,9 @@ async fn start_port_forwards(
         }
     };
     let exe_str = shell_escape(&exe.to_string_lossy());
-    let cluster = shell_escape(cluster_name);
+    let gateway = shell_escape(gateway_name);
     let proxy_command = format!(
-        "{exe_str} ssh-proxy --gateway {gateway_url} --sandbox-id {} --token {} --cluster {cluster}",
+        "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}",
         session.sandbox_id, session.token,
     );
 
diff --git a/crates/navigator-tui/src/ui/dashboard.rs b/crates/navigator-tui/src/ui/dashboard.rs
index d310ef533..0426c2f36 100644
--- a/crates/navigator-tui/src/ui/dashboard.rs
+++ b/crates/navigator-tui/src/ui/dashboard.rs
@@ -19,13 +19,13 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) {
         ])
         .split(area);
 
-    draw_cluster_list(frame, app, chunks[0]);
+    draw_gateway_list(frame, app, chunks[0]);
     super::providers::draw(frame, app, chunks[1], app.focus == Focus::Providers);
     super::sandboxes::draw(frame, app, chunks[2], app.focus == Focus::Sandboxes);
 }
 
-fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
-    let focused = app.focus == Focus::Clusters;
+fn draw_gateway_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
+    let focused = app.focus == Focus::Gateways;
 
     let header = Row::new(vec![
         Cell::from(Span::styled("  NAME", styles::MUTED)),
@@ -35,12 +35,12 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
     .bottom_margin(1);
 
     let rows: Vec<Row<'_>> = app
-        .clusters
+        .gateways
         .iter()
         .enumerate()
         .map(|(i, entry)| {
-            let is_active = entry.name == app.cluster_name;
-            let is_cursor = focused && i == app.cluster_selected;
+            let is_active = entry.name == app.gateway_name;
+            let is_cursor = focused && i == app.gateway_selected;
 
             let cursor = if is_cursor { ">" } else { " " };
             let dot = if is_active { "* " } else { "  " };
@@ -92,7 +92,7 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
     };
 
     let block = Block::default()
-        .title(Span::styled(" Clusters ", styles::HEADING))
+        .title(Span::styled(" Gateways ", styles::HEADING))
         .borders(Borders::ALL)
         .border_style(border_style)
         .padding(Padding::horizontal(1));
@@ -107,14 +107,14 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
 
     frame.render_widget(table, area);
 
-    if app.clusters.is_empty() {
+    if app.gateways.is_empty() {
         let inner = Rect {
             x: area.x + 2,
             y: area.y + 2,
             width: area.width.saturating_sub(4),
             height: area.height.saturating_sub(3),
         };
-        let msg = Paragraph::new(Span::styled(" No clusters found.", styles::MUTED));
+        let msg = Paragraph::new(Span::styled(" No gateways found.", styles::MUTED));
         frame.render_widget(msg, inner);
     }
 }
diff --git a/crates/navigator-tui/src/ui/mod.rs b/crates/navigator-tui/src/ui/mod.rs
index 8bbb63b12..640490ba0 100644
--- a/crates/navigator-tui/src/ui/mod.rs
+++ b/crates/navigator-tui/src/ui/mod.rs
@@ -115,8 +115,8 @@ fn draw_title_bar(frame: &mut Frame<'_>, app: &App, area: Rect) {
                 .add_modifier(ratatui::style::Modifier::BOLD),
         ),
         Span::styled(" | ", styles::MUTED),
-        Span::styled("Current Cluster: ", styles::TEXT),
-        Span::styled(&app.cluster_name, styles::HEADING),
+        Span::styled("Current Gateway: ", styles::TEXT),
+        Span::styled(&app.gateway_name, styles::HEADING),
         Span::styled(" (", styles::MUTED),
         status_span,
         Span::styled(")", styles::MUTED),
diff --git a/crates/navigator-tui/src/ui/providers.rs b/crates/navigator-tui/src/ui/providers.rs
index a453da788..5e77579d2 100644
--- a/crates/navigator-tui/src/ui/providers.rs
+++ b/crates/navigator-tui/src/ui/providers.rs
@@ -71,7 +71,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) {
         Line::from(vec![
             Span::styled(" Providers ", styles::HEADING),
             Span::styled("- ", styles::BORDER),
-            Span::styled(&app.cluster_name, styles::MUTED),
+            Span::styled(&app.gateway_name, styles::MUTED),
             Span::styled(" ", styles::MUTED),
         ])
     };
diff --git a/crates/navigator-tui/src/ui/sandboxes.rs b/crates/navigator-tui/src/ui/sandboxes.rs
index e38f47552..661b95723 100644
--- a/crates/navigator-tui/src/ui/sandboxes.rs
+++ b/crates/navigator-tui/src/ui/sandboxes.rs
@@ -78,7 +78,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) {
     let title = Line::from(vec![
         Span::styled(" Sandboxes ", styles::HEADING),
         Span::styled("─ ", styles::BORDER),
-        Span::styled(&app.cluster_name, styles::MUTED),
+        Span::styled(&app.gateway_name, styles::MUTED),
         Span::styled(" ", styles::MUTED),
     ]);
 
diff --git a/deploy/docker/Dockerfile.cluster b/deploy/docker/Dockerfile.cluster
index 3cb288a9c..2fd5cfb93 100644
--- a/deploy/docker/Dockerfile.cluster
+++ b/deploy/docker/Dockerfile.cluster
@@ -8,7 +8,7 @@
 # - HelmChart CR for auto-deploying OpenShell
 # - Custom entrypoint for DNS configuration in Docker environments
 #
-# Component images (navigator/server, navigator/sandbox) are
+# Component images (openshell/server, openshell/sandbox) are
 # pulled at runtime from the distribution registry rather than bundled as tarballs.
 # Registry credentials are generated by the entrypoint script at container start.
 #
@@ -30,8 +30,8 @@ FROM rancher/k3s:${K3S_VERSION}
 RUN mkdir -p /var/lib/rancher/k3s/server/manifests \
              /var/lib/rancher/k3s/server/static/charts \
              /etc/rancher/k3s \
-             /opt/navigator/manifests \
-             /opt/navigator/charts
+             /opt/openshell/manifests \
+             /opt/openshell/charts
 
 # Copy entrypoint script that configures DNS for Docker environments
 # This script detects the host gateway IP and configures CoreDNS to use it
@@ -48,11 +48,11 @@ RUN chmod +x /usr/local/bin/cluster-healthcheck.sh
 # Copy packaged helm charts to a staging directory that won't be
 # overwritten by the /var/lib/rancher/k3s volume mount. The entrypoint
 # script copies them into the k3s static charts directory at container start.
-COPY deploy/docker/.build/charts/*.tgz /opt/navigator/charts/
+COPY deploy/docker/.build/charts/*.tgz /opt/openshell/charts/
 
 # Copy Kubernetes manifests to a persistent location that won't be overwritten by the volume mount.
 # The bootstrap code will copy these to /var/lib/rancher/k3s/server/manifests/ after cluster start.
-COPY deploy/kube/manifests/*.yaml /opt/navigator/manifests/
+COPY deploy/kube/manifests/*.yaml /opt/openshell/manifests/
 
 # Use custom entrypoint that configures DNS before starting k3s
 ENTRYPOINT ["/usr/local/bin/cluster-entrypoint.sh"]
diff --git a/deploy/docker/cluster-entrypoint.sh b/deploy/docker/cluster-entrypoint.sh
index 012456189..33f6d348f 100644
--- a/deploy/docker/cluster-entrypoint.sh
+++ b/deploy/docker/cluster-entrypoint.sh
@@ -27,6 +27,36 @@ set -e
 
 RESOLV_CONF="/etc/rancher/k3s/resolv.conf"
 
+has_default_route() {
+    ip -4 route show default 2>/dev/null | grep -q '^default ' \
+        || ip -6 route show default 2>/dev/null | grep -q '^default '
+}
+
+wait_for_default_route() {
+    attempts=${1:-30}
+    delay_s=${2:-1}
+    i=1
+
+    while [ "$i" -le "$attempts" ]; do
+        if has_default_route; then
+            return 0
+        fi
+        sleep "$delay_s"
+        i=$((i + 1))
+    done
+
+    echo "Error: no default route present before starting k3s"
+    echo "IPv4 routes:"
+    ip -4 route show 2>/dev/null || true
+    echo "IPv6 routes:"
+    ip -6 route show 2>/dev/null || true
+    echo "/proc/net/route:"
+    cat /proc/net/route 2>/dev/null || true
+    echo "/proc/net/ipv6_route:"
+    cat /proc/net/ipv6_route 2>/dev/null || true
+    return 1
+}
+
 # ---------------------------------------------------------------------------
 # Configure DNS proxy via iptables
 # ---------------------------------------------------------------------------
@@ -165,12 +195,12 @@ else
 fi
 
 # Copy bundled Helm chart tarballs to the k3s static charts directory.
-# These are stored in /opt/navigator/charts/ because the volume mount
+# These are stored in /opt/openshell/charts/ because the volume mount
 # on /var/lib/rancher/k3s overwrites any files baked into that path.
 # Without this, a persistent volume from a previous deploy would keep
 # serving stale chart tarballs.
 K3S_CHARTS="/var/lib/rancher/k3s/server/static/charts"
-BUNDLED_CHARTS="/opt/navigator/charts"
+BUNDLED_CHARTS="/opt/openshell/charts"
 CHART_CHECKSUM=""
 
 if [ -d "$BUNDLED_CHARTS" ]; then
@@ -179,30 +209,30 @@ if [ -d "$BUNDLED_CHARTS" ]; then
         [ ! -f "$chart" ] && continue
         cp "$chart" "$K3S_CHARTS/"
     done
-    # Compute a checksum of the navigator chart so we can inject it into the
+    # Compute a checksum of the openshell chart so we can inject it into the
     # HelmChart manifest below. When the chart content changes between image
     # versions the checksum changes, which modifies the HelmChart CR spec and
     # forces the k3s Helm controller to re-install.
-    NAV_CHART="$BUNDLED_CHARTS/navigator-0.1.0.tgz"
-    if [ -f "$NAV_CHART" ]; then
+    OPENSHELL_CHART="$BUNDLED_CHARTS/openshell-0.1.0.tgz"
+    if [ -f "$OPENSHELL_CHART" ]; then
         if command -v sha256sum >/dev/null 2>&1; then
-            CHART_CHECKSUM=$(sha256sum "$NAV_CHART" | cut -d ' ' -f 1)
+            CHART_CHECKSUM=$(sha256sum "$OPENSHELL_CHART" | cut -d ' ' -f 1)
         elif command -v shasum >/dev/null 2>&1; then
-            CHART_CHECKSUM=$(shasum -a 256 "$NAV_CHART" | cut -d ' ' -f 1)
+            CHART_CHECKSUM=$(shasum -a 256 "$OPENSHELL_CHART" | cut -d ' ' -f 1)
         fi
     fi
 fi
 
 # Copy bundled manifests to k3s manifests directory.
-# These are stored in /opt/navigator/manifests/ because the volume mount
+# These are stored in /opt/openshell/manifests/ because the volume mount
 # on /var/lib/rancher/k3s overwrites any files baked into that path.
 #
 # When reusing a persistent volume from a previous deploy, stale manifests
 # (e.g. envoy-gateway-helmchart.yaml from an older image) may linger.
-# We remove any navigator-managed manifests that no longer exist in the
+# We remove any openshell-managed manifests that no longer exist in the
 # bundled set so k3s does not keep installing removed components.
 K3S_MANIFESTS="/var/lib/rancher/k3s/server/manifests"
-BUNDLED_MANIFESTS="/opt/navigator/manifests"
+BUNDLED_MANIFESTS="/opt/openshell/manifests"
 
 if [ -d "$BUNDLED_MANIFESTS" ]; then
     echo "Copying bundled manifests to k3s..."
@@ -211,10 +241,11 @@ if [ -d "$BUNDLED_MANIFESTS" ]; then
         cp "$manifest" "$K3S_MANIFESTS/"
     done
 
-    # Remove navigator-managed manifests that are no longer bundled.
-    # Only clean up files that look like navigator manifests (navigator-* or
+    # Remove openshell-managed manifests that are no longer bundled.
+    # Only clean up files that look like openshell manifests (openshell-* or
     # envoy-gateway-* or agent-*) to avoid removing built-in k3s manifests.
-    for existing in "$K3S_MANIFESTS"/navigator-*.yaml \
+    for existing in "$K3S_MANIFESTS"/openshell-*.yaml \
+                    "$K3S_MANIFESTS"/navigator-*.yaml \
                     "$K3S_MANIFESTS"/envoy-gateway-*.yaml \
                     "$K3S_MANIFESTS"/agent-*.yaml; do
         [ ! -f "$existing" ] && continue
@@ -233,7 +264,7 @@ fi
 # images in the HelmChart manifest so k3s deploys the locally-pushed versions.
 # When IMAGE_PULL_POLICY is set, override the default "Always" so k3s uses
 # images already present in containerd instead of pulling from the registry.
-HELMCHART="/var/lib/rancher/k3s/server/manifests/navigator-helmchart.yaml"
+HELMCHART="/var/lib/rancher/k3s/server/manifests/openshell-helmchart.yaml"
 
 if [ -n "${IMAGE_REPO_BASE:-}" ] && [ -f "$HELMCHART" ]; then
     target_tag="${IMAGE_TAG:-latest}"
@@ -343,5 +374,10 @@ else
     sed -i '/__CHART_CHECKSUM__/d' "$HELMCHART"
 fi
 
+# Docker Desktop can briefly start the container before its bridge default route
+# is fully installed. k3s exits immediately in that state, so wait briefly for
+# routing to settle first.
+wait_for_default_route
+
 # Execute k3s with explicit resolv-conf.
 exec /bin/k3s "$@" --resolv-conf="$RESOLV_CONF"
diff --git a/deploy/docker/cluster-healthcheck.sh b/deploy/docker/cluster-healthcheck.sh
index e253626b8..829f2ccb1 100644
--- a/deploy/docker/cluster-healthcheck.sh
+++ b/deploy/docker/cluster-healthcheck.sh
@@ -9,12 +9,12 @@ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
 
 kubectl get --raw='/readyz' >/dev/null 2>&1 || exit 1
 
-kubectl -n navigator get statefulset/navigator >/dev/null 2>&1 || exit 1
-kubectl -n navigator wait --for=jsonpath='{.status.readyReplicas}'=1 statefulset/navigator --timeout=1s >/dev/null 2>&1 || exit 1
+kubectl -n openshell get statefulset/openshell >/dev/null 2>&1 || exit 1
+kubectl -n openshell wait --for=jsonpath='{.status.readyReplicas}'=1 statefulset/openshell --timeout=1s >/dev/null 2>&1 || exit 1
 
-# Verify TLS secrets exist (created by navigator-bootstrap before the StatefulSet starts)
+# Verify TLS secrets exist (created by openshell-bootstrap before the StatefulSet starts)
 # Skip when TLS is disabled — secrets are not required.
 if [ "${DISABLE_TLS:-}" != "true" ]; then
-    kubectl -n navigator get secret navigator-server-tls >/dev/null 2>&1 || exit 1
-    kubectl -n navigator get secret navigator-client-tls >/dev/null 2>&1 || exit 1
+    kubectl -n openshell get secret openshell-server-tls >/dev/null 2>&1 || exit 1
+    kubectl -n openshell get secret openshell-client-tls >/dev/null 2>&1 || exit 1
 fi
diff --git a/deploy/docker/sandbox/Dockerfile.nvidia b/deploy/docker/sandbox/Dockerfile.nvidia
index 417e7c548..7d8a42be8 100644
--- a/deploy/docker/sandbox/Dockerfile.nvidia
+++ b/deploy/docker/sandbox/Dockerfile.nvidia
@@ -10,7 +10,7 @@
 #
 # TODO: Add NVIDIA-specific layers (CUDA toolkit, cuDNN, GPU drivers, etc.)
 
-ARG BASE_IMAGE=navigator/sandbox:dev
+ARG BASE_IMAGE=openshell/sandbox:dev
 FROM ${BASE_IMAGE}
 
 # Placeholder: add NVIDIA GPU packages and configuration here.
diff --git a/deploy/helm/navigator/templates/rolebinding.yaml b/deploy/helm/navigator/templates/rolebinding.yaml
deleted file mode 100644
index 201549cbe..000000000
--- a/deploy/helm/navigator/templates/rolebinding.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: {{ include "navigator.fullname" . }}-sandbox
-  labels:
-    {{- include "navigator.labels" . | nindent 4 }}
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: {{ include "navigator.fullname" . }}-sandbox
-subjects:
-  - kind: ServiceAccount
-    name: {{ include "navigator.serviceAccountName" . }}
-    namespace: {{ .Release.Namespace }}
diff --git a/deploy/helm/navigator/templates/serviceaccount.yaml b/deploy/helm/navigator/templates/serviceaccount.yaml
deleted file mode 100644
index 9731c6d3b..000000000
--- a/deploy/helm/navigator/templates/serviceaccount.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-{{- if .Values.serviceAccount.create -}}
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: {{ include "navigator.serviceAccountName" . }}
-  labels:
-    {{- include "navigator.labels" . | nindent 4 }}
-  {{- with .Values.serviceAccount.annotations }}
-  annotations:
-    {{- toYaml . | nindent 4 }}
-  {{- end }}
-{{- end }}
diff --git a/deploy/helm/navigator/.helmignore b/deploy/helm/openshell/.helmignore
similarity index 100%
rename from deploy/helm/navigator/.helmignore
rename to deploy/helm/openshell/.helmignore
diff --git a/deploy/helm/navigator/Chart.yaml b/deploy/helm/openshell/Chart.yaml
similarity index 94%
rename from deploy/helm/navigator/Chart.yaml
rename to deploy/helm/openshell/Chart.yaml
index b3e4e8ce2..f97e316cf 100644
--- a/deploy/helm/navigator/Chart.yaml
+++ b/deploy/helm/openshell/Chart.yaml
@@ -2,7 +2,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 apiVersion: v2
-name: navigator
+name: openshell
 description: runtime environment for autonomous agents
 type: application
 version: 0.1.0
diff --git a/deploy/helm/navigator/templates/_helpers.tpl b/deploy/helm/openshell/templates/_helpers.tpl
similarity index 73%
rename from deploy/helm/navigator/templates/_helpers.tpl
rename to deploy/helm/openshell/templates/_helpers.tpl
index a2b3eaa42..10fd3de0d 100644
--- a/deploy/helm/navigator/templates/_helpers.tpl
+++ b/deploy/helm/openshell/templates/_helpers.tpl
@@ -1,14 +1,14 @@
 {{/*
 Expand the name of the chart.
 */}}
-{{- define "navigator.name" -}}
+{{- define "openshell.name" -}}
 {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
 {{- end }}
 
 {{/*
 Create a default fully qualified app name.
 */}}
-{{- define "navigator.fullname" -}}
+{{- define "openshell.fullname" -}}
 {{- if .Values.fullnameOverride }}
 {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
 {{- else }}
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
 {{/*
 Create chart name and version as used by the chart label.
 */}}
-{{- define "navigator.chart" -}}
+{{- define "openshell.chart" -}}
 {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
 {{- end }}
 
 {{/*
 Common labels
 */}}
-{{- define "navigator.labels" -}}
-helm.sh/chart: {{ include "navigator.chart" . }}
-{{ include "navigator.selectorLabels" . }}
+{{- define "openshell.labels" -}}
+helm.sh/chart: {{ include "openshell.chart" . }}
+{{ include "openshell.selectorLabels" . }}
 {{- if .Chart.AppVersion }}
 app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
 {{- end }}
@@ -43,17 +43,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
 {{/*
 Selector labels
 */}}
-{{- define "navigator.selectorLabels" -}}
-app.kubernetes.io/name: {{ include "navigator.name" . }}
+{{- define "openshell.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "openshell.name" . }}
 app.kubernetes.io/instance: {{ .Release.Name }}
 {{- end }}
 
 {{/*
 Create the name of the service account to use
 */}}
-{{- define "navigator.serviceAccountName" -}}
+{{- define "openshell.serviceAccountName" -}}
 {{- if .Values.serviceAccount.create }}
-{{- default (include "navigator.fullname" .) .Values.serviceAccount.name }}
+{{- default (include "openshell.fullname" .) .Values.serviceAccount.name }}
 {{- else }}
 {{- default "default" .Values.serviceAccount.name }}
 {{- end }}
diff --git a/deploy/helm/navigator/templates/networkpolicy.yaml b/deploy/helm/openshell/templates/networkpolicy.yaml
similarity index 61%
rename from deploy/helm/navigator/templates/networkpolicy.yaml
rename to deploy/helm/openshell/templates/networkpolicy.yaml
index aea332dae..3e0b6f504 100644
--- a/deploy/helm/navigator/templates/networkpolicy.yaml
+++ b/deploy/helm/openshell/templates/networkpolicy.yaml
@@ -1,20 +1,23 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
 {{- if .Values.networkPolicy.enabled }}
 # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway pod.
 # Sandbox pods are dynamically created by the server and labelled with
-# navigator.ai/managed-by=navigator. This policy ensures only the gateway
-# (navigator server) pod can reach the sandbox SSH port (2222), blocking
+# openshell.ai/managed-by=openshell. This policy ensures only the gateway
+# (openshell server) pod can reach the sandbox SSH port (2222), blocking
 # lateral movement from other in-cluster workloads.
 apiVersion: networking.k8s.io/v1
 kind: NetworkPolicy
 metadata:
-  name: {{ include "navigator.fullname" . }}-sandbox-ssh
+  name: {{ include "openshell.fullname" . }}-sandbox-ssh
   namespace: {{ .Values.server.sandboxNamespace }}
   labels:
-    {{- include "navigator.labels" . | nindent 4 }}
+    {{- include "openshell.labels" . | nindent 4 }}
 spec:
   podSelector:
     matchLabels:
-      navigator.ai/managed-by: navigator
+      openshell.ai/managed-by: openshell
   policyTypes:
     - Ingress
   ingress:
@@ -24,7 +27,7 @@ spec:
               kubernetes.io/metadata.name: {{ .Release.Namespace }}
           podSelector:
             matchLabels:
-              app.kubernetes.io/name: {{ include "navigator.name" . }}
+              app.kubernetes.io/name: {{ include "openshell.name" . }}
               app.kubernetes.io/instance: {{ .Release.Name }}
       ports:
         - protocol: TCP
diff --git a/deploy/helm/navigator/templates/role.yaml b/deploy/helm/openshell/templates/role.yaml
similarity index 61%
rename from deploy/helm/navigator/templates/role.yaml
rename to deploy/helm/openshell/templates/role.yaml
index 489afbf48..1d756117c 100644
--- a/deploy/helm/navigator/templates/role.yaml
+++ b/deploy/helm/openshell/templates/role.yaml
@@ -1,9 +1,12 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  name: {{ include "navigator.fullname" . }}-sandbox
+  name: {{ include "openshell.fullname" . }}-sandbox
   labels:
-    {{- include "navigator.labels" . | nindent 4 }}
+    {{- include "openshell.labels" . | nindent 4 }}
 rules:
   - apiGroups:
       - agents.x-k8s.io
diff --git a/deploy/helm/openshell/templates/rolebinding.yaml b/deploy/helm/openshell/templates/rolebinding.yaml
new file mode 100644
index 000000000..2bb3c7d08
--- /dev/null
+++ b/deploy/helm/openshell/templates/rolebinding.yaml
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ include "openshell.fullname" . }}-sandbox
+  labels:
+    {{- include "openshell.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "openshell.fullname" . }}-sandbox
+subjects:
+  - kind: ServiceAccount
+    name: {{ include "openshell.serviceAccountName" . }}
+    namespace: {{ .Release.Namespace }}
diff --git a/deploy/helm/navigator/templates/service.yaml b/deploy/helm/openshell/templates/service.yaml
similarity index 55%
rename from deploy/helm/navigator/templates/service.yaml
rename to deploy/helm/openshell/templates/service.yaml
index 2086d56a9..0e4aa4b76 100644
--- a/deploy/helm/navigator/templates/service.yaml
+++ b/deploy/helm/openshell/templates/service.yaml
@@ -1,9 +1,12 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
 apiVersion: v1
 kind: Service
 metadata:
-  name: {{ include "navigator.fullname" . }}
+  name: {{ include "openshell.fullname" . }}
   labels:
-    {{- include "navigator.labels" . | nindent 4 }}
+    {{- include "openshell.labels" . | nindent 4 }}
 spec:
   type: {{ .Values.service.type }}
   ports:
@@ -16,4 +19,4 @@ spec:
       nodePort: {{ .Values.service.nodePort }}
       {{- end }}
   selector:
-    {{- include "navigator.selectorLabels" . | nindent 4 }}
+    {{- include "openshell.selectorLabels" . | nindent 4 }}
diff --git a/deploy/helm/openshell/templates/serviceaccount.yaml b/deploy/helm/openshell/templates/serviceaccount.yaml
new file mode 100644
index 000000000..1f03f8e94
--- /dev/null
+++ b/deploy/helm/openshell/templates/serviceaccount.yaml
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+{{- if .Values.serviceAccount.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "openshell.serviceAccountName" . }}
+  labels:
+    {{- include "openshell.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
diff --git a/deploy/helm/navigator/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml
similarity index 84%
rename from deploy/helm/navigator/templates/statefulset.yaml
rename to deploy/helm/openshell/templates/statefulset.yaml
index d086315e9..ce79e3c82 100644
--- a/deploy/helm/navigator/templates/statefulset.yaml
+++ b/deploy/helm/openshell/templates/statefulset.yaml
@@ -1,15 +1,18 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
 apiVersion: apps/v1
 kind: StatefulSet
 metadata:
-  name: {{ include "navigator.fullname" . }}
+  name: {{ include "openshell.fullname" . }}
   labels:
-    {{- include "navigator.labels" . | nindent 4 }}
+    {{- include "openshell.labels" . | nindent 4 }}
 spec:
-  serviceName: {{ include "navigator.fullname" . }}
+  serviceName: {{ include "openshell.fullname" . }}
   replicas: {{ .Values.replicaCount }}
   selector:
     matchLabels:
-      {{- include "navigator.selectorLabels" . | nindent 6 }}
+      {{- include "openshell.selectorLabels" . | nindent 6 }}
   template:
     metadata:
       {{- with .Values.podAnnotations }}
@@ -17,7 +20,7 @@ spec:
         {{- toYaml . | nindent 8 }}
       {{- end }}
       labels:
-        {{- include "navigator.labels" . | nindent 8 }}
+        {{- include "openshell.labels" . | nindent 8 }}
         {{- with .Values.podLabels }}
         {{- toYaml . | nindent 8 }}
         {{- end }}
@@ -27,7 +30,7 @@ spec:
       imagePullSecrets:
         {{- toYaml . | nindent 8 }}
       {{- end }}
-      serviceAccountName: {{ include "navigator.serviceAccountName" . }}
+      serviceAccountName: {{ include "openshell.serviceAccountName" . }}
       securityContext:
         {{- toYaml .Values.podSecurityContext | nindent 8 }}
       containers:
@@ -65,11 +68,11 @@ spec:
               value: "true"
             {{- else }}
             - name: OPENSHELL_TLS_CERT
-              value: /etc/navigator-tls/server/tls.crt
+              value: /etc/openshell-tls/server/tls.crt
             - name: OPENSHELL_TLS_KEY
-              value: /etc/navigator-tls/server/tls.key
+              value: /etc/openshell-tls/server/tls.key
             - name: OPENSHELL_TLS_CLIENT_CA
-              value: /etc/navigator-tls/client-ca/ca.crt
+              value: /etc/openshell-tls/client-ca/ca.crt
             - name: OPENSHELL_CLIENT_TLS_SECRET_NAME
               value: {{ .Values.server.tls.clientTlsSecretName | quote }}
             {{- if .Values.server.disableGatewayAuth }}
@@ -78,14 +81,14 @@ spec:
             {{- end }}
             {{- end }}
           volumeMounts:
-            - name: navigator-data
-              mountPath: /var/navigator
+            - name: openshell-data
+              mountPath: /var/openshell
             {{- if not .Values.server.disableTls }}
             - name: tls-cert
-              mountPath: /etc/navigator-tls/server
+              mountPath: /etc/openshell-tls/server
               readOnly: true
             - name: tls-client-ca
-              mountPath: /etc/navigator-tls/client-ca
+              mountPath: /etc/openshell-tls/client-ca
               readOnly: true
             {{- end }}
           ports:
@@ -131,7 +134,7 @@ spec:
       {{- end }}
   volumeClaimTemplates:
     - metadata:
-        name: navigator-data
+        name: openshell-data
       spec:
         accessModes: ["ReadWriteOnce"]
         resources:
diff --git a/deploy/helm/navigator/values.yaml b/deploy/helm/openshell/values.yaml
similarity index 86%
rename from deploy/helm/navigator/values.yaml
rename to deploy/helm/openshell/values.yaml
index 3553385b1..d2a58b677 100644
--- a/deploy/helm/navigator/values.yaml
+++ b/deploy/helm/openshell/values.yaml
@@ -6,7 +6,7 @@
 replicaCount: 1
 
 image:
-  repository: ghcr.io/nvidia/nemoclaw/server
+  repository: ghcr.io/nvidia/openshell/server
   pullPolicy: Always
   tag: "latest"
 
@@ -65,11 +65,11 @@ affinity: {}
 # Server configuration
 server:
   logLevel: info
-  sandboxNamespace: navigator
-  dbUrl: "sqlite:/var/navigator/navigator.db"
-  sandboxImage: "d1i0nduu2f6qxk.cloudfront.net/navigator/sandbox:latest"
+  sandboxNamespace: openshell
+  dbUrl: "sqlite:/var/openshell/openshell.db"
+  sandboxImage: "ghcr.io/nvidia/openshell/sandbox:latest"
   # gRPC endpoint for sandboxes to callback to OpenShell (must be reachable from pods)
-  grpcEndpoint: "https://navigator.navigator.svc.cluster.local:8080"
+  grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080"
   # Public host/port returned to CLI clients for SSH proxy CONNECT requests.
   # For local clusters the default 127.0.0.1:8080 is correct; for remote
   # clusters these should be set to the externally reachable host and port.
@@ -90,11 +90,11 @@ server:
   disableTls: false
   tls:
     # K8s secret (type kubernetes.io/tls) with tls.crt and tls.key for the server
-    certSecretName: navigator-server-tls
+    certSecretName: openshell-server-tls
     # K8s secret with ca.crt for client certificate verification
-    clientCaSecretName: navigator-server-client-ca
+    clientCaSecretName: openshell-server-client-ca
     # K8s secret mounted into sandbox pods for mTLS to the server
-    clientTlsSecretName: navigator-client-tls
+    clientTlsSecretName: openshell-client-tls
 
 # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway only.
 networkPolicy:
diff --git a/deploy/kube/manifests/navigator-helmchart.yaml b/deploy/kube/manifests/openshell-helmchart.yaml
similarity index 69%
rename from deploy/kube/manifests/navigator-helmchart.yaml
rename to deploy/kube/manifests/openshell-helmchart.yaml
index 3c97ffde6..b55cd91a8 100644
--- a/deploy/kube/manifests/navigator-helmchart.yaml
+++ b/deploy/kube/manifests/openshell-helmchart.yaml
@@ -6,7 +6,7 @@
 # automatically installed when the k3s cluster starts.
 #
 # The chart tarball is served from the k3s static file server at:
-# https://<k8s-api>/static/charts/navigator-0.1.0.tgz
+# https://<k8s-api>/static/charts/openshell-0.1.0.tgz
 #
 # Component images are pulled at runtime from ghcr.io.
 # Authentication is configured via /etc/rancher/k3s/registries.yaml
@@ -16,27 +16,27 @@
 apiVersion: helm.cattle.io/v1
 kind: HelmChart
 metadata:
-  name: navigator
+  name: openshell
   namespace: kube-system
 spec:
-  chart: https://%{KUBERNETES_API}%/static/charts/navigator-0.1.0.tgz
-  targetNamespace: navigator
+  chart: https://%{KUBERNETES_API}%/static/charts/openshell-0.1.0.tgz
+  targetNamespace: openshell
   createNamespace: true
   valuesContent: |-
     chartChecksum: __CHART_CHECKSUM__
     image:
-      repository: ghcr.io/nvidia/nemoclaw/server
+      repository: ghcr.io/nvidia/openshell/server
       tag: latest
       pullPolicy: Always
     server:
-      sandboxImage: ghcr.io/nvidia/nemoclaw/sandbox:latest
+      sandboxImage: ghcr.io/nvidia/openshell/sandbox:latest
       sshGatewayHost: __SSH_GATEWAY_HOST__
       sshGatewayPort: __SSH_GATEWAY_PORT__
       sshHandshakeSecret: __SSH_HANDSHAKE_SECRET__
-      grpcEndpoint: "https://navigator.navigator.svc.cluster.local:8080"
+      grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080"
       disableGatewayAuth: __DISABLE_GATEWAY_AUTH__
       disableTls: __DISABLE_TLS__
       tls:
-        certSecretName: navigator-server-tls
-        clientCaSecretName: navigator-server-client-ca
-        clientTlsSecretName: navigator-client-tls
+        certSecretName: openshell-server-tls
+        clientCaSecretName: openshell-server-client-ca
+        clientTlsSecretName: openshell-client-tls
diff --git a/examples/gateway-deploy-connect.md b/examples/gateway-deploy-connect.md
index 25b7c077e..1f084c9f1 100644
--- a/examples/gateway-deploy-connect.md
+++ b/examples/gateway-deploy-connect.md
@@ -190,7 +190,7 @@ openshell gateway info
 If the gateway is unreachable, inspect the container:
 
 ```bash
-docker logs navigator-cluster-openshell
+docker logs openshell-cluster-openshell
 ```
 
 Re-running `gateway start` is idempotent -- it reuses existing
diff --git a/examples/private-ip-routing/README.md b/examples/private-ip-routing/README.md
index 1f419aedf..9dba91da6 100644
--- a/examples/private-ip-routing/README.md
+++ b/examples/private-ip-routing/README.md
@@ -37,7 +37,7 @@ docker push 127.0.0.1:5000/demo/private-api:latest
 Deploy the pod:
 
 ```bash
-docker exec navigator-cluster-navigator \
+docker exec openshell-cluster-openshell \
   kubectl run private-api \
     --image=127.0.0.1:5000/demo/private-api:latest \
     --port=8080 \
@@ -47,7 +47,7 @@ docker exec navigator-cluster-navigator \
 Wait for it to be running and note the pod IP:
 
 ```bash
-docker exec navigator-cluster-navigator kubectl get pod private-api -o wide
+docker exec openshell-cluster-openshell kubectl get pod private-api -o wide
 ```
 
 Example output:
@@ -86,5 +86,5 @@ request returns `HTTP/1.1 403 Forbidden`.
 ## Cleanup
 
 ```bash
-docker exec navigator-cluster-navigator kubectl delete pod private-api
+docker exec openshell-cluster-openshell kubectl delete pod private-api
 ```
diff --git a/scripts/bin/openshell b/scripts/bin/openshell
index 5d80aa198..f421766f2 100755
--- a/scripts/bin/openshell
+++ b/scripts/bin/openshell
@@ -88,7 +88,7 @@ else
 fi
 
 if [[ "$needs_build" == "1" ]]; then
-  echo "Recompiling navigator-cli..." >&2
+  echo "Recompiling openshell..." >&2
   cargo build --package navigator-cli --quiet
   # Persist state after successful build
   mkdir -p "$(dirname "$STATE_FILE")"
diff --git a/scripts/remote-deploy.sh b/scripts/remote-deploy.sh
index e8e68982e..eb23a72fc 100755
--- a/scripts/remote-deploy.sh
+++ b/scripts/remote-deploy.sh
@@ -252,8 +252,8 @@ mise exec -- tasks/scripts/docker-build-cluster.sh
 mise exec -- tasks/scripts/docker-build-component.sh server
 mise exec -- tasks/scripts/docker-build-component.sh sandbox
 
-export OPENSHELL_CLUSTER_IMAGE="navigator/cluster:${IMAGE_TAG}"
-export OPENSHELL_PUSH_IMAGES="navigator/server:${IMAGE_TAG},navigator/sandbox:${IMAGE_TAG}"
+export OPENSHELL_CLUSTER_IMAGE="openshell/cluster:${IMAGE_TAG}"
+export OPENSHELL_PUSH_IMAGES="openshell/server:${IMAGE_TAG},openshell/sandbox:${IMAGE_TAG}"
 
 start_args=(
   gateway
diff --git a/tasks/docker.toml b/tasks/docker.toml
index 32d09bb1b..76123a0d9 100644
--- a/tasks/docker.toml
+++ b/tasks/docker.toml
@@ -25,7 +25,7 @@ hide = true
 ["docker:build:sandbox:nvidia"]
 description = "Build the NVIDIA GPU sandbox Docker image"
 depends = ["docker:build:sandbox"]
-run = "tasks/scripts/docker-build-component.sh sandbox nvidia --build-arg BASE_IMAGE=navigator/sandbox:${IMAGE_TAG:-dev}"
+run = "tasks/scripts/docker-build-component.sh sandbox nvidia --build-arg BASE_IMAGE=openshell/sandbox:${IMAGE_TAG:-dev}"
 hide = true
 
 ["docker:build:server"]
diff --git a/tasks/helm.toml b/tasks/helm.toml
index 9f65c753a..65305af39 100644
--- a/tasks/helm.toml
+++ b/tasks/helm.toml
@@ -5,5 +5,5 @@
 
 ["helm:lint"]
 description = "Lint the openshell helm chart"
-run = "helm lint deploy/helm/navigator"
+run = "helm lint deploy/helm/openshell"
 hide = true
diff --git a/tasks/scripts/cluster-bootstrap.sh b/tasks/scripts/cluster-bootstrap.sh
index ad1f67b2e..96c258ef0 100755
--- a/tasks/scripts/cluster-bootstrap.sh
+++ b/tasks/scripts/cluster-bootstrap.sh
@@ -5,6 +5,11 @@
 
 set -euo pipefail
 
+# Normalize cluster name: lowercase, replace invalid chars with hyphens
+normalize_name() {
+  echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
+}
+
 MODE=${1:-build}
 if [ "${MODE}" != "build" ] && [ "${MODE}" != "fast" ]; then
   echo "usage: $0 [build|fast]" >&2
@@ -24,9 +29,9 @@ LOCAL_REGISTRY_ADDR=127.0.0.1:5000
 if [ -n "${CI:-}" ] && [ -n "${CI_REGISTRY_IMAGE:-}" ]; then
   IMAGE_REPO_BASE_DEFAULT=${CI_REGISTRY_IMAGE}
 elif [ "${MODE}" = "fast" ]; then
-  IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/navigator
+  IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/openshell
 else
-  IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/navigator
+  IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/openshell
 fi
 
 IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-${IMAGE_REPO_BASE_DEFAULT}}}
@@ -34,7 +39,7 @@ REGISTRY_HOST=${OPENSHELL_REGISTRY_HOST:-${IMAGE_REPO_BASE%%/*}}
 REGISTRY_NAMESPACE_DEFAULT=${IMAGE_REPO_BASE#*/}
 
 if [ "${REGISTRY_NAMESPACE_DEFAULT}" = "${IMAGE_REPO_BASE}" ]; then
-  REGISTRY_NAMESPACE_DEFAULT=navigator
+  REGISTRY_NAMESPACE_DEFAULT=openshell
 fi
 
 has_env_key() {
@@ -50,10 +55,12 @@ append_env_if_missing() {
     return
   fi
   if [ -f "${ENV_FILE}" ] && [ -s "${ENV_FILE}" ]; then
-    printf "\n%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}"
-  else
-    printf "%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}"
+    # Ensure file ends with newline before appending, but don't add extra blank line
+    if [ "$(tail -c1 "${ENV_FILE}" | wc -l)" -eq 0 ]; then
+      printf "\n" >>"${ENV_FILE}"
+    fi
   fi
+  printf "%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}"
 }
 
 port_is_in_use() {
@@ -90,6 +97,7 @@ pick_random_port() {
 }
 
 CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")}
+CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}")
 
 if [ -n "${GATEWAY_PORT:-}" ]; then
   RESOLVED_GATEWAY_PORT=${GATEWAY_PORT}
@@ -102,7 +110,6 @@ fi
 OPENSHELL_CLUSTER=${OPENSHELL_CLUSTER:-${CLUSTER_NAME}}
 GATEWAY_PORT=${RESOLVED_GATEWAY_PORT}
 
-append_env_if_missing "CLUSTER_NAME" "${CLUSTER_NAME}"
 append_env_if_missing "GATEWAY_PORT" "${GATEWAY_PORT}"
 append_env_if_missing "OPENSHELL_CLUSTER" "${OPENSHELL_CLUSTER}"
 
@@ -203,8 +210,8 @@ if is_local_registry_host; then
   ensure_local_registry
 fi
 
-CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}"
-VOLUME_NAME="navigator-cluster-${CLUSTER_NAME}"
+CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}"
+VOLUME_NAME="openshell-cluster-${CLUSTER_NAME}"
 
 if [ "${MODE}" = "fast" ]; then
   if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1 || docker volume inspect "${VOLUME_NAME}" >/dev/null 2>&1; then
@@ -233,7 +240,7 @@ fi
 # `docker-build-cluster.sh` and contains the bundled Helm chart and
 # manifests from the current working tree.
 if [ -z "${OPENSHELL_CLUSTER_IMAGE:-}" ]; then
-  export OPENSHELL_CLUSTER_IMAGE="navigator/cluster:${IMAGE_TAG}"
+  export OPENSHELL_CLUSTER_IMAGE="openshell/cluster:${IMAGE_TAG}"
 fi
 
 DEPLOY_CMD=(openshell gateway start --name "${CLUSTER_NAME}" --port "${GATEWAY_PORT}" --update-kube-config)
diff --git a/tasks/scripts/cluster-deploy-fast.sh b/tasks/scripts/cluster-deploy-fast.sh
index 07f6c4ab8..61029805b 100755
--- a/tasks/scripts/cluster-deploy-fast.sh
+++ b/tasks/scripts/cluster-deploy-fast.sh
@@ -5,9 +5,15 @@
 
 set -euo pipefail
 
+# Normalize cluster name: lowercase, replace invalid chars with hyphens
+normalize_name() {
+  echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
+}
+
 CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")}
-CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}"
-IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/navigator}}
+CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}")
+CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}"
+IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/openshell}}
 IMAGE_TAG=${IMAGE_TAG:-dev}
 RUST_BUILD_PROFILE=${RUST_BUILD_PROFILE:-debug}
 DEPLOY_FAST_MODE=${DEPLOY_FAST_MODE:-auto}
@@ -169,7 +175,7 @@ matches_sandbox() {
 matches_helm() {
   local path=$1
   case "${path}" in
-    deploy/helm/navigator/*)
+    deploy/helm/openshell/*)
       return 0
       ;;
     *)
@@ -196,7 +202,7 @@ compute_fingerprint() {
       committed_trees=$(git ls-tree HEAD Cargo.toml Cargo.lock proto/ deploy/docker/cross-build.sh crates/navigator-core/ crates/navigator-policy/ crates/navigator-providers/ crates/navigator-sandbox/ deploy/docker/sandbox/ python/ pyproject.toml uv.lock 2>/dev/null || true)
       ;;
     helm)
-      committed_trees=$(git ls-tree HEAD deploy/helm/navigator/ 2>/dev/null || true)
+      committed_trees=$(git ls-tree HEAD deploy/helm/openshell/ 2>/dev/null || true)
       ;;
   esac
   if [[ -n "${committed_trees}" ]]; then
@@ -341,7 +347,7 @@ for component in server sandbox; do
   if [[ "${!var}" == "1" ]]; then
     # Tag may fail with AlreadyExists when the image digest hasn't changed;
     # this is harmless — the registry already has the correct image.
-    docker tag "navigator/${component}:${IMAGE_TAG}" "${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}" 2>/dev/null || true
+    docker tag "openshell/${component}:${IMAGE_TAG}" "${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}" 2>/dev/null || true
     pushed_images+=("${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}")
     built_components+=("${component}")
   fi
@@ -381,21 +387,21 @@ if [[ "${needs_helm_upgrade}" == "1" ]]; then
   # sandbox callbacks to plaintext.
   # Retrieve the existing handshake secret from the running release, or generate
   # a new one if this is the first deploy with the mandatory secret.
-  EXISTING_SECRET=$(helm get values navigator -n navigator -o json 2>/dev/null \
+  EXISTING_SECRET=$(helm get values openshell -n openshell -o json 2>/dev/null \
     | grep -o '"sshHandshakeSecret":"[^"]*"' \
     | cut -d'"' -f4) || true
   SSH_HANDSHAKE_SECRET="${EXISTING_SECRET:-$(openssl rand -hex 32)}"
 
-  helm upgrade navigator deploy/helm/navigator \
-    --namespace navigator \
+  helm upgrade openshell deploy/helm/openshell \
+    --namespace openshell \
     --set image.repository=${IMAGE_REPO_BASE}/server \
     --set image.tag=${IMAGE_TAG} \
     --set image.pullPolicy=Always \
-    --set-string server.grpcEndpoint=https://navigator.navigator.svc.cluster.local:8080 \
+    --set-string server.grpcEndpoint=https://openshell.openshell.svc.cluster.local:8080 \
     --set server.sandboxImage=${IMAGE_REPO_BASE}/sandbox:${IMAGE_TAG} \
-    --set server.tls.certSecretName=navigator-server-tls \
-    --set server.tls.clientCaSecretName=navigator-server-client-ca \
-    --set server.tls.clientTlsSecretName=navigator-client-tls \
+    --set server.tls.certSecretName=openshell-server-tls \
+    --set server.tls.clientCaSecretName=openshell-server-client-ca \
+    --set server.tls.clientTlsSecretName=openshell-client-tls \
     --set server.sshHandshakeSecret=${SSH_HANDSHAKE_SECRET} \
     "${helm_wait_args[@]}"
   helm_end=$(date +%s)
@@ -405,14 +411,14 @@ fi
 if [[ "${#pushed_images[@]}" -gt 0 ]]; then
   rollout_start=$(date +%s)
   echo "Restarting deployment to pick up updated images..."
-  if kubectl get statefulset/navigator -n navigator >/dev/null 2>&1; then
-    kubectl rollout restart statefulset/navigator -n navigator
-    kubectl rollout status statefulset/navigator -n navigator
-  elif kubectl get deployment/navigator -n navigator >/dev/null 2>&1; then
-    kubectl rollout restart deployment/navigator -n navigator
-    kubectl rollout status deployment/navigator -n navigator
+  if kubectl get statefulset/openshell -n openshell >/dev/null 2>&1; then
+    kubectl rollout restart statefulset/openshell -n openshell
+    kubectl rollout status statefulset/openshell -n openshell
+  elif kubectl get deployment/openshell -n openshell >/dev/null 2>&1; then
+    kubectl rollout restart deployment/openshell -n openshell
+    kubectl rollout status deployment/openshell -n openshell
   else
-    echo "Warning: no navigator workload found to roll out in namespace 'navigator'."
+    echo "Warning: no openshell workload found to roll out in namespace 'openshell'."
   fi
   rollout_end=$(date +%s)
   log_duration "Rollout" "${rollout_start}" "${rollout_end}"
diff --git a/tasks/scripts/cluster-push-component.sh b/tasks/scripts/cluster-push-component.sh
index c8e48da7f..38ecf9fa4 100755
--- a/tasks/scripts/cluster-push-component.sh
+++ b/tasks/scripts/cluster-push-component.sh
@@ -20,17 +20,23 @@ case "${component}" in
     ;;
 esac
 
+# Normalize cluster name: lowercase, replace invalid chars with hyphens
+normalize_name() {
+  echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
+}
+
 IMAGE_TAG=${IMAGE_TAG:-dev}
-IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/navigator}}
+IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/openshell}}
 CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")}
-CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}"
-SOURCE_IMAGE="navigator/${component}:${IMAGE_TAG}"
+CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}")
+CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}"
+SOURCE_IMAGE="openshell/${component}:${IMAGE_TAG}"
 TARGET_IMAGE="${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}"
 
 source_candidates=(
-  "navigator/${component}:${IMAGE_TAG}"
-  "localhost:5000/navigator/${component}:${IMAGE_TAG}"
-  "127.0.0.1:5000/navigator/${component}:${IMAGE_TAG}"
+  "openshell/${component}:${IMAGE_TAG}"
+  "localhost:5000/openshell/${component}:${IMAGE_TAG}"
+  "127.0.0.1:5000/openshell/${component}:${IMAGE_TAG}"
 )
 
 resolved_source_image=""
@@ -42,15 +48,9 @@ for candidate in "${source_candidates[@]}"; do
 done
 
 if [ -z "${resolved_source_image}" ]; then
-  echo "missing local image for ${component}:${IMAGE_TAG}" >&2
-  echo "checked candidates:" >&2
-  for candidate in "${source_candidates[@]}"; do
-    echo "  ${candidate}" >&2
-  done
-  echo "build it first with either:" >&2
-  echo "  mise run docker:build:${component}" >&2
-  echo "  mise run cluster" >&2
-  exit 1
+  echo "Local image not found for ${component}:${IMAGE_TAG}, building..."
+  tasks/scripts/docker-build-component.sh "${component}"
+  resolved_source_image="openshell/${component}:${IMAGE_TAG}"
 fi
 
 docker tag "${resolved_source_image}" "${TARGET_IMAGE}"
diff --git a/tasks/scripts/cluster.sh b/tasks/scripts/cluster.sh
index 90273c66f..6dc6bf3d4 100755
--- a/tasks/scripts/cluster.sh
+++ b/tasks/scripts/cluster.sh
@@ -8,8 +8,14 @@
 
 set -euo pipefail
 
+# Normalize cluster name: lowercase, replace invalid chars with hyphens
+normalize_name() {
+  echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
+}
+
 CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")}
-CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}"
+CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}")
+CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}"
 
 if ! docker ps -q --filter "name=${CONTAINER_NAME}" | grep -q .; then
   echo "No running cluster found. Bootstrapping..."
diff --git a/tasks/scripts/docker-build-cluster.sh b/tasks/scripts/docker-build-cluster.sh
index 24c626afe..9f52f54c4 100755
--- a/tasks/scripts/docker-build-cluster.sh
+++ b/tasks/scripts/docker-build-cluster.sh
@@ -16,7 +16,7 @@
 set -euo pipefail
 
 IMAGE_TAG=${IMAGE_TAG:-dev}
-IMAGE_NAME="navigator/cluster"
+IMAGE_NAME="openshell/cluster"
 if [[ -n "${IMAGE_REGISTRY:-}" ]]; then
   IMAGE_NAME="${IMAGE_REGISTRY}/cluster"
 fi
@@ -49,9 +49,9 @@ fi
 # Create build directory for charts
 mkdir -p deploy/docker/.build/charts
 
-# Package navigator helm chart
-echo "Packaging navigator helm chart..."
-helm package deploy/helm/navigator -d deploy/docker/.build/charts/
+# Package helm chart
+echo "Packaging helm chart..."
+helm package deploy/helm/openshell -d deploy/docker/.build/charts/
 
 # Build cluster image (no bundled component images — they are pulled at runtime
 # from the distribution registry; credentials are injected at deploy time)
diff --git a/tasks/scripts/docker-build-component.sh b/tasks/scripts/docker-build-component.sh
index eefea9c83..3156614ab 100755
--- a/tasks/scripts/docker-build-component.sh
+++ b/tasks/scripts/docker-build-component.sh
@@ -8,11 +8,11 @@
 #
 # Components with a subdirectory layout (e.g. deploy/docker/sandbox/) support
 # an optional variant argument:
-#   docker-build-component.sh sandbox          -> Dockerfile.base  -> navigator/sandbox:dev
-#   docker-build-component.sh sandbox nvidia   -> Dockerfile.nvidia -> navigator/sandbox-nvidia:dev
+#   docker-build-component.sh sandbox          -> Dockerfile.base  -> openshell/sandbox:dev
+#   docker-build-component.sh sandbox nvidia   -> Dockerfile.nvidia -> openshell/sandbox-nvidia:dev
 #
 # Components without a subdirectory use the flat layout:
-#   docker-build-component.sh server           -> Dockerfile.server -> navigator/server:dev
+#   docker-build-component.sh server           -> Dockerfile.server -> openshell/server:dev
 #
 # Environment:
 #   IMAGE_TAG          - Image tag (default: dev)
@@ -72,14 +72,14 @@ if [[ -d "${COMPONENT_DIR}" ]]; then
   VARIANT=${VARIANT:-base}
   DOCKERFILE="${COMPONENT_DIR}/Dockerfile.${VARIANT}"
   if [[ "${VARIANT}" == "base" ]]; then
-    IMAGE_NAME="navigator/${COMPONENT}"
+    IMAGE_NAME="openshell/${COMPONENT}"
   else
-    IMAGE_NAME="navigator/${COMPONENT}-${VARIANT}"
+    IMAGE_NAME="openshell/${COMPONENT}-${VARIANT}"
   fi
 else
   # Flat layout: deploy/docker/Dockerfile.<component>
   DOCKERFILE="deploy/docker/Dockerfile.${COMPONENT}"
-  IMAGE_NAME="navigator/${COMPONENT}"
+  IMAGE_NAME="openshell/${COMPONENT}"
 fi
 
 if [[ ! -f "${DOCKERFILE}" ]]; then
@@ -88,9 +88,9 @@ if [[ ! -f "${DOCKERFILE}" ]]; then
 fi
 
 # Prefix with registry when set (e.g. ghcr.io/org/repo/server:tag).
-# Replaces the default "navigator/" prefix with the registry path.
+# Replaces the default "openshell/" prefix with the registry path.
 if [[ -n "${IMAGE_REGISTRY:-}" ]]; then
-  _suffix="${IMAGE_NAME#navigator/}"
+  _suffix="${IMAGE_NAME#openshell/}"
   IMAGE_NAME="${IMAGE_REGISTRY}/${_suffix}"
 fi
 
diff --git a/tasks/scripts/docker-publish-multiarch.sh b/tasks/scripts/docker-publish-multiarch.sh
index 1c5d4ba01..918f16445 100755
--- a/tasks/scripts/docker-publish-multiarch.sh
+++ b/tasks/scripts/docker-publish-multiarch.sh
@@ -104,13 +104,13 @@ fi
 case "$MODE" in
   registry)
     REGISTRY=${DOCKER_REGISTRY:?Set DOCKER_REGISTRY to push multi-arch images (e.g. ghcr.io/myorg)}
-    IMAGE_PREFIX="navigator-"
+    IMAGE_PREFIX="openshell-"
     ;;
   ecr)
     AWS_ACCOUNT_ID=${AWS_ACCOUNT_ID:-524473328983}
     AWS_REGION=${AWS_REGION:-us-west-2}
     ECR_HOST="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
-    REGISTRY="${ECR_HOST}/navigator"
+    REGISTRY="${ECR_HOST}/openshell"
     IMAGE_PREFIX=""
     EXTRA_BUILD_FLAGS="--provenance=false --sbom=false"
     ;;
@@ -187,7 +187,7 @@ done
 # Step 2: Package helm charts (architecture-independent)
 # ---------------------------------------------------------------------------
 mkdir -p deploy/docker/.build/charts
-echo "Packaging navigator helm chart..."
+echo "Packaging helm chart..."
 helm package deploy/helm/navigator -d deploy/docker/.build/charts/
 
 # ---------------------------------------------------------------------------
diff --git a/tasks/scripts/sandbox.sh b/tasks/scripts/sandbox.sh
index 4cf9bbca9..bf7b3cc63 100755
--- a/tasks/scripts/sandbox.sh
+++ b/tasks/scripts/sandbox.sh
@@ -15,7 +15,7 @@ set -euo pipefail
 
 SANDBOX_NAME="dev"
 CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")}
-CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}"
+CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}"
 STATE_DIR=${SANDBOX_STATE_DIR:-.cache}
 SANDBOX_STATE_FILE=${STATE_DIR}/sandbox-dev.state
 DEPLOY_STATE_FILE=${DEPLOY_FAST_STATE_FILE:-${STATE_DIR}/cluster-deploy-fast.state}
