See what broke. Understand why. Act safely.
Browse, diagnose, and act on your cluster — all from one terminal console.
kute is a keyboard-driven Kubernetes console built with Bubble Tea and Lip Gloss, designed around the first 15 minutes of an incident rather than plain object browsing.
One frame from an incident walkthrough: a clean production namespace, then all-namespaces surfaces a CrashLoopBackOff pod, logs show the cause, delete asks for explicit confirmation, and the timeline ties the crash to a rollout ten minutes earlier. Watch it play, or run it yourself with --demo below.
go run ./cmd/kute --demoNo cluster or kubeconfig required — --demo runs against a built-in in-memory fake cluster so you can explore every screen immediately.
Answers, not objects.
k9s is an excellent general-purpose Kubernetes browser. If fast object browsing is your main need, it may already be the right tool.
kute is built for the first 15 minutes after something breaks: unhealthy workloads rise first, termination causes remain visible, logs mark container restarts, and the incident timeline correlates crashes, events, and rollout changes. It still browses and edits resources, but its center of gravity is diagnosis.
- Unhealthy-first triage — every list sorts broken workloads to the top; fully healthy groups collapse to a single line instead of burying the incident in green rows.
- Restart-aware logs and an incident timeline — pod logs draw a boundary at every container restart; the timeline screen merges events, restarts, and rollout revisions into one newest-first feed answering "what changed recently."
- Termination causes and conditions shown verbatim — the actual condition message is the diagnosis, never paraphrased or summarized away.
- Batch workloads get their own failure trail — CronJob detail promotes the controller's own failure reason above retained run history; Jobs open an attempt ledger with every retry's exit code, duration, and node kept together.
- Routing tables resolve to what's actually answering — Ingress and Gateway API HTTPRoute show every host and path against the backend that's actually serving it, live endpoint health included.
- Every mutating action shows its command first — exec, port-forward, scale, image/resource changes, label edits, rollout restarts, and Helm rollbacks all print the exact command that's about to run before it runs. Copyable documentation, not a black box.
- Deliberate friction on destructive actions — reversible verbs like cordon execute immediately; delete and rollout-restart are tiered, with inline y/N confirmation normally and a type-the-name modal when the context is explicitly listed as production in your config file, never guessed from a name. Drain always confirms; force delete (
ctrl+k) and deleting a Namespace, CRD or Node always need the typed name, prod or not. - CRDs, Flux, and Argo CD render with zero configuration — kinds discovered from the API automatically get columns, status, and detail views (a cert-manager
Certificate → CertificateRequest → Order → Challengechain shows the real failure reason at each step); Flux's sources/reconcilers and Argo CD's Applications get their own screens, sync status kept separate from workload health. No plugins, no per-CRD setup. - Alt-tab namespace/context switching — the same palette that jumps to any resource kind also toggles between your last two namespaces or contexts with no typing, and recalls recent ones by number.
- One palette jumps to anything — a kind's alias letter, a fuzzy kind name, or a specific resource by name all live in the same
gpalette; typing a pod's name jumps straight to it, switching kind and namespace as a side effect.
See it: namespace palette alt-tab + digit recall
Alt-tabs to ingress-nginx and back to default with no typing, then recalls production and argocd from the RECENT row by their digit. Watch it play.
See it: goto palette alias switch, alt-tab, and jump-to-pod
Pins Deployments to rank 1 with the d alias, alt-tabs between Pods and Deployments with no typing, then types "cache" to jump straight to the cache-0 pod. Watch it play.
When the API server connection drops, kute keeps showing your last known state (desaturated, with an age stamp) instead of blanking the screen — and disables every mutating action until the connection is back, including the ones that hand off to kubectl: exec, node shell, and edit.
One binary. Nothing installed in the cluster.
curl -fsSL https://kute.dev/install.sh | shOr with Homebrew:
brew install kute-dev/tap/kuteOn Windows, with PowerShell:
irm https://kute.dev/install.ps1 | iexOr with Scoop:
scoop bucket add kute-dev https://github.com/kute-dev/scoop-bucket
scoop install kuteVerify the download
Release archives are signed with a keyless cosign identity bound to the release workflow, ship an SPDX SBOM, and carry a GitHub build provenance attestation. Both installers verify the signature automatically when cosign is on your PATH, and always verify the checksum.
VERSION=0.4.0
FILE=kute_${VERSION}_linux_amd64.tar.gz
BASE=https://github.com/kute-dev/kute/releases/download/v${VERSION}
curl -fsSLO "${BASE}/${FILE}"
curl -fsSLO "${BASE}/${FILE}.sigstore.json"
cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/kute-dev/kute/\.github/workflows/release\.yml@refs/tags/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--bundle "${FILE}.sigstore.json" \
"${FILE}"Success is Verified OK. Needs cosign 3 or newer as written; on 2.x add --new-bundle-format. docs/verifying-releases.md covers provenance (gh attestation verify), the SBOM, and what the installers do on a machine without cosign.
Run kute with no arguments and it opens on the context you last used, in the namespace you left it in. The flags below pin a starting point instead — useful in a script, an alias, or when reproducing something for a bug report.
| Flag | What it does |
|---|---|
--context <name> |
Launch against a specific kubeconfig context, instead of the last-used one (or the kubeconfig's current-context on a first run). |
-n, --namespace <name> |
Launch in a specific namespace. Outranks both the remembered namespace and the context's own default. |
--namespace-scoped <name> |
Restrict every informer to this one namespace instead of the whole cluster, and launch in it — for an identity that only has namespace-scoped (Role) access, not cluster-wide (ClusterRole) list. Mutually exclusive with -n/--namespace, since it already selects the namespace. |
--kubeconfig <path> |
Read a specific kubeconfig file. Takes precedence over $KUBECONFIG, which takes precedence over ~/.kube/config. |
--theme dark|light |
Force a theme instead of detecting it from the terminal background. |
--demo |
Run against a built-in in-memory fake cluster. No kubeconfig or cluster needed. |
--log-file <path> |
Write the error and client-go log stream to a file. Nothing is logged to the terminal — it's the screen. |
--version |
Print version, commit and build date, then exit. |
Everything else is a keystroke: ? from any screen lists the keys for that screen plus the global ones.
| Keys | In kute |
|---|---|
j k |
move, on every screen |
gg G |
first/last row (home/end too) |
ctrl+d ctrl+u |
half page down/up |
ctrl+f ctrl+b |
page down/up (pgdown/pgup too) |
ctrl+e ctrl+y |
one row down/up |
/ then n N |
search the YAML view, then step through matches (↵ keeps the search, esc clears it) |
: |
the goto palette, same as g |
:q :q! |
quit, after a confirm / without one |
esc |
back exactly one level |
E |
edit the object in $KUBE_EDITOR/$EDITOR (via kubectl edit) |
ctrl+o |
in a ConfigMap's buffer editor (e), edit that one value in the same editor; it comes back for review before alt+a applies |
g still opens the goto palette: a second g pressed straight away (with nothing typed after it) is gg, so g then gitlab still searches for gitlab.
If kute crashes it writes a crash report — build, context, namespace, kind, screen, terminal size, the panic and the tail of the log — to ~/.local/state/kute/kute-crash-<timestamp>.log, and prints the path. Attach that file to the bug form.
If it misbehaves without crashing, re-run with --log-file /tmp/kute.log, reproduce, and attach that instead. Both files name your contexts, clusters and namespaces — redact anything you can't share. docs/diagnostics.md has the details.
kute authenticates exactly the way kubectl does, from the same kubeconfig: client certificates, bearer tokens, OIDC (Rancher, Dex, Keycloak), and exec credential plugins — gke-gcloud-auth-plugin for GKE, aws eks get-token for EKS, kubelogin for AAD-authenticated AKS. The plugin binary has to be on your PATH, as it does for kubectl; kute bundles no cloud SDKs.
One difference worth knowing about: kute holds the terminal, so a credential plugin can never prompt you. When a plugin's own login has expired, kute says so — with the plugin's own message — and stops retrying rather than re-running it every couple of seconds. Re-authenticate in another shell (aws sso login, gcloud auth login, …) and press r.
kute reads cluster-wide by default. It watches each kind once across the whole cluster and filters by namespace in memory, so switching namespaces is instant and costs no extra API traffic. The consequence is that a token scoped to a single namespace — a Role and RoleBinding, with nothing bound at cluster scope — is not enough by default: kute is refused on its first read and can't get started. If that's your situation, launch with --namespace-scoped <name> instead, which restricts every informer to that one namespace so a namespaced Role is sufficient; see #9 for background.
The minimum to launch is cluster-wide read on three kinds:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kute
rules:
# Namespaces, Pods and Nodes are read at connect — the breadcrumb, the
# namespace palette, the default landing screen and the node counts.
- apiGroups: [""]
resources: ["namespaces", "pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
# Discovery. Without it kute still runs; it just never finds your CRDs.
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]Everything past those three is read on demand, when you first open a screen that needs it — so grant whatever you want to browse (apps, configmaps, services, events, secrets for Helm releases, and so on) and no more. A kind you haven't been granted isn't a broken cluster: that screen shows a 403 card naming the exact resource and verb, and the rest of kute keeps working.
If your cluster already binds the built-in view role, a cluster-wide binding of it covers most of this. It does not include nodes, secrets or customresourcedefinitions, so the node-derived screens, Helm releases and custom kinds will come up empty or refused until you add them.
If you can't get a cluster-wide grant at all — only a Role bound in one namespace, nothing bound at cluster scope — launch with --namespace-scoped <name> instead. It restricts every namespaced kind's cache to that one namespace, so a plain Role is enough to get started; nothing below needs a ClusterRole.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kute
namespace: my-namespace
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kute
namespace: my-namespace
subjects:
- kind: User # or ServiceAccount/Group, matching your auth
name: <your-identity>
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: kute
apiGroup: rbac.authorization.k8s.io