A fast, opinionated TUI for Kubernetes (k8s & k3s) — written in Go on top of Bubble Tea.
Hybrid TUI and kubectl companion. Every keystroke shows its
kubectlequivalent in the footer, so you build CLI muscle memory while you work.
- kubectl companion — the footer always shows the exact
kubectlcommand behind the screen, or behind the panel or row under the cursor, with--contextand--kubeconfigadded whenever a plainkubectlin your shell would reach another cluster.ycopies it, exec and describe run that very argv, and on exit k4s prints the session's commands so they stay in your scrollback.≈marks the few commands that do a little more or less than k4s - Dashboard — the home screen answers three questions in order: is the
cluster OK (a verdict line), what do I run next (up to five
▸ nextsuggestions, each a read-only kubectl command), and where is the problem (seven numbered panels: cluster, nodes, pods, workloads, events, k3s, top pods). Every panel and row maps to one kubectl command; Enter opens the matching k4s view - Lists — pods, deployments, services, namespaces, events and nodes,
with a
/filter, auto-refresh, and Esc back to the list as you left it - Describe and YAML —
kubectl describethrough your own kubectl (a built-in describer when there is none) andoforkubectl get -o yaml, both searchable with/ - Logs you can trust — every pod of a deployment or a service in one
stream, with per-pod colours; the pods of a rollout join on their own,
every stream that ends says why,
pshows the previous instance, and search is smart-case. Soft-wrap (w) and horizontal scroll (←/→) - Exec, port-forward, top —
eopens a shell in a pod (cmd.exein Windows pods),fa port-forward that the next launch can revive, and:topshows pods and nodes from the metrics API - Any cluster, any RBAC — a list forbidden across all namespaces falls
back to the context's namespace, and every error says in one line what
went wrong and which
kubectl auth can-iasks the same - Best on k3s — the header names the distribution and release, the
dashboard checks k3s's bundled components, and on a k3s node k4s finds
/etc/rancher/k3s/k3s.yamlandk3s kubectlby itself - Readable on any theme — dark / light background detection, a
high-contrast theme,
K4S_THEMEandNO_COLOR; every state is a glyph and a word, never colour alone - Safe to point at any cluster — text from the cluster is made inert
before it reaches your terminal, and
yrefuses a command in which a name from the cluster would set--serveror--kubeconfig
Every release from
v1.1.5 on carries an archive per platform and a checksums.txt (SHA-256).
Each archive holds the k4s binary, LICENSE and README.md.
| Platform | Archive |
|---|---|
| Linux amd64 / arm64 / armv7 (Raspberry Pi) | k4s_<version>_linux_<amd64|arm64|armv7>.tar.gz |
| macOS Intel / Apple silicon | k4s_<version>_darwin_<amd64|arm64>.tar.gz |
| Windows amd64 | k4s_<version>_windows_amd64.zip |
<version> has no leading v, e.g. k4s_1.1.5_linux_arm64.tar.gz.
V=1.1.5 TARGET=linux_arm64
curl -fLO "https://github.com/LywwKkA-aD/k4s/releases/download/v${V}/k4s_${V}_${TARGET}.tar.gz"
curl -fLO "https://github.com/LywwKkA-aD/k4s/releases/download/v${V}/checksums.txt"
sha256sum --ignore-missing -c checksums.txt # macOS: shasum -a 256 --ignore-missing -c checksums.txt
tar xzf "k4s_${V}_${TARGET}.tar.gz" k4s
sudo install k4s /usr/local/bin/
k4s --version- Windows: unzip, then compare
(Get-FileHash k4s_1.1.5_windows_amd64.zip -Algorithm SHA256).Hashwith that file's line inchecksums.txt. - macOS: the binaries are not notarized. If a browser downloaded the
archive, clear the quarantine flag with
xattr -d com.apple.quarantine k4s.
go install github.com/LywwKkA-aD/k4s/cmd/k4s@latestRequires Go 1.26+.
kubectl on PATH (or k3s kubectl on a k3s node) is needed for exec
(e) and for the terminal login of a credential plugin. Describe runs it
too when it is there; without it k4s describes pods, deployments and
services with a built-in describer.
k4s # current context, starting in its namespace
k4s --context staging -n web # another context and namespace
k4s -A # all namespaces
k4s --kubeconfig ./lab.yaml # a kubeconfig of your choice
k4s --theme high-contrast # text at 7:1 or better
k4s --no-recap # print no kubectl commands on exit
k4s --version # print the version and exit
k4s --help # list the flags and exit| Flag | Meaning |
|---|---|
--kubeconfig <file> |
kubeconfig to use instead of the lookup below |
--context <name> |
context to use instead of the kubeconfig's current-context |
-n, --namespace <name> |
namespace to start in |
-A, --all-namespaces |
start in all namespaces |
--theme <mode> |
auto, dark, light, mono or high-contrast; wins over K4S_THEME and NO_COLOR |
--no-preflight |
do not run the credential plugin before the TUI starts |
--no-recap |
do not print the session's kubectl commands on exit |
-h, --help |
show the flags and exit |
--version |
print the version and exit |
| Environment | Meaning |
|---|---|
KUBECONFIG |
kubeconfig file list, as kubectl reads it |
K4S_THEME |
auto (default), dark, light, mono or high-contrast |
K4S_NO_PREFLIGHT |
anything but 0, false, no or off acts as --no-preflight |
K4S_NO_RECAP |
anything but 0, false, no or off acts as --no-recap |
NO_COLOR |
set: no colour, as K4S_THEME=mono |
XDG_STATE_HOME |
where k4s/k4s.log and k4s/portforwards.json live when it is an absolute path (default ~/.local/state) |
A --kubeconfig or --context that cannot be loaded stops k4s before the
TUI starts. Without --kubeconfig, k4s looks in this order:
$KUBECONFIG(a list, as kubectl reads it);~/.kube/config;- on Linux, the k3s server's own
/etc/rancher/k3s/k3s.yaml, so k4s works on a k3s node without setup. k3s writes that file for root only; when it is not readable k4s prints how to make a copy for your user.
k4s starts in the namespace the context sets, as kubectl does, and in all
namespaces when it sets none; -n and -A override it. In the TUI,
:ns <name> switches namespace and :ns -A shows all of them (all is a
valid namespace name, so it means that namespace).
Footer commands always reach the cluster k4s shows when pasted into the
shell k4s was started from: --context is added when you passed
--context or k4s talks to another context than the kubeconfig's
current-context (after :ctx, say), and --kubeconfig when k4s reads a
file a plain kubectl would not (an explicit --kubeconfig, the k3s
fallback). y
copies the command through the terminal (OSC 52), which also works over
SSH; inside tmux it needs set -g set-clipboard on. y copies nothing,
and says why, when a word in the command would set a session flag such as
--server (a hostile object name can carry one) or holds control
characters.
When k4s exits it prints the kubectl commands the session led to (the
views you opened, describe, logs, exec and port-forward), each once, the 15
most recently used, oldest first, as the footer showed them. They stay in
your shell's scrollback after the TUI has gone; --no-recap or
K4S_NO_RECAP=1 turns this off.
k4s never prints over the TUI. client-go errors and Kubernetes API warnings
go to $XDG_STATE_HOME/k4s/k4s.log (default ~/.local/state/k4s/k4s.log);
if anything was logged, k4s prints the count and the path when it exits.
Contexts that log in through a credential plugin (EKS, GKE, OIDC/kubelogin)
are contacted once before the TUI starts, so an SSO or device-code prompt
shows up on the plain terminal; it gives up after 30 s. Plugin output while
the TUI runs is kept in memory and printed after exit; it is never written
to the log file. When a plugin needs a login while k4s owns the terminal
(after :ctx, or after a preflight that failed for want of one), k4s asks
whether to log in; Enter runs kubectl version for that context on the
plain terminal and comes back.
: opens the command bar in the footer. Commands ignore case.
| Command | Aliases | Shows |
|---|---|---|
:dashboard |
:home |
the dashboard |
:pods |
:po, :pod |
kubectl get pods |
:deployments |
:deploy, :deployment, :dp |
kubectl get deployments |
:services |
:svc, :service |
kubectl get services |
:namespaces |
:ns |
kubectl get namespaces |
:events |
:ev, :event |
kubectl events |
:nodes |
:no, :node |
kubectl get nodes -o wide |
:top |
:metrics |
kubectl top pods (n for nodes) |
:contexts |
:ctx, :context |
kubectl config get-contexts |
:forwards |
:pf, :forward, :portforward, :port-forward |
the port-forwards of this cluster |
:pods, :deploy, :svc, :top and :events take a namespace as
kubectl spells it:
:pods demo :pods -n demo :pods -A :pods --all-namespaces
From another view the list opens in that namespace, one Esc away; on the
list itself it switches in place, keeping the filter, watch and mode.
:ns <name> does the same for the current list (from any other view it
opens pods there) and :ns -A shows every namespace. The name is checked
as a namespace name and never looked up, since RBAC may forbid reading
namespaces. :nodes and the other commands take no namespace.
The footer's first row lists the keys of the screen you are on, and ?
lists them all. While a filter, a search or a prompt has the keyboard,
every key goes to it: q, y and o are just letters there.
| Global | Action |
|---|---|
q |
go home to the dashboard; on the dashboard, quit |
ctrl+c |
quit, from anywhere |
esc |
back: first what the view uses it for (a kept filter, a search, a zoom), then the previous view as you left it |
: |
command bar (see Commands) |
? |
help: global keys, the view's keys, the dashboard map, the commands; ?, esc or q close it |
y |
copy the footer's kubectl command (OSC 52) |
o |
YAML of the object the view shows or has under the cursor |
| Key | Action |
|---|---|
tab / shift+tab |
next / previous panel |
0–7 |
jump to a panel (0 is the banner); one the terminal is too small for opens zoomed |
← / → |
neighbouring panel |
↑ / ↓ (k / j) |
rows of the focused panel; in the banner, the ▸ next suggestions |
enter |
on a panel, its k4s view (:pods, :deploy, :events with only the warnings, ...); on a row, describe its object or open its list; on a suggestion, what it says; on the verdict, the most severe panel |
l |
logs of a pod row, or of a deployment row's pods; on a crash loop suggestion, the previous instance's |
z |
zoom the focused panel to the whole screen, and back |
s |
in panel 7, sort the top pods by CPU or memory |
r |
refresh every source now |
w |
watch on or off; off polls nothing |
esc |
leave the zoom, then the row, then the panel |
o / y |
YAML of the row's object / copy the focused command |
The panels, by key: 1 cluster, 2 nodes, 3 pods, 4 workloads,
5 events, 6 k3s (on k3s only), 7 top pods (with metrics-server).
? lists each with its kubectl command.
In pods, deployments, services, namespaces, events and nodes:
| Key | Action |
|---|---|
/ |
filter by substring, ignoring case; enter keeps it, esc drops it |
w |
auto-refresh on or off; the hint shows watch 5s, watch 20s (slow API) or paused |
r |
refresh now |
| In a pod row | Action |
|---|---|
enter |
describe |
l |
tail logs; a pod with several containers opens a picker, init containers as init:<name> |
e |
kubectl exec -it -- sh (cmd.exe in a Windows pod) |
f |
port-forward to the pod |
| In a deployment row | Action |
|---|---|
enter |
describe |
l |
tail logs of every pod its ReplicaSets run |
f |
port-forward to a backing pod |
| In a service row | Action |
|---|---|
enter |
describe |
l |
tail logs of every pod behind it, finished ones skipped |
f |
port-forward to the service (default local = remote + 8000 if remote < 1024) |
| Other views | Action |
|---|---|
enter in :ns |
pods of that namespace (<all> for every namespace) |
/ in :ns |
type a namespace, when RBAC forbids listing them |
enter in :events |
describe the object the event is about |
W in :events |
only warnings, or every type again |
enter in :nodes |
describe the node |
n in :top |
pods or nodes |
s in :top |
sort by CPU or memory |
enter in :ctx |
switch to that context |
r in :ctx |
read the kubeconfig again |
In the forwards view (:pf) |
Action |
|---|---|
enter |
start a stopped forward |
s |
stop a running forward |
r |
restart |
d |
delete from state |
ctrl+r |
refresh |
| Key | Action |
|---|---|
↑ ↓ pgup pgdn |
scroll; scrolling up pauses auto-follow, the bottom resumes it |
f |
auto-follow on or off |
w |
soft-wrap on or off |
← / → |
horizontal scroll, with wrap off |
t |
full pod names instead of the colour tag (several pods) |
/ |
search, smart-case: case matters only with an upper-case letter |
n / N |
next / previous match |
c |
clear the search, then the buffer |
p |
previous instance of the container, and back (one pod) |
The buffer keeps the last 5000 lines (8 MiB at most), and the tail you ask
for is shared out over the pods. Deployment and service logs look for their pods again every 10 s, so
the pods of a rollout join; kubectl logs -l does not follow new pods.
| Key | Action |
|---|---|
↑ ↓ pgup pgdn (k j) |
scroll |
g / G |
top / bottom |
/ |
search, ignoring case |
n / N |
next / previous match |
r |
fetch again, keeping the scroll position |
esc |
clear the search, then back |
o in describe opens the object's YAML; in the YAML view it fetches it
again. Describe runs kubectl describe with the footer's exact flags and
says in its title who wrote the text (via kubectl v1.36.0 or
built-in).
k4s picks its colours to match the terminal background. Override it with
K4S_THEME or --theme:
K4S_THEME |
Colours |
|---|---|
auto (default) |
follow the terminal background |
dark |
tuned for dark backgrounds |
light |
tuned for light backgrounds |
mono |
none: bold, underline and reverse video only |
high-contrast |
like auto, with text at 7:1 and borders at 3:1 or better |
auto and high-contrast ask the terminal for its background when the TUI
starts and hold the first frame up to 150 ms for the answer; until then
they go by COLORFGBG, else dark. Inside tmux or screen, on TERM=dumb,
under CI or when stdout is not a terminal they do not ask. A terminal
that does not answer gets the dark colours: on a light theme set
K4S_THEME=light (PowerShell: $env:K4S_THEME = "light").
Setting NO_COLOR gives mono; --theme wins over
both K4S_THEME and NO_COLOR. The default colours keep text at 4.5:1 or
better on Solarized Dark and Light, black, white, Campbell, VS Code dark,
Dracula and Gruvbox.
No state is told by colour alone. The status row and the dashboard lead
every state with a glyph, and mono adds bold (△), bold and underline
(▲) and reverse video (the row under the cursor, also marked ›):
| Glyph | Meaning |
|---|---|
● |
ok |
△ |
waiting or degraded; a warning |
▲ |
failing; an error |
○ |
off or absent |
◇ |
worth a look, such as a recent restart |
◆ |
a neutral fact |
· |
minor, such as an evicted pod |
— |
forbidden or unknown |
≈ |
worked out by k4s, or a command that does a little more or less than k4s |
To try k4s against a throwaway cluster, or to hack on it: requires Go 1.26+,
Docker (tested on OrbStack), and kubectl on PATH.
# k3s v1.36 in docker with traefik, servicelb, metrics-server and local-path;
# applies the demo workloads, writes .kube/config, waits until all are Ready
make demo
# run k4s against that cluster
KUBECONFIG=$(pwd)/.kube/config make runmake k3s-down stops the cluster and keeps its state, make k3s-clean wipes
it. make k3s-up only waits for a kubeconfig written by the current k3s
start and a live API, so a file left by an earlier cluster is never used.
K3S_PROFILE picks the cluster shape, e.g. make demo K3S_PROFILE=agents:
K3S_PROFILE |
Cluster |
|---|---|
| (unset) | one server, every bundled component |
minimal |
no traefik, servicelb or metrics-server |
agents |
server + 2 agent nodes |
minimal,agents |
both |
Traefik answers on localhost:18080 / 18443 and the nginx-lb
LoadBalancer Service on localhost:18090 (K4S_HTTP_PORT, K4S_HTTPS_PORT,
K4S_LB_PORT override them).
Kubeconfigs for the ServiceAccounts in deploy/seed/90-restricted-rbac.yaml,
with 24h tokens (rerun the target to refresh). Neither account can read
outside k4s-demo, so kubectl get pods -A is Forbidden.
| Target | Writes | Acts as |
|---|---|---|
make kubeconfig-restricted |
.kube/restricted |
k4s-viewer: built-in view + exec / port-forward in k4s-demo |
make kubeconfig-narrow |
.kube/narrow |
k4s-narrow: pods only (no logs, services, deployments, events) |
make kubeconfig-plugin |
.kube/plugin |
k4s-viewer through an exec credential plugin that logs to stderr and takes 3s |
make kubeconfig-plugin-tty |
.kube/plugin-tty |
the same plugin, but it refuses to run without a terminal |
make kind-up/make kind-down: a vanilla kind cluster, kubeconfig in.kube/kind; seed it withmake seed KUBECONFIG_PATH=$(pwd)/.kube/kind. Skipped when kind is missing.- If your Docker host cannot run the bundled components, use
K3S_PROFILE=minimal, or k3d:k3d cluster create k4s --image rancher/k3s:v1.36.5-k3s1 && k3d kubeconfig get k4s > .kube/config && make seed.
make seed applies the manifests in deploy/seed/:
k4s-demoandk4s-brokennamespacesnginxDeployment (3 replicas) + Service + ConfigMapweb-with-sidecar— pod with two containers for container-switching testslog-spammer— two pods, one log line per second, ideal for multi-log streamingcrash-loop— exercises theCrashLoopBackOffrenderingnamed-port— a Service whose targetPorts are container port names, like Traefik'sinit-crashloop— a failing init container, stuck inInit:CrashLoopBackOffnative-sidecar— a sidecar declared as an init container withrestartPolicy: Alwaysnginx-lb— a LoadBalancer Service served by ServiceLB (<pending>onminimalor kind)- a Secret and a CronJob (every 5 minutes) for read-only browsing
k4s-viewerandk4s-narrowServiceAccounts for the restricted kubeconfigs
Linters and the vulnerability scanner are pinned in a separate module,
tools/go.mod (run as go tool -modfile=tools/go.mod <tool>), so tool
upgrades never move the app's dependencies — no global installs needed. The
tool targets run on the toolchain pinned by go.mod; the first run may
download it (GOTOOLCHAIN=local make lint to opt out).
make fmt # gofmt + goimports (via golangci-lint)
make lint # golangci-lint v2 — errcheck, staticcheck, gosec, revive, gocyclo, ...
make vuln # govulncheck — known CVEs in the dependency graph
make tidy-check # go.mod / go.sum of both modules are tidy
make lines # no non-test .go file over 800 lines
make cover # per-package test coverage
make ci # tidy-check + line cap + vet + lint + race tests
# + idle-cpu harness lint and tests + vulnGitHub Actions runs make ci and cross-builds (linux amd64/arm64/armv7,
darwin, windows) on every push and pull request, plus a weekly scheduled run
to catch new advisories. GitHub disables schedules on public repos after 60
days without activity; re-enable with gh workflow enable ci.
gosec runs as part of make lint and covers the OWASP-aligned checks
(hardcoded credentials, weak crypto, integer overflows, path traversal, ...).
Pushing a v* tag runs .github/workflows/release.yml: GoReleaser
(.goreleaser.yaml) builds the six archives and checksums.txt and uploads
them to that tag's GitHub release, keeping the hand-written release notes.
make build # bin/k4s, version stamped from `git describe`
make snapshot # every release archive into dist/, nothing publishedmake snapshot fetches GoReleaser with go run, so its first run needs
network access.
How the pieces fit together, and why, is in docs/architecture.md.
cmd/k4s/ entry point
internal/
cli/ command-line flags
startup/ kubeconfig lookup, starting namespace, preflight
cluster/ distribution detection (k3s, rke2, EKS, GKE, ...)
and the hints worded for it
k8s/ client-go wrapper: lists, describe, YAML, events,
nodes, log targets and streams, metrics,
port-forward, API error classification
dash/ dashboard data layer: refresh plan, Table lists,
snapshot, severity, verdict, next suggestions
kubectl/ the kubectl command behind every action: argv,
shell quoting, --context/--kubeconfig, runner
sanitize/ text from the cluster made inert before it is drawn
forwards/ port-forward state file + Manager (intents persist
across launches; SPDY sessions are in-process)
logsink/ client-go and plugin output kept off the TUI
version/ build version (ldflags stamp, else module version)
tui/ Bubble Tea root: router, header, footer, history,
prompts, exec, exit recap
views/{dashboard,pods,deployments,services,namespaces,events,nodes,
top,contexts,describe,yaml,logs,forwards}/
async/ tokens, bounded calls, in-flight actions, poller
overlay/ command bar, prompts, confirms, help
status/ the footer's status row
pager/ scroll and search for describe and YAML
viz/ dashboard widgets: meters, sparklines, phase bar
{stale,filter,command,keys,styles,tuitest}/
deploy/ demo manifests, dev scripts, kubeconfig fixtures
tools/ lint and vuln tools in their own go.mod, plus the
idle CPU harness (make idle-cpu)
docker-compose.yml k3s for local development
Makefile build / test / k3s / quality / release targets
.goreleaser.yaml release archives + checksums
Release notes for every version are on the releases page; CHANGELOG.md has the short version, with what comes in 1.2.0 under Unreleased.
Next, in 1.3: delete, scale and rollout restart behind a confirmation that shows the exact command, read-only ConfigMap and Secret views, and a Homebrew tap. Roadmap on https://k4scli.io/changelog/.
MIT — see LICENSE.
