sofka
sofka is a Kubernetes text user interface (TUI), written in Rust. It is built on
kube-rs and ratatui. It uses async
operations for all tasks.
Screenshots
| Pod list + command palette | Namespace switcher | Flux suspend/resume/reconcile menu |
|---|---|---|
![]() |
![]() |
![]() |
Why "sofka"
That is Sophie. Sophie is a Russian Blue cat. She sits behind the monitor and
watches the screen. She watches it constantly, not sometimes. She has the
narrow-eyed look of someone who has seen a pod in CrashLoopBackOff. She sees
each state change. She does not get distracted. She is, in effect, a cluster
watchman that is a cat.
sofka is the Serbian short form of Sophia. Sophia means "wisdom". A good
cluster TUI and a good cat both watch things closely. Both know when something
is wrong.
sofka is a new version of k9s (originally approximately 51,000 lines of Go). sofka is not a line-by-line copy. It keeps the same purpose: a fast cluster navigator that you control with the keyboard. But it uses a different architecture. It uses one generic object pipeline instead of one renderer for each resource kind.
How it differs from k9s
- One generic render pipeline, not one file for each kind. k9s has one Go
file (a struct and a
ColorerFunc) for each resource type that it knows. sofka has one function that changes aDynamicObjectinto cells. It has selected columns for the common kinds and a NAME/AGE fallback for all other kinds. So a CRD that has no renderer still lists, sorts, and filters correctly immediately. - Flux CD has built-in support, not a plugin. Press
tto open a Suspend/Resume/Reconcile-now menu. It works for Kustomizations, HelmReleases, git/helm/oci repositories, buckets, image automation, and notification alerts and receivers. sofka changesspec.suspendand thereconcile.fluxcd.io/requestedAtannotation directly with the Kubernetes API. You do not need thefluxbinary. This function also works with bulk multiselect. - Port-forwards run in the background. A new port-forward does not stop the
TUI for its whole lifetime. Type
:pfto list the active forwards and stop each one while the others continue. sofka stops all forwards automatically on quit and does not leave them orphaned. - Bulk actions with multiselect. Press
spaceto mark rows for delete, kill, or Flux suspend/resume/reconcile across many resources at the same time, not one row at a time. - CRD rows drill into their custom resources, not their YAML. Press
enteron a CustomResourceDefinition. sofka finds its served version and lists the actual objects. - Skins, not one fixed palette. sofka has built-in Catppuccin, Gruvbox,
Solarized, Nord, Dracula, Tokyo Night, One Dark, Rosé Pine, and Monokai
palettes. You select one in the config file, with a hex override for each
swatch. If you configure no skin, sofka detects a light or dark terminal
background automatically. sofka calculates every semantic color (row status,
severity badges, headers, and borders) from the active palette. So one skin
change is consistent everywhere at the same time. Set
background = trueto fill the view with the skin background instead of the terminal background. Use a light per-context skin to make production easy to identify. - A combined row colorer. sofka tints the whole row by status, like k9s (healthy rows, errors, pending, and completed each read as one color). It also shows a separate STATUS badge and colors unusual values in the RESTARTS, CPU, and MEM columns. So a pod that crash-loops or uses too many resources is easy to see in an otherwise uniform row. The warning and critical thresholds for RESTARTS, CPU, MEM (and container request/limit) are configurable for each resource and each context.
- It explains why something is broken. Press
Xto open a deterministic incident view for the selection that uses evidence: the rollout state, the degraded conditions, the blocking pods and their container failure reasons (ImagePullBackOff, CrashLoopBackOff, OOMKilled, unschedulable pods, failed probes), and recent Warning events. It uses no AI and no external service. Press⏎,E, orlto go from a finding to the pod, its events, or its logs. - A session-local timeline. Press
Tto see the state changes that sofka saw for an object during the watch — generation increases, replica and readiness changes, pod phase, restart, and waiting-reason changes, and condition changes — as a timestamped log. sofka calculates it from the watch stream and stores nothing on disk.
Why it is faster
These are specific design choices that you can check. They are not marketing numbers.
- No garbage collector. The Rust ownership model has no garbage-collector pauses. When you watch thousands of pods or custom resources across a large cluster, the in-memory store gets larger. But the redraw latency stays smooth. A runtime with a garbage collector can get unsteady under constant allocation load.
- Batched redraws. The event loop reads every pending watch message before
it triggers one redraw (
while let Ok(m) = rx.try_recv()). A rollout that touches 50 pods costs one render pass, not fifty. - Cached row computation. sofka sorts and fuzzy-filters the visible rows again only when the data or the filter text changes. A dirty flag guards the cache. sofka does not recompute on every frame or every keystroke across the full object set.
- No subprocess overhead on the hot paths. Delete, scale,
suspend/resume/reconcile, and CRD drill-down are direct kube API calls (JSON
merge-patches over the existing client). They do not fork and run a
kubectlorfluxprocess for each action. - Generation-tagged streams. When you change views, sofka does not wait for the old watcher to tear down. A generation tag identifies stale messages, and sofka drops them the instant a newer watch takes over. So navigation never stalls behind a slow stream.
Features
-
Connect to the current kubeconfig context. This includes exec credential plugins (for example, GKE).
-
API discovery of every resource type on the cluster, with k9s-style short aliases (
po,dp,svc,no,cm,sts,ds,ks,hr, and more) and correct precedence (the corepodswins overpods.metrics.k8s.io). -
Live watch of any kind through
kube::runtime::watcher, streamed into an in-memory store. -
Selected columns for common kinds (pods, deployments, replicasets, statefulsets, daemonsets, services, nodes, namespaces, configmaps, secrets, jobs, cronjobs, PVC/PV, ingresses, endpoints, CustomResourceDefinitions), with a NAME/AGE fallback for all other kinds.
-
Custom views — you define columns for any resource in the config file (
[views]). sofka extracts each column with a JSON Pointer and sorts by type (quantities, numbers, and timestamps sort by value). An unknown custom resource uses its CRDadditionalPrinterColumnsautomatically. Presswto show or hide the wide-only columns (kubectl-o wide). -
Live CPU and MEM columns for pods and nodes from the metrics API, with color for unusual values. Nodes also get %CPU and %MEM of allocatable (
status.allocatable— the pool the scheduler hands out), colored by the configurableutilizationthresholds and sortable, so "which node is full" is one glance and oneS. The pod container picker also shows CPU and memory for each container, each container usage as a percent of its request and its limit (-marks an unset request or limit), and the pod QoS class. All of it works correctly when metrics-server is not present. -
Configurable thresholds (
[thresholds]) — the warning and critical values for RESTARTS, CPU, memory, and container request/limit color. There are global defaults, plus per-resource and per-context overrides. -
Explain-unhealthy view (
X/:explain) — a deterministic explanation, with evidence, of why the selected object is unhealthy (the rollout state, degraded conditions, blocking pods and their container failure reasons, and recent Warning events). Press⏎,E, orlto go to the resource, its events, or its logs. -
Session-local timeline (
T/:timeline) — a per-object timestamped log of the state changes that sofka saw during the watch (generation increases, readiness changes, and phase, restart, and condition changes). It has a maximum size and stays only in memory. -
Drill-down navigation with a breadcrumb stack: workload/service → pods, node → its pods, pod → containers, namespace → re-scope, CRD → its custom resources. Press
escto go back. -
Command palette (
:) — fuzzy search over the full resource catalog, the built-in commands (ctx,helm,pulse,xray,explain,timeline,gitops,can-i,journal,debug,debug-clean,bundle,bundle-save,snapshot,snapshots,diff,events,pf,vlogs,rightsize,fleet,skin,reload,config,info), and your saved bookmarks/workspaces together. It also does row filtering (/) with matched-character highlighting: fuzzy text,!textinverse match,-l/-flabel and field selectors (the API server evaluates them on ⏎), and typed column comparisons (status=CrashLoopBackOff,cpu>500m,memory>1Gi,restarts>=5,age<2h). Terms with a space between them use AND. -
Global fuzzy find (
:find <text>) — search object names across the common kinds (workloads, pods, services, config, ingresses, jobs, storage, nodes, namespaces, Flux objects) in every namespace at once, concurrently. Results rank by fuzzy score;⏎jumps straight to the object. When a kind can't be listed (RBAC), the result says it's incomplete instead of pretending otherwise. -
Multiselect (
space) for bulk delete/kill/suspend/resume/reconcile. -
Pulse dashboard (
:pulse) — cluster-health tiles. sofka refreshes them every 5 seconds. -
Xray tree (
:xray) — a hierarchical view from the current kind down through owner references to pods and containers. -
Flux CD controls (
t) — a suspend/resume/reconcile menu that uses native Kubernetes API patches. Press⏎on a HelmRelease to open the revision history of the Helm release it manages (resolved the way helm-controller composesreleaseName/storageNamespace): from there⏎shows a revision's values,ythe rendered manifest,dthe NOTES, andrrolls back — the Flux object and the Helm storage inspector are one keystroke apart. -
CronJob controls (
t) — trigger now (creates a Job from the jobTemplate, likekubectl create job --from), suspend, and resume. -
A native Helm inspector (
:helm/:hm) — sofka decodes Helm's release storage Secrets directly (double base64 → gunzip → JSON, like Helm itself) and shows one row per release at its latest revision, likehelm list. Press⏎on a release for its full revision history (helm history); on a revision,⏎shows the user-supplied values,ythe rendered chart manifest, anddthe NOTES.txt.ron a history row rolls back andctrl-duninstalls — those two shell out to the realhelmbinary; all the inspection is native, nohelmneeded. -
File transfer (
ton a pod, ortin the container picker for one container) — download from or upload to a pod viakubectl cp, run off-thread with a completion flash. Uploads are gated by thetransferguardrail action and read-only mode. -
Background port-forwards (
f/Fto start,:pfto manage), plus saved forwards ([[forwards]]) — named entries that appear in:pfeven while stopped (⏎starts one), with optionalautostart = trueto open them on connect and on matching context switches:[[]] = "argocd" = "svc/argocd-server" # kubectl syntax: pod/…, svc/…, deploy/… = "argocd" = "8080:443" # LOCAL:REMOTE = true # start when sofka connects (default false) = ["home"] # optional: only these contexts -
Plugins — shell-out commands that you define in the config file and bind to keys, scoped for each resource. Keys are full chords (
ctrl-g,alt-x,shift-b,f5). Commands run in the terminal, in a captured popup, or in the background (with a timeout and a maximum output size). A command can need confirmation or carry a dangerous flag. A command can declare itself read-only (mutating = false) to stay usable in--readonly. A command can substitute rich placeholders as separate arguments ($NAME/$NAMESPACE/$CONTEXT/$CLUSTER/$RESOURCE/$GROUP/$VERSION/$KIND/$FILTER). A command can run over every marked row at the same time and report partial failures. -
Bookmarks (
[[bookmarks]]) — saved navigation commands that you bind to a chord and the command palette. One keystroke goes to a resource, and can also change the context or namespace and apply a filter, a sort, and a view. -
Workspaces (
[[workspaces]]) — a named collection of views for one task. Open it, then pressTaborShift-Tabto cycle its views. You stay in the workspace. -
Watch notifications (
:notify) — toggle a notification on the selected object. Sophie watches it so you don't have to: every state change the watch sees (the same transitions the timeline records — rollout progress, readiness, phase, restarts, waiting reasons, conditions) flashes in the status line, rings the terminal bell, and emits a desktop notification. The protocol is configurable ([notify]):osc777(default — rxvt-style title+body, the form Ghostty recommends; also kitty, WezTerm, foot, urxvt) orosc9(iTerm2-style body-only, for iTerm2 and Windows Terminal, which speak only that), plusbothandoff. Terminals ignore protocols they don't speak. Each notify is its own bounded single-object watch, so it keeps firing while you browse other views — "tell me when this rollout finishes" and keep working. Toggle it off with:notifyon the same row; everything is session-local.[] = true # ring the terminal bell = "osc777" # "osc777" | "osc9" | "both" | "off" -
Diff (
:diff) — a unified diff of the live object and itslast-applied-configuration. When that annotation is absent — as it is for every Flux- or Helm-managed object, which nothing everkubectl applys — sofka diffs against the previous revision the session's watch saw instead, so "what just changed?" has an answer on GitOps clusters. sofka keeps the last revision of up to 256 changed objects in memory for this. -
Events (
:events/E) — live Kubernetes Events for the selected object. sofka filters by UID when the UID is available. -
GitOps view (
:gitops/:flux) — for the selected object, the Flux ownership and reconciliation chain: the owning Kustomization/HelmRelease, its source (GitRepository/OCIRepository/HelmRepository) and its applied revision and latest revision, thedependsOnedges, and the ready status. Each item is a finding with evidence that you can⏎to go to. -
Managed-resource mutation warnings — sofka warns you first before you edit, delete, scale, or otherwise change an object that Flux (or another controller) owns. The warning says that the next reconcile will revert your change or recreate the object. So you fix the source instead of fighting the controller.
-
Action-aware authorization (
:can-i) — aSelfSubjectRulesReviewoverview of what you can do in the current namespace, plus:can-i <verb> <resource> [ns]to check a single action before you try it. This is the same answer thatkubectl auth can-igives, inside the TUI. -
Declarative guardrails (
[[guardrails]]) — rules in the config file that match on context/namespace/resource/action globs. A rule can deny a destructive action outright (delete/force-delete/drain/restart/shell/debug/node-debug/transfer— a file upload into a pod), force type-to-confirm (the resource name orcontext/name), or cap bulk operations. So rules like "never delete in prod", "always confirm a prod shell", and "no more than one at a time" are enforced, not remembered. -
Action journal (
:journal/:audit) — a session-local log in memory of every mutating action you did (the action, the target, the context, and the time), newest first. It records identifiers only, never secret input or decoded values, and never writes to disk. -
Ephemeral debug containers (
:debug) — attach a temporary debug container to the selected pod withkubectl debug. sofka prompts for the image (prefilled with the[debug]default). Pressdin the container picker to target the process namespace of one container (--target). Read-only mode and thedebugguardrail control this action. sofka records it in the journal. -
Node debug pods (
:debugon a node) — start a privileged diagnostic pod on the selected node (kubectl debug node/…) that mounts the host filesystem at/hostand joins the host namespaces. sofka shows a preview of exactly that access and needs your confirmation before it creates the pod. Read-only mode and thenode-debugguardrail control this action. sofka records what it started, so:debug-cleancan remove the debugger pods later. -
Diagnostic bundles (
:bundle) — sofka assembles a redacted incident bundle for the selected object into one Markdown document: its (redacted) YAML, the owner, the incident explanation, recent events, the session timeline, bounded recent logs, and a metrics snapshot. sofka always strips Secretdata/stringData, credential-like annotations, andlast-applied-configuration. A manifest lists what the bundle includes and what it withholds. Review the bundle in a preview, then use:bundle-saveto write it to a file. -
Snapshots (
:snapshot) — capture the current table view (its columns and visible rows, with metadata) to a file as an aligned text table, JSON, or YAML. Use:snapshotsto browse the saved captures (newest first, with age), open one in a viewer that is marked stale, and delete one withd. This is different from the one-frame--snapshotCI mode. This is an interactive capture-and-review workflow. -
RBAC-aware palette — sofka hides the resource kinds that you cannot
list. -
Namespace switcher (
n) with pinned favourites (favorite_namespaces, ★) and per-context session recents (·) above the rest, and a context switcher (:ctx). -
YAML view (
y) and describe (d, throughkubectl). -
Logs (
l) — per-container on a pod, or aggregated across all matching pods on a workload/service. In the logs view,/filters: a case-insensitive substring (highlighted), a/regex/, or!to invert. Presszto clear the buffer. Presspto show previous-container logs. The initial tail, the follow buffer, and thesincelookback are configurable ([logs]). sofka parses ANSI color codes from the source app and maps them onto the active skin. It does not print them as literal escapes. -
VictoriaLogs integration (
L/:vlogs) — log history for the selected pod, container, workload, service, or whole namespace from a VictoriaLogs backend: a lookback query and a live tail, in the same logs view. It needs no config: sofka finds the VictoriaLogs service in the cluster automatically and reaches it through the API-server proxy. Or point[providers.logs]at an external URL. It covers restarted and deleted pods, because the backend keeps what the kubelet no longer has. -
Right-sizing (
:rightsize) — when a Prometheus or VictoriaMetrics backend is reachable, sofka estimates right-sized requests for the selected workload from past usage: for each container, the current requests, the P50/P95/P99 CPU and memory over a window, a suggested request (P95 plus headroom), OOM and throttle evidence, and a patch preview. It never mutates. sofka finds the backend in the cluster automatically (or set[providers.metrics]). -
Fleet dashboard (
:fleet) — an opt-in health summary across contexts: connectivity, Kubernetes version, node readiness, unhealthy pods, Flux failures, and the read-only policy for each configured context, side by side. sofka queries only the contexts in[fleet], and gathers each one at the same time with its own timeout, so one slow cluster never blocks the rest. Press⏎to switch to a context. Pressrto refresh. -
Mouse support — the wheel scrolls every view (table, logs, documents, pickers — one notch is three steps of that view's own up/down), clicking a table row selects it, and clicking a column header sorts by it (click again to flip the direction). Set
mouse = falsein the config to keep the terminal's native mouse behavior (text selection) instead; sofka releases the mouse while a suspended command (kubectl exec,$EDITOR) runs. -
Compact mode (
ctrl-e) — collapse the seven-line header and the footer into one info line (kind · count · namespace · context, with a flash and the live indicator). So a tiled or multiplexed pane is almost all table. -
Skinnable — built-in Catppuccin, Gruvbox, Solarized, Nord, Dracula, Tokyo Night, One Dark, Rosé Pine, and Monokai palettes, an auto-detected dark/light default, and per-swatch overrides in the config file.
-
Config file (TOML): aliases, default namespace/resource, favourite namespaces, plugins, bookmarks, workspaces, views, thresholds, log provider, and skin — with per-cluster and per-context overrides and live
:reload. -
Runtime diagnostics (
:info, orsofka --info) — the version and build, the config sources, the live context/cluster/API server, the discovery and Metrics API status, the watch error counts, and the state/snapshot/bundle directories. It prints only identifiers and counts, never credentials, tokens, or Secret values.
Installation
Download from Github
Each GitHub release has prebuilt binaries for macOS (aarch64/x86_64) and Linux (aarch64/x86_64).
Nix
Nix users can run sofka directly. You do not need to install anything:
Cargo
Or build from source (see Development).
macOS: "cannot be opened because the developer cannot be verified"
The release binaries are not signed or notarized yet. So if you download a tarball in a browser and extract it, Gatekeeper refuses to run it. This is expected. It is not a broken build. Clear the quarantine flag one time:
xattr -d com.apple.quarantine sofka
(Or right-click the binary in Finder, select Open, and confirm in the dialog one time.) Signing and notarization are planned for the next release. Then this step is not necessary.
Configuration
$XDG_CONFIG_HOME/sofka/config.toml (or ~/.config/sofka/config.toml):
= "kube-system"
= "deployments"
= false # true disables every mutating action (delete, edit,
# scale, shell, plugins, …); --readonly/--write win
= true # false keeps the terminal's native mouse behavior
# (text selection) instead of scroll/click/sort
# Namespaces pinned to the top of the `n` switcher (★); session recents (·)
# follow them.
= ["kube-system", "monitoring"]
[]
= "deployments"
[]
# name omitted: auto-detects dark/light and picks catppuccin-mocha/-latte.
# Or pick one explicitly: catppuccin-mocha, -latte, -frappe, -macchiato,
# gruvbox-dark, gruvbox-light, nord, dracula, solarized-dark, solarized-light,
# tokyo-night, one-dark, rose-pine, monokai.
= "gruvbox-dark"
= true # fill views with the skin's own background swatch
# (default: false = inherit the terminal background)
[] # optional per-swatch overrides
= "#fb4934"
[[]]
= "ctrl-g" # a key chord: ctrl-/alt-/shift-, function keys, …
= "argocd-sync"
= "argocd"
= ["app", "sync", "$NAME"]
= ["deployments"] # omit for all resources
= true # confirm (showing the command) before running
# mutating = false # allow in --readonly mode (declares it read-only)
# output = "popup" # "terminal" (default) / "popup" / "background"
# shell = true # run via `sh -c` (args stay positional $1, $2, …)
See Plugins below for all plugin options: output modes, placeholders, and bulk operation over marked rows.
Custom views
Define table columns for any resource. This is most useful for custom resources,
because they use the NAME/AGE fallback. sofka keys views by apiVersion/plural
("cert-manager.io/v1/certificates", "v1/pods"), group/plural, bare plural,
or lowercase kind. The most specific key wins.
[]
= "EXPIRES:desc" # initial sort column, ":asc" (default) or ":desc"
# replace = true # replace the curated columns instead of overlaying
[[]]
= "READY"
= "Ready" # the condition *type* name, found wherever it is
= "condition" # in the array — order isn't guaranteed by anything
[[]]
= "EXPIRES"
= "/status/notAfter"
= "time" # rendered as elapsed ("3d4h") / "in 30d"
[[]]
= "ISSUER"
= "/spec/issuerRef/name"
= true # only shown in wide mode (`w`)
path is a JSON Pointer (RFC 6901) into the object as the API serves it:
/metadata/…, /spec/…, /status/…, and array indices like
/spec/ports/0/port. The column type is text (default), status,
number, quantity (500m, 1Gi), time, or condition. Typed columns
sort by value, not by text. For a condition column, path is the condition
type name (Ready, Available, Reconciling…): sofka finds it in
status.conditions by name — never by array index, whose order nothing
guarantees — renders its status (True/False/Unknown), and colors the
row like a status column. The optional width (for fixed columns) and
align (left/center/right) tune the layout. By default, columns overlay
the selected ones: a header that matches replaces it in place, and new columns
go before AGE. sofka skips invalid entries and shows a warning in the app.
They never stop the TUI.
A custom resource that has no explicit view uses its CRD
additionalPrinterColumns automatically (columns with priority > 0 become
wide-only). The canonical condition lookup
(.status.conditions[?(@.type=="Ready")].status — how most CRDs express
their READY printer column) becomes a condition column; other JSONPath
filter or wildcard expressions aren't representable and those columns are
skipped. So most custom resources get useful columns with no configuration.
Thresholds
The warning and critical values behind the RESTARTS/CPU/MEM cell color (and the
request/limit utilization in the container picker) are configurable. Any value
that you do not set keeps the sofka default, so an empty config colors exactly
as before. Global [thresholds] apply everywhere. [thresholds.resources.<key>]
overrides them for each resource (keyed like [views]). Like every section, a
per-cluster or per-context override file can retune them for one context.
Thresholds also apply again live on :reload.
[]
= { = 3, = 10 } # count
= { = "200m", = "1" } # absolute usage
= { = "256Mi", = "1Gi" }
= { = 75, = 90 } # percent of request/limit
[] # per-kind override
= { = 5, = 20 }
You can omit either bound of a band to disable that level. warn is peach.
critical is red.
Plugins
[[plugins]] binds a shell-out command to a key. key is a chord: a single
character ("g"), a modifier combination ("ctrl-g", "alt-x", "shift-b"),
or a function or named key ("f5", "ctrl-f2"). A built-in key wins over a
plugin on the same chord.
[[]]
= "shift-y"
= "yaml-summary"
= "kubectl"
= ["get", "$RESOURCE", "$NAME", "-n", "$NAMESPACE", "-o", "yaml"]
= ["pods", "deployments"] # omit for all resources
= false # read-only: still runs under --readonly
= "popup" # captured into a scrollable view (see below)
[[]]
= "ctrl-x"
= "restart-rollout"
= "kubectl"
= ["rollout", "restart", "$RESOURCE/$NAME", "-n", "$NAMESPACE"]
= ["deployments"]
= true # confirm (showing the exact command) first
- Placeholders are substituted as whole arguments. sofka never splices them
into a shell string. They are
$NAME,$NAMESPACE/$NS,$CONTEXT,$CLUSTER,$RESOURCE(plural),$GROUP,$VERSION,$KIND,$FILTER. output:terminal(default, interactive — it suspends the TUI),popup(captured off-thread into a scrollable view), orbackground(detached — a notification flashes when it completes).popupandbackgroundobeytimeout("30s", default) and bound the captured output.mutating(defaulttrue): read-only mode blocks a mutating plugin. Set it tofalseto allow a known read-only one.confirm/dangerous: prompt before the command runs, and show the exact executable and arguments.dangerousalso shows ⚠.shell = true: opt intosh -c. sofka still passes placeholders as positional parameters ($1,$2, …). It never interpolates them into the script.- Bulk: with rows marked (
space), apopuporbackgroundplugin runs over every marked row and reports partial failures. An interactiveterminalplugin cannot run over a set and refuses a marked run.
For an invalid value (a bad chord, an unknown output, or a malformed
timeout), sofka disables just that plugin or uses the default, and shows a
warning in :config. Plugins appear in ? help with their chord and scope.
Bookmarks
[[bookmarks]] are saved navigation commands. One keystroke goes to a resource,
and can also change the context or namespace and apply a filter, a sort, and a
view. An optional key chord triggers a bookmark. Bookmarks are always in the
command palette (★, ranked above resources).
[[]]
= "shift-1" # optional
= "Prod API failures"
= "pods"
= "prod-eu" # optional: switched first
= "checkout" # optional; all/* = all namespaces
= "status!=Running -l app=api" # optional, same syntax as `/`
= "RESTARTS:desc" # optional: COLUMN[:asc|:desc]
= "xray" # optional: xray | pulse
Workspaces
[[workspaces]] group several views into a named set for one task (checkout
ops, a cluster upgrade, or cert renewal). Open one with a chord or the palette
(▦). sofka changes its optional context one time and shows the first view.
Press Tab or Shift-Tab to cycle the other views. You stay in the
workspace.
[[]]
= "ctrl-w"
= "Checkout ops"
= "prod-eu" # optional: switched once on open
[[]]
= "API pods"
= "pods"
= "checkout"
= "-l app=api"
= "RESTARTS:desc"
[[]]
= "Ingress"
= "ingresses"
= "checkout"
Guardrails
[[guardrails]] make rules like "never delete in prod", "always confirm
drains", and "no more than 5 at a time" into enforced rules. You do not have to
remember them. Each rule matches on contexts, namespaces, resources, and
actions globs (all optional; an omitted glob matches everything). Then it
applies the strictest of these: deny (block the action), confirmation (type
to confirm), and max_bulk (a maximum number of rows for one action). The
gated actions are the destructive verbs that sofka does directly — delete,
force-delete, drain, restart, shell (exec), debug, node-debug, and
transfer (a file upload into a pod).
The first rule that matches wins. sofka shows the reason when a rule fires.
[[]]
= ["*prod*"]
= ["delete", "force-delete", "drain"]
= true
= "Destructive actions on prod go through GitOps, not the TUI."
[[]]
= ["*prod*"]
= ["shell"]
# "type-resource-name" | "type-context-name"; any other value = a plain y/N
= "type-context-name"
= "Confirm the exact pod before shelling into prod."
[[]]
= ["kube-system"]
= ["delete"]
= 1 # no bulk deletes in kube-system
Debug containers and pods
:debug on a pod attaches a temporary ephemeral debug container with
kubectl debug. sofka prompts for the image (prefilled with image below). An
empty command starts an interactive shell (bash if the image has it, or else
sh), like the pod shell. Press d in the container picker to set
--target=<container>, so the debug container shares the process namespace of
that container. The ephemeral container stays on the pod until the pod is
recreated. Kubernetes cannot remove it, so sofka has nothing to clean up.
:debug on a node starts a privileged diagnostic pod on it
(kubectl debug node/<node>, image node_image in node_namespace, optional
node_profile). This pod mounts the host filesystem at /host and joins the
host PID, network, and IPC namespaces. So sofka shows a preview of exactly that
access and makes you confirm before it creates the pod. sofka records the node
debuggers that it starts this session. :debug-clean deletes them (matched by
the node-debugger-* name and the node). kubectl leaves the pod after you exit,
so clean up when you finish.
[]
= "nicolaka/netshoot:latest" # ephemeral (in-pod) debug image
= ["bash"] # entrypoint; omit for an interactive shell
= "nicolaka/netshoot:latest" # node debug pod image
= "default" # namespace the node debugger lands in
= "sysadmin" # kubectl debug --profile (optional)
Read-only mode and guardrails disable both actions: the debug action for
pods, and the node-debug action for nodes.
Diagnostic bundles
:bundle assembles a redacted incident bundle for the selected object — its
YAML, the owner, the incident explanation, recent events, the session timeline,
bounded recent logs, and a metrics snapshot — into one Markdown document. It
helps you hand off an incident between application and platform teams. sofka
gathers it off-thread and shows it in a preview. Then :bundle-save writes it
to a temp file.
sofka always redacts these items: Secret data/stringData values, any
credential-like annotation (a key that contains token, password, secret,
apikey, credential, and similar), and last-applied-configuration. It
replaces them with a placeholder. It drops managedFields. It flags env vars
that come from Secrets (their values are references, not literals). Every
bundle has a manifest of exactly what it includes and what it withholds.
[]
= false # replace context/cluster identity with placeholders
= 200 # max recent log lines per pod
= 3 # cap how many pods contribute logs
Snapshots
:snapshot captures the current table view — its columns and visible rows, plus
metadata (context, cluster, namespace, resource, filter, and timestamp) — to a
file. An optional argument sets the format: text (default; an aligned table
with a header block), json, or yaml. sofka writes the files to
$XDG_STATE_HOME/sofka/snapshots (or ~/.local/state/sofka/snapshots).
:snapshots browses the saved captures, newest first with their age. Press ⏎
to open one in a viewer with a staleness banner (it is a point-in-time
capture). Press d to delete the highlighted file. This is different from the
one-frame --snapshot CI flag. This is an interactive capture-and-review
workflow.
Log controls
The kubelet logs view (l) keeps a bounded follow buffer. Tune the initial
tail, the buffer size, and an optional since lookback:
[]
= 300 # initial lines fetched per stream (kubectl --tail)
= 5000 # max lines kept while following (oldest dropped)
= "1h" # optional: only logs newer than this — replaces tail
= false # open log views fullscreen (F toggles per session)
In the view, / filters with a case-insensitive substring, a /regex/, or a
leading ! to invert (keep the lines that do not match). sofka flags a
malformed regex instead of hiding everything. Press z to clear the on-screen
buffer (the live stream continues to append). A pod streams the logs of every
container at the same time.
Fleet dashboard
:fleet summarizes several clusters side by side. You do not switch through
them. It is opt-in: sofka queries only the kubeconfig contexts that you
list.
[]
= ["prod-eu", "prod-us", "staging"]
sofka gathers each context at the same time (bounded, with a per-context
timeout). So an unreachable or slow cluster shows an error on its own row and
does not block the others. Each row shows connectivity, Kubernetes version,
node readiness, the unhealthy pod count, the Flux Ready=False failures, and
the resolved read-only policy. Press ⏎ to switch to the highlighted context
(through the normal context-switch path). Press r to gather again. sofka
keeps only these non-sensitive summaries in memory.
Right-sizing (metrics provider)
:rightsize on a workload (or pod) estimates right-sized requests from past
usage in a Prometheus-compatible backend — Prometheus or VictoriaMetrics,
which share the query API. For each container it shows the current requests, the
P50/P95/P99 CPU and memory over the window, a suggested request (P95 plus
headroom), OOM and throttle evidence, and a strategic-merge patch preview
(press c to copy). It never mutates. Apply the patch yourself with
kubectl patch if you agree.
It needs no config by default: with no [providers.metrics] section, sofka
finds a Prometheus or VictoriaMetrics query Service in the cluster
automatically (by well-known labels) and reaches it through the API-server
proxy, like the log provider. Configure it only to point at an external
endpoint or to tune the window or headroom:
[]
= "prometheus" # or "victoriametrics" (same query API)
= "https://prom.example.com" # omit to autodiscover in-cluster
= "7d" # lookback for the P50/P95/P99 quantiles
= "5m" # subquery resolution for the CPU rate()
= 15 # percent added over P95 for the suggestion
[] # optional
= "Bearer <token>"
It uses the standard cAdvisor metric names
(container_cpu_usage_seconds_total, container_memory_working_set_bytes,
container_oom_events_total, container_cpu_cfs_throttled_periods_total).
VictoriaMetrics cluster mode (vmselect) needs a tenant path in the url.
Single-node VM and Prometheus serve the API at the root and autodiscover
correctly.
Log provider (VictoriaLogs)
L (or :vlogs) opens log history for the selection from a VictoriaLogs
backend instead of the kubelet. With no configuration, sofka finds the
VictoriaLogs Service in the cluster by its well-known labels (Helm charts and
the VictoriaMetrics operator). It queries the service through the Kubernetes
API-server service proxy and reuses your kubeconfig credentials. Configure it
only to point at an external endpoint or to change the defaults:
[]
= "victorialogs"
= "https://vlogs.example.com" # omit to autodiscover in-cluster
= "1h" # initial query window (s/m/h/d)
= 300 # lines fetched by the initial query
[] # optional, sent with every request
= "Bearer <token>"
# Field names as ingested by your log shipper. Omit this section to let
# sofka detect the convention from the backend's stream fields — vector,
# fluentd, fluent-bit, OpenTelemetry, and bare namespace/pod/container
# names are recognized. Configure only for exotic pipelines.
[]
= "kubernetes.pod_namespace"
= "kubernetes.pod_name"
= "kubernetes.container_name"
Like every section, [providers.logs] can live in a per-cluster or per-context
override file. So each cluster can use its own backend.
Per-cluster / per-context overrides
You can override any option for a specific cluster or kubeconfig context, like
k9s. Put partial config files under clusters/:
~/.config/sofka/
├── config.toml # base, applies everywhere
└── clusters/
└── prod-cluster/ # kubeconfig *cluster* name
├── config.toml # every context on prod-cluster
└── prod-admin/ # kubeconfig *context* name
└── config.toml # that context only
Overrides merge over the base config (the cluster level first, then the context
level). Tables like [aliases] and [skin.colors] merge key by key. Everything
else — strings, booleans, and arrays like [[plugins]] — replaces the base
value. Directory names are the kubeconfig names. sofka replaces any character
that is not a letter, a digit, ., _, or - with -. So an EKS context
arn:aws:eks:eu-west-1:123456789:cluster/prod becomes the directory
arn-aws-eks-eu-west-1-123456789-cluster-prod.
# clusters/prod-cluster/config.toml — make prod unmistakable and hands-off
= true
[]
= "catppuccin-latte"
= true
A skin in an override sets the colors for that context. A context that has no
skin keeps the session skin (the config skin.name, the auto-detected default,
or your last :skin choice). sofka reads the overrides again on every :ctx
switch. So edits apply without a restart.
Headless modes (no TTY required)
sofka --check # connect, run discovery, print a summary, exit
sofka pods --snapshot # render one frame of a resource view to stdout
sofka dp -A --snapshot # deployments, all namespaces
sofka --info # version/build, config sources, dirs, kubeconfig context (no connection)
These also work as CI smoke tests. --info prints only identifiers and paths,
never credentials, tokens, or Secret values.
Usage
sofka [RESOURCE] [-n NAMESPACE] [-A] [--readonly | --write]
RESOURCE resource to open (alias/plural/kind), default: pods
-n, --namespace namespace to start in
-A, --all-namespaces
--readonly disable every mutating action for the session
--write force write mode, overriding any config `readonly`
--readonly and --write set the mode for the whole session. They win over the
config readonly option — and over per-cluster and per-context overrides — on
every :ctx switch. With no flag, a switch into a context whose config sets
readonly = true enables read-only mode (shown as [read-only] in the header).
A switch away from it restores write mode.
Keys
| Key | Action |
|---|---|
:<resource> |
command palette - fuzzy over kinds and built-in commands |
:<resource> <ns> |
switch kind and namespace at once (:deploy social; all/* = all namespaces; the namespace tab-completes) |
[ / ] |
view history - back / forward through visited kind+namespace views |
Tab / shift-Tab |
cycle views of the active workspace (when one is open) |
enter |
drill down (workload/svc → pods, node → its pods, pod → containers, ns → re-scope, CRD → its resources) |
esc |
go back / pop the view stack / clear filter / clear marks |
j/k, ↓/↑, g/G |
navigate |
S / I |
sort-column picker (fuzzy; ⏎ on the active column inverts) / invert sort direction |
ctrl-e |
compact mode: collapse the header + footer (for tiled/multiplexed panes) |
space |
mark/unmark row for bulk actions |
/ |
filter: fuzzy text · !inverse · -l/-f selectors (server-side on ⏎) · status=X cpu>500m age<2h |
n / 0 |
namespace switcher / all namespaces |
shift-j |
jump to owner/controller |
o |
show the node hosting the selected pod |
ctrl-r |
refresh the watch |
y / d / E |
view YAML / describe (kubectl) / live events |
x |
secrets: show data base64-decoded (as stringData) |
X / T |
explain why the selection is unhealthy / session-local state-change timeline |
:gitops / :flux |
Flux owner, source, revisions & reconciliation chain for the selection (⏎ to jump) |
:can-i / :can-i <verb> <resource> [ns] |
what you can do here / check a single action (SelfSubjectAccessReview) |
:journal / :audit |
session-local log of the mutating actions you've taken |
:rightsize |
historical right-sizing: P50/P95/P99 usage → suggested requests + patch preview (needs a metrics backend) |
:ctx / :ctx <name> |
context switcher popup / switch directly (the name tab-completes) |
:helm |
Helm releases (native storage-Secret decode): ⏎ history → values · y manifest · d notes · r rollback |
:fleet |
cross-context health dashboard (opt-in [fleet] contexts; ⏎ switches, r refreshes) |
:skin |
switch the color skin live (:skin gruvbox-dark applies directly) |
:reload / :config / :info |
reload config from disk · config sources + warnings · runtime diagnostics |
l / p |
logs (workload = all matching pods) / previous-container logs |
c |
copy resource name to clipboard |
e |
edit in $EDITOR (kubectl edit) |
s |
shell into pod / scale a workload (context-dependent) |
a |
attach to pod |
:debug |
pod: ephemeral debug container (d in the picker targets one) · node: privileged debug pod (previewed + confirmed) |
:debug-clean |
delete the node debugger pods launched this session |
:bundle / :bundle-save |
assemble a redacted diagnostic bundle for the selection · write the previewed bundle to a file |
:snapshot [text|json|yaml] / :snapshots |
capture the current view to a file · browse, open, and delete saved snapshots |
i |
set container image |
r |
rollout restart (workloads) / force-sync (ExternalSecrets/PushSecrets) / refresh (elsewhere) |
f / shift-f |
port-forward (pods/services) - runs in the background |
t |
Flux: suspend/resume/reconcile menu · CronJobs: trigger/suspend/resume · pods: file transfer (kubectl cp) |
C / U / D |
nodes: cordon / uncordon / drain |
ctrl-d / ctrl-k |
delete / force-delete (marked rows, or current); in confirm: f toggles force, c cycles cascade (background → foreground → orphan) |
:q, ctrl-c |
quit |
? |
help |
| (config) | plugin / bookmark / workspace key chords — ctrl-/alt-/shift-/fN; listed in ? help |
Logs view: / filter (substring · /regex/ · !invert) · s/f
autoscroll · w wrap · t timestamps · x stop/resume stream · z clear
buffer · c copy buffer · ctrl-s save to file · F fullscreen (no chrome,
clean text selection) · 0–5 time anchors (tail · 1m · 5m · 15m · 30m ·
1h) · T provider lookback (VictoriaLogs views) · esc back. The newest
line anchors to the bottom of the viewport.
Text inputs (palette, filters, prompts): ctrl-u clears the line,
ctrl-w / alt-⌫ delete the previous word.
Document views (YAML, describe, diff, events): / searches like vim. The
whole document stays on screen, and sofka highlights every match. Press n or
N to go to the next or previous match. Press w to wrap. Press c to copy
the document. Press esc to back out (the first press clears an active search).
In the ? help panel, / filters instead and narrows to the matching
keybinds.
Explain view (X): press j or k to move. Press ⏎ to go to the
resource behind a finding (a blocking pod). Press E for its events. Press l
for its logs. Press r to gather again. Press esc to go back. A finding that
you can go into has a trailing →.
Interactive actions (e, s for shell, a) suspend the TUI and shell out to
kubectl. Delete, scale, restart, set-image, suspend, resume, reconcile, and
port-forward go through the kube API (or a backgrounded process) directly.
Architecture
main.rs CLI (clap), terminal lifecycle, the async select! event loop,
and the --check / --snapshot headless modes.
app.rs All application state + input handling (a mode state machine:
Table / Command / Filter / Detail / Logs / FluxMenu /
PortForwards / Help / Namespaces / …), split into app/*.rs
(plugins, bookmarks, workspaces, navigation, …). Spawns watch/
log/port-forward tasks.
k8s.rs Cluster connect, API discovery, alias registry + group-priority
resolution, watch-task spawning, namespace listing.
keys.rs Key-chord parsing + matching (ctrl-/alt-/shift-, function keys)
for plugin, bookmark, and workspace bindings, with unit tests.
store.rs In-memory resource store + the Msg enum that watch tasks send to
the UI (generation-tagged so stale streams are dropped).
columns.rs Per-kind column definitions and cell extraction from
DynamicObjects (the "render" layer), with unit tests.
thresholds.rs Configurable RESTARTS/CPU/MEM/utilization coloring bands
(global + per-resource), compiled from config, with unit tests.
explain.rs Deterministic "why is this unhealthy?" analysis — pure, turns
an object + its pods + events into ranked findings, unit-tested.
timeline.rs Session-local per-object state-change history diffed from the
watch stream (pure transition logic, unit-tested).
ui.rs All ratatui rendering: header, table, scrollable views, popups,
status bar.
theme.rs Palette + semantic styles, skin resolution.
Data flow: watcher tasks push generation-tagged Msgs over an
mpsc::UnboundedSender. The main tokio::select! loop folds them into the
Store. It batches any other queued updates before it redraws. It shares that
same loop with terminal input and a 1s tick (age columns and dead port-forward
reaping). So the UI never blocks on the network.
Development
cargo run -- pods # run against current context
cargo test # unit tests (no cluster required)
cargo clippy --all-targets # lints (clean)
Release
After you merge the release-ready changes to main, run one of:
just release-patch
just release-minor
just release-major
The recipe switches to a clean, up-to-date main. It bumps Cargo.toml and
Cargo.lock. It commits and pushes the version bump. Then it creates the GitHub
Release. The release workflow runs from that published release. It uploads the
platform binaries, publishes to crates.io, and warms the Nix cache.
Future roadmap
Milestone 1: power-user foundation
- Custom columns and view overlays.
- CRD
additionalPrinterColumnssupport. - Structured filter parser.
- Server-side label and field selectors.
- Config reload and validation view.
- Wide/narrow column visibility.
Milestone 2: actionable health
- Container metrics.
- Request/limit percentages and QoS.
- Configurable thresholds.
- Explain-unhealthy view.
- Direct evidence navigation.
- Initial session-local timeline.
Milestone 3: extensibility and repeatable workflows
- Modifier-aware hotkeys.
- Rich plugin execution and output modes.
- Bulk plugins.
- Bookmarks and saved queries.
- Operational workspaces.
- Namespace favourites and recents.
Milestone 4: GitOps and safety
- Flux ownership and dependency navigation.
- Revision and reconciliation-chain visibility.
- Managed-resource mutation warnings.
- Action-aware authorization checks.
- Declarative guardrails.
- Local action journal.
Milestone 5: debugging and collaboration
- Ephemeral container workflow.
- Node debug pod workflow.
- Redacted diagnostic bundles.
- Screen dumps and structured snapshots.
- Runtime diagnostics. (Structured application logging is still to come.)
- Richer log controls.
Milestone 6: fleet and integrations
- Opt-in cross-context health dashboard (
:fleet). - Historical metrics provider interface (Prometheus/VictoriaMetrics: autodiscovery or configured URL).
- Log provider interface (VictoriaLogs: autodiscovery or configured URL).
- Trace provider interface.
- Extended relationship graph.
- Vulnerability scanner integration.
- Historical right-sizing recommendations (
:rightsize).
Milestone 7: distribution polish
- Signed and notarized macOS releases.
- Homebrew distribution.
- Checksums, attestations, and SBOM.
- Evaluate Windows support.
- Document a Kubernetes compatibility matrix.
License
Dual-licensed under MIT or Apache-2.0, at your option. This is the standard for the Rust ecosystem.


