The full list. For how sofka compares to k9s, see vs k9s.
sofka completion <shell> generates CLI completion scripts for Bash, Zsh,
Fish, Elvish, and PowerShell. Tab completes contexts, namespaces, resource types,
local paths, and plugin IDs and versions. Cluster queries use earlier CLI options
and have a time limit. Generation does not need a cluster connection.
See shell setup instructions.
sofka import k9s converts a k9s setup into sofka configuration. It reads the
k9s directory the way k9s does: $K9S_CONFIG_DIR, then $XDG_CONFIG_HOME/k9s,
then the platform default (~/.config/k9s on Linux,
~/Library/Application Support/k9s on macOS, %LOCALAPPDATA%\k9s on Windows).
Use --from DIR to read another directory. --dry-run prints the files
without writing them.
| k9s | sofka |
|---|---|
aliases.yaml |
aliases, or bookmarks for destinations |
plugins.yaml, plugins/ |
plugins, merged by name |
hotkeys.yaml |
bookmarks with the same keys |
views.yaml |
views with replace = true |
config.yaml |
top-level settings, logs, thresholds |
clusters/<cluster>/<context>/ |
the matching clusters/ override file |
From config.yaml, the import reads readOnly, defaultView, ui.skin,
ui.enableMouse, ui.headless, ui.defaultsToFullScreen, logger, and the
CPU and memory thresholds. An alias such as web: pods default app=web
names a destination, so it becomes a bookmark.
Global settings go to conf.d/00-k9s.yaml. Each k9s context with a read-only
flag, skin, locked favorite namespaces, aliases, plugins, or hotkeys gets an
override file. Contexts that only hold k9s state, such as the last namespace,
get none. k9s rewrites unlocked favorites with recently used namespaces, so
they are imported only with lockFavorites: true.
k9s settings that still have their k9s default values are not imported.
The import does not change your own files:
- A setting your base config, or a drop-in that sorts before
00-k9s.yaml, already sets keeps your value, and the report lists it. Your bookmarks stay next to the imported ones, including in contexts with their own hotkeys. - A file the importer did not write is never replaced. A context that already has a sofka override file is reported instead.
- Running the import again requires
--force, also with--dry-run. It replaces the files an earlier import wrote and removes those the new import no longer produces. If a k9s file cannot be read, a re-import stops without changing anything. - Every file is validated before any file is written. If sofka would reject one, nothing is written.
Plugin placeholders are rewritten to sofka names: $RESOURCE_NAME becomes
$RESOURCE, $RESOURCE_GROUP becomes $GROUP, $RESOURCE_VERSION becomes
$VERSION, and ${NAME} or $name becomes $NAME. A plugin marked
dangerous in k9s is hidden in read-only mode, so it imports as
mutating = true; other plugins import as mutating = false. k9s
Shift-1 to Shift-0 keys become the US-layout characters ! to ).
The report lists everything that was not imported, with the reason. These cannot be translated:
- Plugins that read table cells (
$COL-<NAME>), use inverted placeholders ($!NAME), usepipes, or run only in the containers view ($POD). - Plugin inputs used inside a longer string. sofka passes an input as a whole
argument (
${input.NAME}). - View columns whose JSONPath uses filters, wildcards, or slices.
- k9s skins without a matching sofka skin,
shellPod,imageScans, andportForwardAddress.
The importer checks the result with sofka's own config validation. It reports
plugin and hotkey keys that a sofka built-in key takes first, with your own
[keys] bindings applied. Rebind the built-in under [keys]
or change the imported key.
Press D on a node to set options for the current node or marked nodes. The form
shows the targets and requires confirmation before it sends mutation requests.
Read-only mode and configured confirmation rules still apply.
- Ignore DaemonSets starts on. DaemonSet pods stay on the node. Turn this off to block drain when an eligible DaemonSet pod is present.
- Force starts off. Turn it on to permit pods without a live controller. Pod replacement is not assured. Force does not change the grace period or bypass PodDisruptionBudgets. Without Force, sofka must be able to read each pod controller and verify its UID.
- Delete emptyDir data starts off. Turn it on to permit removal of pods with emptyDir volumes and loss of their local data.
- Disable eviction starts off. Turn it on to use pod deletion and bypass PodDisruptionBudgets. Failed eviction requests never cause an automatic switch to deletion. Eviction and deletion requests both use pod UID checks.
- Grace period is empty by default and uses each pod's setting. Enter a nonnegative whole number of seconds to override it. Zero requests immediate termination.
- Timeout is empty or
0for unlimited waiting. Use durations such as30s,5m, or1h30m. One deadline covers all selected nodes, including requests, retries, and waiting. Individual API requests have a 30-second limit.
Use Tab or the arrow keys to select a field, Space to toggle a checkbox, and
text input to edit a duration. Backspace removes a character; Ctrl-U clears
the field. Enter reviews the options. PgUp and PgDn scroll the form,
confirmation, and progress. Risk options reset to off for each new operation.
Nodes are processed in name order, one at a time. For each node, sofka cordons it, lists its pods, and checks all eligible pods before the first pod removal. Mirror pods and completed pods are excluded. Pods already terminating are included when checking completion. Temporary eviction failures, including PodDisruptionBudget rejection, are retried with delays from 1 to 10 seconds. Permanent API failures stop the operation. Progress shows the current node, remaining pod count, and retry reason.
Sofka reports a node as drained only after its eligible pods are gone. On failure, timeout, or cancellation, it stops before starting another node. The result lists completed, incomplete, and unstarted nodes and nodes that remain cordoned.
During the operation, Esc or Ctrl-C cancels requests, retries, and waiting.
Accepted requests cannot be reversed. Nodes are not automatically uncordoned.
If cancellation or failure interrupts a cordon request, check that node's state:
the server may have accepted the request. Press Enter or Esc to close the
result. Navigation stays disabled until the operation ends.
Persistent defaults, saved profiles, arbitrary kubectl arguments, pod selectors, concurrent drains, and full kubectl drain parity are outside this feature.
The built-in HTTPRoute view shows a ROUTES column with paths from all rules
and matches, in source order. Each entry includes its match type, such as
Prefix /api, Exact /health, or RegularExpression /v[0-9]+. Duplicate type
and path pairs appear once. Rules without matches and matches without a path
show Prefix /. A route without rules shows <none>.
The column uses the existing table width limits. Press y to see the full
object, including method, header, and query matches. The path summary does not
include those conditions. Row filters can search the route paths.
-
Container details show readiness, state or failure reason, and restart count beside the existing resource columns. The selected container's details show its type, full image reference, declared ports, and configured startup, readiness, and liveness probes. Images and ports wrap to fit the popup. Probe labels show configuration, not the current probe result. Regular, init, native sidecar, and ephemeral containers are included. The pod's default container (the
kubectl.kubernetes.io/default-containerannotation, else the first container) is listed first and selected when the picker opens; the rest are sorted by name. Pod watch updates refresh the details and keep the selected container by name. Missing status values show-orUnknown. If the selected container disappears, its selection clears. Name, state, and restarts have priority on narrow terminals. Resource percentages hide first, then usage and readiness columns. The popup uses fixed columns. -
Container trends show CPU and memory for the selected container on wide screens. Each chart covers the last five minutes in five-second bins, with the newest sample on the right. The scale is zero to the largest visible value. A dot marks a missing sample; measured zero has an empty bar. History starts while the container is selected and keeps at most 60 samples. It clears on a selection or view change, context change, or pod replacement. It uses the existing metrics polls and is not saved between sessions. The table keeps the current values; the charts show their history.
-
Node trends - the
node-cpu-trendandnode-memory-trendmetric sources show five minutes of node CPU and memory as a bar chart in a table column, scaled to allocatable. They are opt-in through[views]. See Views. -
Scroll position appears on the borders of long resource tables, document views, logs, and pickers. Tables and unwrapped documents also show horizontal position. Wrapped views use display rows. Scrollbars use thin lines. They appear during keyboard or mouse scrolling and hide after 700 ms without scrolling. They stay hidden when content fits. Fullscreen logs and documents keep their borderless layout for text selection.
-
Fullscreen documents -
Ftoggles the full terminal area for YAML, decoded Secret, describe, diff, events, and plugin popup output. Borders, scrollbars, the application header, status line, and key hints are hidden. The title and active search or command prompt remain visible. Search, scrolling, wrapping, copying, and refresh still work. The document setting is kept for the current session, including new documents, and is separate from the Logs setting. -
Document wrapping - set
detail_wrap = trueto enable line wrapping at startup. Thewkey changes wrapping for the session. New documents, reloads, and context switches keep the selected setting. -
Terminal title shows
sofka: <context>/<namespace>and follows navigation. The namespace isallwhen all namespaces are selected. Setterminal_title = falseto disable title changes. Sofka clears the title on exit. -
Compact startup - set
compact_mode = trueto start with a one-line header and no footer.Ctrl-Etoggles the layout for the session; reloads and context switches preserve it. -
Optional header - set
hide_header = truein the configuration to hide the header and logo, including the one-line header in compact mode. -
Connect to the current kubeconfig context, including exec credential plugins (GKE, EKS, and friends).
-
MFA prompts from exec plugins - a plugin that asks for input, such as
aws eks get-tokenwith an MFA profile, prompts on the terminal at startup. Later, sofka offers to suspend and run it. See exec plugins that ask for input. -
Optional TLS session resumption workaround through
--no-tls-resumptionfor clusters that reject resumed connections with HTTP 401. The default is unchanged. See TLS session resumption. -
Optional v1 client certificates through
--allow-v1-client-cert, disabled by default. See certificate compatibility. -
Teleport local proxy certificates work when the server certificate exactly matches a configured CA. Hostname, date, usage, and TLS signature checks remain enabled. See proxy certificates.
-
API discovery of every resource type on the cluster, with k9s-style short aliases (
po,dp,svc,no,cm,sts,ds,ks,hr, …) and correct precedence - corepodswins overpods.metrics.k8s.io. Discovered short names also work for custom resources, such as:mdfor MachineDeployments. Exact aliases appear before fuzzy resource matches. Resource names and built-in aliases take priority over discovered short names. Shared short names use group priority, then alphabetical group and resource order. User aliases override discovered aliases. Discovery runs at connect. When a:lookup matches no known kind, sofka runs discovery again in the background and retries the lookup, so a CRD installed while sofka runs opens without a restart. It does this at most once every 5 seconds; the usual error shows if the kind is still unknown, with a note when discovery could not read an API group. Sofka can connect when it cannot read one API group. Examples: the extension API server is down, or it sends anapiVersionthat is notv1. Sofka does not load that group. It shows a warning at startup and a flash on the first screen.:infoshows the group and the reason. -
Namespace switcher (
n) opens with the cursor on the active namespace.currentmarks the active namespace, including<all>.context defaultmarks the kubeconfig namespace. Both labels can apply to one row and stay visible when you filter the list. The active and context default namespaces are available even if namespace listing is restricted. Favourites, recent namespaces, and shortcuts keep their existing order. Startup rules do not change. -
Namespace patterns:
:ns *-cronsselects names that end with-crons.*matches zero or more characters, and?matches one character. Patterns match the full name and are case-sensitive. A bare*still selects all namespaces. This is wildcard syntax, not regular expression syntax. sofka lists namespaces, then watches resources only in matching namespaces. Namespace discovery requires permission to list namespaces. If discovery fails or no names match, the current selection stays active. If a saved pattern cannot be resolved at startup or after a context switch, the pattern stays selected with an empty view and anunresolvedlabel. Pressctrl-rto retry or select an exact namespace. The header shows the pattern and match count. Watch errors identify the namespace and mark results as incomplete. Each namespace has separate watch reset state. Run the command again or pressctrl-rto update the set. New namespaces are not added automatically. Each match needs a separate watch, so a large match count can increase API load. History and resource navigation reuse the resolved set. Metrics and Xray use that set too. CRD columns remain available; server Table fallback is disabled for patterns. Select one namespace for:can-i. Select one namespace or all namespaces for context plugins and PVC helper cleanup. Object actions use the selected object's namespace. -
Namespace commands:
:ns <name>changes namespace and keeps the current resource view, as the namespace switcher does.:namespaceand:namespacesaccept the same argument;alland*select all namespaces. From the Namespaces list, the command returns to the previous view, or opens Pods if there is no previous view. Other cluster-scoped views stay open; the namespace selection applies to the next namespaced resource view. Filters follow the namespace switcher's rules, and resource ownership scope is cleared. Without an argument,:nsopens the Namespaces list. Enter on a namespace opens its Pods list. Esc returns to the Namespaces list and restores its filter and selected row. -
Live watch of any kind through
kube::runtime::watcher, streamed into an in-memory store. Watch requests use uncompressed responses to avoid gzip stream errors. List requests retain gzip compression. TCP keepalive fails connections to a peer that stopped answering, and the watch restarts on its own after the machine wakes from sleep on macOS and Linux. -
Curated columns for common kinds (pods, deployments, replicasets, statefulsets, daemonsets, services, nodes, namespaces, configmaps, secrets, jobs, cronjobs, PVC/PV, ingresses, endpoints, CustomResourceDefinitions, Flux and flux-operator objects), with a NAME/AGE fallback for everything else. STATUS columns use a fixed width of 26 characters, or 27 for Nodes, so status changes do not move adjacent columns. A configured column width takes priority. Column widths use the full filtered list so vertical scrolling does not move the columns. Node ROLES combines
node-role.kubernetes.io/labels with the legacykubernetes.io/rolevalue and removes duplicate roles. Node STATUS addsSchedulingDisabledwhen the Node is cordoned and keeps its readiness color. -
Event timing - LAST-SEEN shows the most recent reported occurrence for core and events.k8s.io Events. It advances with time and sorts by occurrence timestamp. AGE continues to show object creation age.
-
Event sizes - the events MESSAGE column shows byte counts such as
80474558 bytesin binary units scaled to the value, like76.7 MiB. Counts under 1 KiB stay as they are. Describe and YAML keep the original message. -
Service endpoints include ExternalName targets, configured external IPs, load balancer addresses, and NodePort values such as
80:30080/TCP. -
Pod health shows init progress and failure reasons, Pod reasons such as
Evicted, scheduling gates, and termination signals or exit codes. Init progress appears after the kubelet reports init state. Until then, the table keeps the Pod phase or reason, includingSchedulingGated. Normal init containers count toward RESTARTS during initialization, but not READY. Native sidecars count toward READY and RESTARTS. Their failures remain visible after initialization, without adding restarts from completed normal init containers. Application waiting and termination reasons take precedence after initialization. A blocked readiness gate gives a Running pod warning colors even when all containers are ready. Failure reasons use red rows and status text, including init failures and theLostPVC state. -
Horizontal scrolling - Left and Right move the table by five text positions. NAME and NAMESPACE stay fixed. Other columns keep their widths while you scroll. Arrows in the title show where more content is available. When all columns fit, Left and Right do nothing.
-
Custom views - define columns for any resource in the config file. Select and order built-in columns, live CPU/MEM usage, pod request and limit totals, and utilization percentages. Metric columns support numeric sorting, structured filters, and threshold colors. Custom text path columns support
format = "image-tag"to show image tags, with registry ports and digests handled separately. Quantity path columns supportformat = "cpu"andformat = "memory"for millicore and Mi/Gi display. Path columns acceptregexto show only the part of a value they need. Sorting and numeric filters use values before display rounding. An unknown custom resource picks up its CRDadditionalPrinterColumnsautomatically. If no usable CRD columns are available, server Table columns can supply the view. Explicit views and built-in columns keep their priority. Table cells use watch updates, periodic refresh, and a polling fallback.wtoggles wide-only columns (kubectl-o wide), including node IP addresses and labels. IP addresses are selected by type, independent of array order. Add@<namespace>to a view key to select columns for one namespace. Add:<name>to keep a second layout for the same kind and switch to it with:pods -o <name>or:layout <name>; bare:layoutlists the kind's layouts. See Views and thresholds. -
Drill-down navigation with a breadcrumb stack: workload/service → pods, cronjob → its jobs, node → its pods, pod → containers, namespace → re-scope, CRD → its custom resources, Argo CD Application or ApplicationSet → the Argo CD view, unless a
[views."applications"].drillis configured.escgoes back. Workload pod selection includes bothmatchLabelsandmatchExpressions. Drill-down, logs, Explain, and diagnostic bundles apply all requirements, includingIn,NotIn,Exists, andDoesNotExist. Services use their plain label map. -
Resource cycling (
Tab/Shift-Tab) - browse pods → services → deployments → statefulsets → daemonsets → secrets → configmaps → ingresses → PVCs, wrapping in either direction without configuration. Keeps the current namespace (including all namespaces), skips kinds absent from API discovery, and follows the active workspace's views when one is open.[/]remain view history. -
Command palette (
:) - fuzzy search over the full resource catalog, your saved bookmarks and workspaces, and the built-in commands (ctx,helm,workloads,ns-resources,pulse,xray,explain,timeline,gitops,argocd,adjacent,users,groups,policy,can-i,journal,debug,debug-clean,bundle,bundle-save,snapshot,snapshots,diff,events,pf,notify,find,vlogs,rightsize,fleet,skin,reload,config,info).:and?open the palette and help from every navigation screen, then close back to the screen where they were opened.→fills the highlighted suggestion into the command so you can keep typing a namespace,@context, or/filter. -
Cross-context resource navigation -
:pods @production-cluster defaultswitches context, resource, and namespace together without changing kubeconfig'scurrent-context. Context names after@fuzzy-complete: Tab/Shift-Tab or Down/Up fill the selected context in the command. Add a namespace if needed, then press Enter to run the query. Omit the namespace to use the target context's remembered or default namespace. Namespace completion after@contextis not provided. -
Help scrolling (
?) - browse all bindings, including plugins, bookmarks, and workspaces. Help opens on the section for the view it was opened from, without changing the section order. Plugins follow the table keys; from a table, the plugins for the current kind come first.j/kand↑/↓scroll one line.ctrl-f,PgDn, andspacemove forward one page;ctrl-bandPgUpmove back one page. Each page uses the visible content height.g/Homego to the top;G/Endgo to the bottom.]and[jump to the next and previous section./filters the bindings and resets the scroll position; a query that matches a section heading, such astable, keeps that whole section.escclears the filter first, then closes help.qor?closes help and returns to the previous screen. -
Picker paging -
PgDnandPgUpmove one visible page through every list picker, including:ctxand:ns, and keep working while a picker filter is being typed. -
Half-page scrolling -
half_page_downandhalf_page_upmove half a page in tables, document views, logs, help, and pickers. They are unbound by default; see key bindings to map them, for example toctrl-dandctrl-u. -
Filtering (
/) with matched-character highlighting: contiguous text ("text"keeps spaces),a|bfor either name,~textfuzzy match,/re/regular expression (all case-insensitive except fuzzy's smart case),!textinverse match (also!"text"and!/re/), local label key and value search (label:text,label:"text",label:/re/, and!label:text),-l/-flabel and field selectors (evaluated server-side on ⏎), and typed column comparisons (status=CrashLoopBackOff,cpu>500m,memory>1Gi,restarts>=5,age<2h). Structured terms AND together with spaces or&&;||combines alternatives, parentheses group expressions, and!(...)negates a group. Quote values containing spaces. Selectors survive refresh, namespace changes, drill-down, and view history. The title shows local, server-side, mixed, or pending evaluation;/edits and Esc clears. Palette queries combine scope and filtering::pods -n prod --context west /-l app=api status=Running. See filter grammar and selectors. -
Toggle faults (
Ctrl+Z) shows only what needs attention:- Pods: pending, failed, unknown, terminating, and running pods that are not ready. Completed pods are hidden.
- Deployments, StatefulSets, DaemonSets, and ReplicaSets: every STATUS
other than
ReadyandScaledDown. - Jobs: failed, pending, suspended, and terminating jobs.
- CronJobs: suspended or terminating, or the last scheduled run finished without succeeding.
- Flux Kustomizations, HelmReleases, Git/Helm/OCI repositories, Buckets,
ResourceSets, FluxInstances, and ResourceSetInputProviders: suspended,
terminating, or a
Readycondition that is notTrue. - Argo CD Applications: health other than
Healthy, sync other thanSynced, or terminating. ApplicationSets: any status other thanSynced, or terminating.
The table title shows
[faults]while the filter is on. It works with the text filter and current namespace or drill scope. PressCtrl+Zagain to turn it off. The setting stays on for these views during the session and does not filter other resource types. ConfiguredCtrl+Zbookmark, workspace, and matching plugin actions take precedence. Live updates keep the selected row selected. If it leaves the list or its UID changes, selection is cleared. -
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 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.Shift+ArrowUp/Shift+ArrowDownextend or reduce a range from a fixed starting row. Separate marks remain selected when the range contracts.ctrl-spacemarks every row from the lastspacemark to the cursor, so a block can be marked after jumping with/,PgDn, orG. Range marks also work with combined pod logs. -
Copy to clipboard -
ccopies the selected resource's name;Yopens a field picker over the selected row's displayed columns (full values, never the width-truncated cell text) - type to match a column name or its value (an IP, an image, a node),⏎copies it. Falls back to OSC 52 on remote terminals without a local clipboard tool. On WSL, sofka first triesclip.exefromPATHto copy to the Windows clipboard, including inside tmux. This requires Windows interoperability. Ifclip.exeis missing or fails, sofka tries the other clipboard tools, then OSC 52. -
Save to a file -
ctrl-sin the YAML, describe, diff, and events views saves the whole document as rendered, so a manifest no longer needs a trip through the clipboard. YAML and diff save as.yaml, the others as.txt.ctrl-sin logs saves the shown lines as.log. Both write tosave_dir, the temp directory by default. See Document views. -
RBAC-aware palette browse - the empty
:list hides kinds you cannotlist. An explicit search checks the full discovery catalog, because some delegated authorizers return incomplete rule reviews. -
Resource type on context switch -
:ctxkeeps the current resource type when the target cluster supports it. If unavailable, sofka opens that context's configured default resource, or Pods, and shows a fallback message. Filters and object ownership scope are cleared. Namespace selection follows the existing per-context rules. Explicit resource queries, bookmarks, and workspaces keep their specified destination. -
Context picker at launch -
sofka ctxandsofka contextsopen the picker before connecting to a cluster. Enter connects to the selected context and opens its configured default resource, or pods.--context NAMEselects the initial context in the picker. An unknown name returns an error.-nand-Aapply to the first successful selection only. These commands require interactive mode. -
Namespace switcher (
n) with pinned favourites (★) and per-context session recents (·) above the rest, plus a context switcher (:ctx). In the resource table,1to9select the first nine configured favourites in fixed configuration order. The picker lists these shortcuts in a column before the names, and while its filter is empty the same digits select the favourite directly; a digit without a favourite is a filter character. Unconfigured slots do nothing in the table.0selects all namespaces in both places. The header'sNamespace:line shows the configured favourites with their keys when the terminal is wide enough for them. The last namespace picked in each context is remembered across restarts (<state-dir>/namespaces.toml);-n/-Aoverride it for a session. -
Selected namespace (
W) switches to the cursor row's namespace and keeps the resource kind. It uses normal namespace history and watch behavior. Rows without a namespace show a status message. -
Sort by age (
A) selectsAGEwith the same direction as the sort picker. Press it again to invert. IfAGEis absent, the current sort stays active. -
Default sort -
[views."*"].sortsets a global initial sort, with resource-specific overrides. Sort choices are saved per kind by default. Setremember_sort = falseto make user sort changes temporary. -
Configurable key bindings - change or disable built-in keyboard actions under
[keys]. Shared navigation settings and mode overrides keep text input separate from navigation. Help and key hints show the effective bindings. Hint keys are bold in the skin's key color in the header, the status bar and popup footers; labels keep the surrounding style. Changes support:reloadand cluster/context overrides. Invalid bindings keep the previous keymap. Legacy palette settings are migrated with a config backup; managed files produce a warning and use the converted keys in memory. See Configure key bindings. -
Text editing -
BackspaceorCtrl-Hremoves a character in text inputs. Both terminal Backspace codes are supported. -
Mouse support - mouse capture is off by default for terminal text selection. Set
mouse = trueto enable mouse controls at startup. With capture on, the wheel scrolls every view (one wheel event is three steps of that view's own up/down;mouse_scroll_linestunes this in views with mouse capture), clicking a row selects it, clicking a column header sorts by it (click again to flip). Document views (YAML/describe, diff, events, logs, help) release the mouse automatically so click-drag selects text natively; the wheel still scrolls them in terminals that translate it to arrow keys in the alternate screen (kitty, Ghostty, iTerm2, ...), at the terminal's own speed, notmouse_scroll_lines. Setmouse = falseto start with the terminal's native mouse behavior everywhere. Use:mouseto switch capture on or off for the current session. With capture off, drag to select text. This command does not change the configuration file. sofka also 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 pane is almost all table.
-
Live CPU and MEM columns for pods and nodes from the metrics API, colored on unusual values. Nodes also get %CPU and %MEM of allocatable (
status.allocatable- the pool the scheduler hands out), colored by theutilizationthresholds and sortable, so "which node is full" is one glance and oneS. %CPU/R and %MEM/R show how much of allocatable the pods on each node request, with limits in wide mode and opt-in columns for extended resources such as GPUs. They come from the pods API and work without metrics-server. See Views. The container picker shows per-container CPU and memory, usage as a percent of request and of limit (-marks an unset one), and the pod QoS class. Memory quantities use Kubernetes units, including decimalk,P, andE, and binaryPiandEi, in metrics and filters. Fractional bytes round up to the next whole byte. Missing samples show-and do not match numeric CPU or memory filters. Measured zero shows0m,0Mi, or0%. Missing metric values sort before measured values in ascending order and after them in descending order. All of it degrades cleanly when metrics-server isn't installed. -
Configurable thresholds for the RESTARTS/CPU/MEM/request-limit coloring, globally and per resource and per context. See Views and thresholds.
-
Workload health at a glance - Deployments, StatefulSets, DaemonSets, and ReplicaSets carry a STATUS column derived from their replica counts and conditions (
Ready,Progressing,Degraded,Unavailable,Stalled,ScaledDown,Terminating), and the whole row is tinted by it - so a workload whose pods are crashing or whose desired replicas aren't met reads red/peach in the list, like k9s, instead of looking uniformly healthy. -
Job execution status distinguishes pending, running, suspended, failed, completing, completed, and terminating jobs. Failed jobs use the error color even when no pod is active. CronJobs carry a STATUS column too:
Running,Scheduled,Suspended,Terminating, orFailedwhen the last scheduled run did not succeed. -
Storage deletion status shows
Terminatingfor PVs and PVCs after deletion starts, including when a storage protection finalizer keeps the object in the API. -
Explain-unhealthy view (
X/:explain) - a deterministic, evidence-based explanation of why the selection is unhealthy: rollout state, degraded conditions, blocking pods and their container failure reasons (ImagePullBackOff, CrashLoopBackOff, OOMKilled, unschedulable, failed probes), and recent Warning events. Jobs, CronJobs, PersistentVolumeClaims, and Nodes get their own checks (see Explain unhealthy). No AI, no external service.⏎,E, orljumps from a finding to the pod, its events, or its logs. After opening evidence,escreturns to Explain before anotherescreturns to the table. Opening the view or pressingrreads the selected resource from the API before gathering its evidence. A failed read or a changed UID produces a warning instead of findings from an old snapshot. Only the latest requested report can update the findings. Closing the view withescorqcancels pending results and clears the report progress message. Navigation to a target resource or a palette destination also cancels pending results. Temporary Events and Logs views keep the parent report active. New findings update that report without changing the evidence view. Refresh keeps the previous findings until new results arrive. Condition, event, and container messages are shown in full. Long findings wrap under their first row;wclips them to one row each instead. The same toggle applies to the GitOps and Argo CD views. -
Session-local timeline (
T/:timeline) - a per-object timestamped log of every state change the watch saw: generation bumps, replica and readiness changes, pod phase, restarts, waiting reasons, condition flips. Computed from the watch stream, bounded, never written to disk. -
Workloads view (
:workloads [ns],:wk) - Deployments, StatefulSets, DaemonSets, CronJobs, Jobs, and Pods in one live table with KIND, READY, and STATUS columns. Keys that act on a row use the row's own kind, soctrl-d,d,y,l,s,r,⏎, plugins, and guardrails behave as in that kind's own view without drilling into it first. With marks across kinds, an action applies to the marked rows of the cursor row's kind; its confirmation says how many marked rows it skips. A dialog or prompt keeps acting on the kind it was opened for, even if a watch update moves the cursor. By default only top-level workloads are listed: pods owned by a ReplicaSet, StatefulSet, DaemonSet, or Job and Jobs owned by a CronJob are hidden, one⏎away from their owner.Oshows them too.Ctrl+Zjudges each row by its kind's faults rule. Kinds missing from API discovery are skipped. -
Namespace resources (
:ns-resources [ns],Ron a namespace row) - every namespaced kind from a fresh API discovery that has objects in the namespace, custom resources included, with KIND, GROUP, and COUNT columns. Defaults to the current namespace; all namespaces counts cluster-wide, and a namespace pattern is refused. The counts are a snapshot, not a watch: onelimit=1list per kind read offremainingItemCount(paging the list when the server leaves it out), eight at a time, with rows filling in as they answer. Kinds with no objects are hidden. A kind you can't list stays as ano accessrow and any other failure shows its error, so neither reads as empty.⏎opens the kind's own view in that namespace andescreturns, counting again.rcounts again too, keeping the previous counts on screen until the new ones are in. Filter, sort, and view history work as in any table; keys, commands, and plugins that act on objects are disabled on these rows, whiletarget = "context"plugins still run. -
Pulse dashboard (
:pulse) - cluster-health tiles, refreshed every 5s. -
Xray tree (
:xray) - a hierarchical view from the current kind down through owner references to pods and containers. -
RBAC subjects and rules:
:usersand:groupslist subjects from RoleBindings across all namespaces and ClusterRoleBindings. Enter opens directly bound rules. Use:policy u:alice,:policy g:developers, or:policy s:namespace/accountto open a subject by name. Bare:policyuses the selected service account. Enter on a Role, ClusterRole, RoleBinding, or ClusterRoleBinding opens its rules. Rules open as a matrix with one row per resource and API group and a column per standard verb. Rows for the same resource merge verbs from all rules. Other verbs, such asescalateor*, are listed inOTHER. A row marked(named)applies only to the objects inRESOURCE NAMES. Under a Role or RoleBinding, rows for cluster-scoped resources and non-resource URLs show no ticks because a namespace cannot grant them. A rule for every API group (*) is checked against each discovered group with that resource. If only some of them are namespaced, or API discovery skipped a group, the row says it covers namespaced API groups only. Presstto switch to the per-rule list. The choice is kept for the session. Setrbac_rule_list = trueto start with the list. Each rule keeps its source binding, source role, namespace scope, resource names, subresources, and non-resource URLs. A RoleBinding to a ClusterRole remains limited to namespaced resources in the binding's namespace. The view reads data on open and onr;Escreturns. Refresh keeps the selected subject. If that subject is no longer listed, the selection clears. It needs permission to list bindings and get referenced roles. Failed reads are marked INCOMPLETE. Empty results mean no matches in the data read. This view shows direct subject matches. It does not resolve group membership or check access with the API server. Use:can-ifor the current identity. -
Adjacent view (
u/:adjacent) - one hop in every direction from the selection: its owners, the objects it owns, the objects its spec names (a pod's node, claims, ConfigMaps, Secrets; a claim's classes and volume), and the objects whose specs name it (the pods mounting a claim, the claims using a class).⏎opens one in its regular view,y/dshow its YAML or describe. Relations are data: a built-in table for core kinds, extended per CRD with[[views."…".refs]]andchildren. A ref whose target kind is a field of the object (an ExternalSecret'ssecretStoreRef.kind) reads it withkind_pathfrom a list of candidatekinds;group_pathtells apart candidates that share a kind by their API group. Reverse lookups stay in the row's namespace unless the rule sayscluster. Presscin this view to discover direct children of a namespaced custom resource with a UID, including resources with no configured child kinds. Wait for the initial adjacent lookup to finish first. The search uses API discovery from the current cluster connection. It selects namespaced resources that support listing, excludes subresources, and selects one API version per resource. It searches only the source namespace and matches owner UIDs, not names or labels. Results are added to the view as pages arrive.⏎opens a result. The initial lookup and Enter action on the resource table stay the same. Each search permits four concurrent requests, 200 objects per page, at most 200 list requests and 20,000 objects checked, five seconds per request, and 30 seconds in total. Objects with other owners count towards the object limit. Access denial, request errors, timeouts, skipped API discovery, and search limits mark the search as incomplete. Results already found remain available. The search status stays above the results. Leaving the view, opening an overlay, refreshing, or changing the source or context cancels the search. Late replies are ignored.cstarts another search;rrepeats the initial adjacent lookup. This action does not search across namespaces, follow descendants recursively, or start background watches. -
Watch notifications (
:notify [note]) - toggle a notification on the selected object and Sophie watches it for you. An optional note reminds you why you are watching it. See Notifications.
- Flux CD controls (
t) - a suspend/resume/reconcile-now menu built on native Kubernetes API patches, for Kustomizations, HelmReleases, HelmCharts, git/helm/oci repositories, buckets, image automation, and notification alerts and receivers. Nofluxbinary needed. Works with bulk multiselect. The flux-operator kinds (ResourceSet, ResourceSetInputProvider, FluxInstance) get the same menu. They have nospec.suspend, so suspend sets thefluxcd.controlplane.io/reconcileannotation todisabled. Resume sets it toenabledand requests a reconcile, asflux-operator resumedoes. Their SUSPENDED column reads that annotation. ResourceSetInputProvider and FluxInstance also offer Force reconcile. For HelmRelease resources, Force reconcile requests a Helm install or upgrade even when the specification has not changed. It setsreconcile.fluxcd.io/requestedAtandreconcile.fluxcd.io/forceAtto the same new timestamp. The status message confirms the request was sent; it does not wait for the Helm operation to finish.⏎on a HelmRelease opens the revision history of the Helm release it manages (resolved the way helm-controller composesreleaseName/storageNamespace):⏎shows a revision's values,ythe rendered manifest,dthe NOTES,rrolls back. - HelmRelease chart version (
i) - prompts forspec.chart.spec.version, pre-filled with the current version, and patches it. A HelmRelease that usesspec.chartRefgets its version from the referenced OCIRepository or HelmChart, soipoints there instead. If a Flux Kustomization manages the HelmRelease, its next reconcile reverts the change; suspend it first (t) to keep the new version. - Argo CD controls (
t) - a suspend/resume/refresh/hard-refresh/sync-now/sync-with-prune menu for ArgoCD Applications, and a suspend/resume menu for ApplicationSets, built on native Kubernetes API patches. Suspend removesspec.syncPolicy.automatedand stashes the original value (includingprune/selfHeal/allowEmpty) as a base64 annotation so resume restores it exactly; ApplicationSet suspend setsapplicationsSynctocreate-only(nononemode exists) and stashes the original value the same way. Refresh sets theargocd.argoproj.io/refreshannotation tonormal, so the controller re-compares the Application against Git and then clears it; hard refresh sets it tohard, which also drops the repo-server manifest cache. Both also setargocd.argoproj.io/refresh-timestamp, as Argo CD's own refresh does, so a request sent while the controller is mid-reconcile isn't dropped. Neither asks first: the patch itself only requests a comparison. On an Application with automated sync, though, an OutOfSync result starts that sync right away, including itspruneif configured, just as the next periodic reconcile would. Sync-now patches the top-leveloperationfield. Sync-with-prune does the same withprune: true, so resources no longer in Git are deleted; it asks first and is gated bypruneguardrails. A plain sync never prunes, even whensyncPolicy.automated.pruneis set, because that only applies to automated syncs. Noargocdbinary needed. Works with bulk multiselect. - GitOps view (
:gitops/:flux) - the Flux ownership and reconciliation chain for the selection: the owning Kustomization/HelmRelease, its source (GitRepository/OCIRepository/HelmRepository) with applied and latest revision, thedependsOnedges, and ready status. Each item is a finding you can⏎into. Opening the view or pressingrreads the original resource again, then follows its current owner labels, source, and dependencies. A missing or replaced resource produces a warning. These reads requiregetaccess. Only the latest requested report can update the findings. Closing the view withescorqcancels pending results and clears the report progress message. Navigation to a target resource or a palette destination also cancels pending results. The Managed resources section shows up to 500 entries from the selected resource’s.status.inventory.entries, including custom and cluster-scoped resources. Press⏎on an entry to open it. Building this list does not read the managed resources. Navigation uses the API version available through cluster discovery. Unknown kinds and invalid entries show a warning. If the selected resource hasspec.kubeConfig, the list is shown without navigation because its resources can be in another cluster. An absent inventory is reported as unavailable. Helm hooks and controller-created children are not added to this list. Navigation uses the normal resource view and its access error handling. A flux-operator ResourceSet or FluxInstance is its own owner:⏎on one opens this view, with no Source section. The view also lists the input providers in the ResourceSet'sspec.inputsFrom, for the ResourceSet and for the objects it applied.⏎on a named provider opens it. An object a ResourceSet applied finds its owner from theresourceset.fluxcd.controlplane.io/namelabel. An object a FluxInstance applied finds it fromfluxcd.controlplane.io/name, asflux-operator tracedoes. - Argo CD view (
:argocd/:argo) - the state of the selected Application: sync and health, the project and destination, every source it deploys from with the revision actually deployed from that source, every object instatus.resources[]with its own sync and health, and a summary of what is blocking - a suspended sync policy, aComparisonError, a failed sync operation, degraded or missing objects, or drift. Each drifted object says whether it is in git but not the cluster, in the cluster but no longer in git (requiresPruning), or differs from git, in which case the view points atargocd app difffor the fields: Argo CD never writes the diff to the Application. Telling a missing object from a differing one needs per-resource health, which Argo CD only persists withcontroller.resource.health.persist; without it the view says it is one or the other.⏎on an Application row opens this view. Each managed resource is a finding you can⏎into. Read entirely from the Application CRD: no Argo CD API server, no token, noargocdbinary. The headline names how long the current health has held, fromstatus.health.lastTransitionTime- "Degraded (since 4m)" is a different problem from "Degraded (since 7d)", and the same field marks how long a recovery has held too. Missing on older Argo CD versions that don't write it, in which case the headline reads as it always did. When health isDegradedorMissingand nothing in the Application's own status accounts for it, sofka looks for the cause in the objects themselves and lists what it finds under the blocking line, each row a finding you can⏎into. Drift does not count as an account of it: an OutOfSync resource says the cluster differs from git, which is a separate fault from something being broken. This is the usual case rather than an edge one: Argo CD fills instatus.resources[].healthonly whencontroller.resource.health.persistis turned on, and it is off by default, so a degraded Application normally names no culprit. The search reads at most five causes, and only when the Application is unexplained, its destination is the cluster you are connected to, and it manages something that can own other objects - a Deployment or a CronJob, never a ConfigMap. Applications deploying to a remote cluster are handled honestly - the destination is resolved against your kubeconfig and shown by context name, and because those objects do not live in the cluster you are connected to,⏎on a managed resource switches to the context that serves that cluster and opens the object there once the connection lands (the kind is resolved against that cluster, so a CRD this one lacks is fine).escat the root of that view switches back and reopens the Argo CD view it came from. Where no context serves the destination,⏎reports where the objects are instead. Adestination.serverURL matches the context whose cluster has that server. Adestination.namematches a context named the same, then a context whose cluster entry is named the same, then one whose cluster entry ends in/<name>- so an EKS entryaws eks update-kubeconfignamed by ARN resolves for an Argo cluster registered aseks-dev-general. The tail only counts when exactly one cluster entry carries it; the same bare name in two accounts stays unresolved rather than guessing. A short alias and the full name pointing at one cluster both resolve.con a managed resource expands what it owns, indented underneath - Deployment to ReplicaSet to Pod, CronJob to Job to Pod - read from the children's ownownerReferences. Child kinds come from the same rules the adjacent view uses, so a[views."…"]childrenentry applies here too. The walk stops two levels down, which covers the built-in workload chains; a longer custom chain shows its first two levels only. Each row carries its own state: a pod's phase or its waiting reason (CrashLoopBackOff), a ReplicaSet's ready count, a Job'sCompleteorFailed. Superseded ReplicaSets scaled to zero that own no pods are left out, otherwiserevisionHistoryLimitburies the running one. It costs one read plus one list per owned kind, so it is on request rather than automatic, and only for resources in the cluster you are connected to. Opened on any other object, the view follows Argo's tracking metadata the other way: theargocd.argoproj.io/tracking-idannotation (preferred, since it is exact) or theapp.kubernetes.io/instancelabel (truncated at 63 characters) names the Application, which is looked up by name. An object Argo does not manage says so, and a tracking reference whose Application no longer exists is reported as a dangling reference rather than as unmanaged. Where several Argo CD instances share a cluster and each holds an Application of the same name, the one that actually lists the object among its managed resources wins.rre-reads the resource and follows its tracking metadata again. These reads requiregetandlistaccess. Opened on an ApplicationSet directly, there is no owning Application to walk up to, so the view instead names its configured generators - unwrapping amatrixormergegenerator to what it actually combines rather than reporting just "matrix" - and lists the Applications it produced, straight out ofstatus.resources[], each one a finding you can⏎into. The ApplicationSet table view carries its own curated columns:STATUSfrom theErrorOccurred/ResourcesUpToDateconditions,GENERATORS, andAPPS- the count of Applications produced. - Native Helm inspector (
:helm/:hm) - sofka decodes Helm's release storage Secrets directly (double base64 → gunzip → JSON, same as Helm) and lists one row per release at its latest revision, likehelm list.⏎opens the full revision history (helm history); on a revision,⏎shows user-supplied values,ythe rendered manifest,dthe NOTES.txt.rrolls back andctrl-duninstalls - those two shell out to the realhelmbinary, all the inspection is native. UPDATED advances with the clock in both the release list and revision history. The table keeps the deployment timestamp in its row cache, so clock updates do not decode the release again. - Restart workloads (
r) - restart marked Deployments, StatefulSets, or DaemonSets after confirmation. With no marked rows, restart the current row. Guardrails apply to the full target set. A failed request does not stop requests for the other targets. The final error report retains all failed targets. - Rollout history and rollback (
:rollout-history) - list the revisions of the selected Deployment, StatefulSet, or DaemonSet, newest first, likekubectl rollout history. Deployment revisions come from their ReplicaSets, StatefulSet and DaemonSet revisions from their ControllerRevisions. Only revisions whose owner reference carries the workload's UID are listed, and the workload's table filter is not carried over. Each row shows the revision,deployedorsuperseded, the images, and thekubernetes.io/change-causeannotation.⏎reads the live workload and diffs its pod template against the selected revision's.rrolls the workload back to that revision after confirmation, likekubectl rollout undo --to-revision; thedeployedrevision is refused without asking. Before patching, sofka reads the workload first and refuses a paused Deployment, a template that already matches, or a workload recreated since the history was opened; the patch carries the read's resourceVersion, so a change in between fails. When Flux or Argo CD manages the workload, the confirmation warns that the next sync reverts the rollback. Therollbackguardrail applies. Delete, edit, and scale are refused in this view: revisions belong to their workload. - Scale discovered resources (
s) - scale built-in or custom resources when API discovery lists ascalesubresource with PATCH support. Changes use/scale, including when a CRD stores replicas at a custom path. Marked rows can be scaled together. Custom resources do not show an assumed current count. The action requires patch permission on the scale subresource. - Managed-resource mutation warnings - before you edit, delete, scale, or otherwise change an object Flux (or another controller) owns, sofka tells you the next reconcile will revert it or recreate it. Fix the source instead of fighting the controller.
- CSR approve and deny (
V/Non:csr) - approve or deny the marked CertificateSigningRequests, or the current one. The confirm popup lists each request's requester, signer and usages, and warns when approving aclient authcert fromkubernetes.io/kube-apiserver-client, since that grants cluster access. Requests that already have anApproved,DeniedorFailedcondition are skipped. The decision is written through theapprovalsubresource, so it needs theupdateverb oncertificatesigningrequests/approvalandapproveon the signer; sofka does not check this up front, so a 403 is shown as the error. Blocked in read-only mode and gated by theapproveanddenyguardrails. The table shows NAME, SIGNER, REQUESTOR, CONDITION and AGE; CONDITION readsPending,Approved,DeniedorFailed, with,Issuedonce the certificate is set. - CronJob controls (
t) - trigger now (creates a Job from the jobTemplate, likekubectl create job --from), suspend, resume. - Background port-forwards (
f/Fto start,:pfto manage) plus saved forwards that show up in:pfeven while stopped, with optional autostart. Pressingfon a pod or service opens a picker listing the manifest's declared ports. Pressenterto start the selected mapping, oreto edit only its local port. The edit prompt contains the current local port;escreturns to the same picker row. Mappings whose local port is already bound showin use. Choose "Custom…" to enter a remote port and then a local port, which starts as the remote port. If the local port cannot bind to either loopback address when you start the forward, the input stays open and shows an error so you can choose another port. Active forwards show a teal●in a dedicated indicator column next to the row name. See Saved forwards. - File transfer (
ton a pod, ortin the container picker for one container) - download from or upload to a pod viakubectl cp, off-thread with a completion flash.ton a pod uses its default container and names it in the menu and prompts. Uploads are gated by thetransferguardrail and read-only mode. - PVC explore (
xon a PVC, or:pvc-explore) - a two-pane browser over a volume's contents, withsfor a shell inside it. See PVC explore. - Shell failure recovery keeps command errors visible until dismissed. A missing shell offers the built-in debug image prompt for the same target, without plugins. Creation requires explicit acceptance and obeys guardrails.
- Ephemeral debug containers and node debug pods (
:debug). See Debug containers and pods. - Logs (
l) - combined logs for marked pods, per-container on a pod, or aggregated across all matching pods on a workload/service, with filtering, previous-container logs, and configurable tail/buffer/lookback. A pod with akubectl.kubernetes.io/default-containerannotation shows only that container, as kubectl does;aswitches between it and every container. PressTto enter a duration ortailfor the current kubelet log view. Lines with timestamps are sorted by time. Presstto show or hide timestamps without changing log order or restarting streams. If a container is waiting to start, sofka retries until its logs are available. Followed streams reconnect from their last line after a container restart, dropped connection, or sleep, and workload and service logs add pods as a rollout creates them. sofka parses ANSI color from the source app and maps it onto the active skin instead of printing literal escapes. See Log controls. - Log severity filter (
Ctrl+Zin logs) shows detected warning and error lines, including fatal and panic levels. The title shows[warn/error]. It combines with/and uses the same severity rules as log colors. Detection depends on how applications format their logs. Lines with no recognized severity, including stack trace continuation lines, can be hidden. PressCtrl+Zagain to show all retained lines. The filter resets when a new log view opens. Copy and save use the filtered lines. - JSON log display (
Jin logs) cycles raw, record, and indented JSON. Record view shows each structured log record on one row: time, level, message, thenkey=valuefields in the order the application wrote them, nested objects as dotted keys, with the level, message, keys, and values styled apart. The setting stays active for the session. Filters and application copy/save use raw records. See Log controls for limits. - Sideways log scrolling (
←/→orh/lin logs) moves unwrapped lines 5 columns at a time so the end of a long line is reachable. It stops at the widest line on screen.wwraps lines instead and resets the offset. - Log markers (
min logs) add visual separators at the buffer tail. Markers stay visible through filters, do not move a paused viewport, and are excluded from sofka copy/save. Clearing or replacing the buffer removes them. Their storage is bounded by the active log buffer cap. - VictoriaLogs integration (
L/:vlogs) - log history from a VictoriaLogs backend for a pod, container, workload, service, or whole namespace, covering restarted and deleted pods. Zero config: sofka finds the service in-cluster and reaches it through the API-server proxy. See Providers. - Log links (
L) - open the selection in Cloud Logging, with the pod, workload selector, CronJob, namespace, or node filter set. Zero config on GKE clusters named by gcloud.type = "link"fills a URL template for any other log UI. See Providers. - Right-sizing (
:rightsize) - estimate right-sized requests from past usage in a Prometheus or VictoriaMetrics backend, with a patch preview. Never mutates. See Providers. - Fleet dashboard (
:fleet) - an opt-in health summary across contexts, side by side: node readiness, unhealthy pods, Flux failures, and Argo CD Applications that areOutOfSyncor notHealthy. A cluster without the Flux or Argo CD CRDs shows—rather than a zero. Contexts come from config orspacein the:ctxswitcher.⏎on a row with a non-zero Argo CD count switches to that context and lands straight on the Applications behind it, filtered to the sameOutOfSync/ not-Healthycriteria the count itself used - across every namespace, not whichever one was active before the switch. A row with nothing degraded still lands on its default view. See Providers. - Experimental native describe uses the standalone Rust
deskribelibrary. Enable it with--experimental-describeor[experimental] native_describe = truein config. Kubectl is the default when neither enables it. Unsupported native resources use a labeled kubectl fallback; fallback failures show errors, not cached YAML. Custom resources use the generic native renderer. - YAML view (
y), describe (d), events (:events/E, filtered by UID when available), and diff (:diff), withctrl-f/ctrl-b(orPgDn/PgUp) paging through each document. Native descriptions include Service application protocols, CronJob time zones, StatefulSet policies, Pod scheduling groups, and Node-local ResourceSlice summaries. Node resource accounting includes Pod-level budgets and resize status; older API compatibility paths remain available. - Edit from a document -
ein the YAML or describe view edits the displayed resource withkubectl edit, so a table row that moved while the watch was filling cannot change the target. The document is read again when the editor closes. - Edit decoded Secrets -
ein the decoded Secret view (x) opens the values as plain-textstringDatain$EDITOR. sofka compares the result, base64-encodes it, and patches only the keys you changed, added, or removed, after a confirmation that names them. Values that are not text are left unchanged. See Document views. - Managed fields in YAML -
mshows or hidesmetadata.managedFieldsin the YAML view. Fields are hidden when a document opens. Showing them reads the full resource from the API. Automatic refresh keeps the current choice. - Diff on GitOps clusters -
:diffshows a unified diff of the live object against itslast-applied-configuration. When that annotation is missing - as it is for every Flux- or Helm-managed object, which nothing everkubectl applys - sofka diffs against the previous revision this session's watch saw, so "what just changed?" has an answer. The last revision of up to 256 changed objects is kept in memory.
Automatic refresh is available in these resource views:
| View | Automatic refresh | Other refresh controls |
|---|---|---|
| YAML, decoded Secret, describe | r turns refresh on or off |
None |
| Diff | r turns refresh on or off |
R resets the baseline to the displayed resource |
| Explain | R turns refresh on or off |
r refreshes immediately |
Automatic refresh is off when a view opens. It reads immediately, then waits 5 seconds after each result before the next read. Describe retains its selected backend: kubectl by default, or native-first with a labeled kubectl fallback when opted in. Native describe fetches fresh resource, related-object, and event data through the current Kubernetes client. The other views also read through the API. YAML and describe support custom resources.
Refresh keeps the original resource and context. It preserves document search,
scroll position where possible, and the selected Explain resource when findings
move or its status text changes. Findings without a resource target match by
content. If the selected finding disappears, the selection clears. Select another
finding before opening its resource, events, or logs.
A shorter document can reduce the scroll position. The status indicator shows
refresh while automatic refresh is on and stopped when it is off. Documents
without refresh support, such as saved snapshots and Helm manifests, show static.
Automatic refresh stops when you leave the view, open help or the command palette, or a request fails. Document search keeps refresh active. A failed request keeps the last result and shows the reason. A deleted resource, or a resource recreated with the same name and a different UID, also stops refresh. Opening events or logs from Explain stops its automatic refresh; returning does not restart it. Its existing manual evidence request can still finish.
Diff keeps the baseline chosen when the view opens, even if the last-applied
annotation or session history changes. R makes the currently displayed object
the new baseline. A Diff view can stay open when both sides match, so automatic
refresh can show later changes. This does not add change highlighting to YAML.
A PersistentVolumeClaim has no API that returns its contents: the only way to
see what is on a volume is from inside a pod that mounts it. x on a PVC row
(or :pvc-explore) does that for you and puts the result on screen as a
two-pane file browser - your local filesystem on the left, the volume on the
right - so a download or an upload is one keystroke rather than a hand-written
kubectl cp path.
-
It uses a pod that is already there. sofka looks for a running pod in the claim's namespace that mounts it, preferring one with a writable mount, and execs into that container at its
mountPath. Nothing is created, so this works in read-only mode. -
Otherwise it offers a helper pod. When nothing mounts the claim - the common case for a volume you are trying to inspect because its workload is scaled to zero - sofka asks before creating a short-lived pod that mounts it at
/pvc. That is a write: it is blocked in read-only mode, matches thepvc-exploreguardrail action, and always confirms, naming the image and the namespace. The helper carries both asleepandactiveDeadlineSeconds, so it expires on its own even if sofka never gets to delete it, and closing the browser - or quitting sofka - deletes it immediately.:pvc-cleanremoves any a crashed session left behind: it sweeps the current namespace, or every namespace when the view is across all of them, requiring the name prefix, both of the labels sofka sets, and the annotation naming the claim, and skipping the pod your own open browser is using. None of that evidence is unforgeable - anything sofka writes on creation, anything else can write too- so it is there to make an accidental match essentially impossible, not as
a permission check; the confirmation, the guardrail and read-only mode are
what bound a deliberate one. It cannot tell a leftover from a pod another session is browsing
through right now, so the confirmation says so. Deleting pods is a mutation
like any other: blocked in read-only mode, matched by the
pvc-exploreguardrail, recorded in:journal.
- so it is there to make an accidental match essentially impossible, not as
a permission check; the confirmation, the guardrail and read-only mode are
what bound a deliberate one. It cannot tell a leftover from a pod another session is browsing
through right now, so the confirmation says so. Deleting pods is a mutation
like any other: blocked in read-only mode, matched by the
-
Missing tools trigger built-in recovery. If the first listing fails because
sh,ls, orheadis missing, sofka tries up to 16 other running containers that mount the same part of the claim. Read-only restrictions remain in force. If none works, it offers a helper pod and asks before creating it. Read-only mode and thepvc-exploreguardrail still apply. For an in-useReadWriteOnceclaim, the helper is scheduled on the consumer's node. An occupiedReadWriteOncePodclaim cannot use a second pod. Recovery reports that restriction without creating a helper. A staticsubPathis preserved; asubPathExprmount cannot recover automatically because its boundary cannot be determined from the pod specification. Permission errors, connection failures, and invalid paths retain their specific messages. Canceling recovery leaves the original error in the browser. Reopen the claim to start a new recovery attempt. -
Navigation is confined to the mount.
⌫stops at the mount point, and every listing verifies withpwd -Pthat it actually landed inside the volume - so a symlink on the volume pointing at/is refused rather than quietly dropping you into the serving pod's root. sofka also treats the volume's contents as untrusted: GNUlswrites file names into a pipe unescaped, so a file whose name contains a newline can inject what looks like an extra row, and a symlink target can carry an absolute path. Such a row may still appear as a phantom entry - there is no way to tell it from a real one - but it is contained: entries naming.,.., or anything containing/are discarded, so a forged row can reach neither outside the mount nor outside the directory a download lands in. busyboxls- the default helper image - substitutes?for control characters instead, so there is nothing to forge; such a name lists looking ordinary and fails when you open or copy it. -
ccopies from the focused pane into the other one - out of the volume when the right pane has the cursor, into it when the left one does. Uploads go throughkubectl cp, are blocked in read-only mode, match thepvc-uploadguardrail action, and are refused up front when the mount isreadOnly. A download that would overwrite a local file confirms first.kubectl cpsplits its arguments on the first:, so a name containing one is refused with an explanation rather than afilespec must match the canonical formatfrom kubectl. -
A copy in flight fills a bar where the row's size was, so a 5 GB file or a directory of thousands of them shows how far it has got rather than one unchanging "copying" line for minutes.
kubectl cpreports nothing while it runs, so what is measured is the destination: a download's bar comes from the local file (or tree) it is writing, an upload's from onekubectl execthat prints the destination's size once a second - one exec for the whole copy, not one per second. A folder is measured whole, so its bar is the recursive total and not one file at a time. The status bar carries the percentage as well, which is also where a copy started byton a pod - with typed paths and no row to draw on - reports itself.Quitting sofka while an upload is running leaves that
duloop in the pod until it times itself out - fifteen minutes where the container has a clock, and 300 passes, at least five minutes, where it has not - because the signal that stops it is the connection closing, and a killed process does not send one; the copy itself is left to finish either way.Measuring the volume side is an exec of its own -
du, alongside thetarthatkubectl cpalready runs there - and it is gated no further than the copy it belongs to; see safety for why. A copy also starts a moment later than it used to, since the destination is measured before it is touched.The source's total comes from the listing for a single file and from
dufor a directory, so the pod needsduas well as thelsa listing needs and thetara copy needs; without it the copy runs with no bar. The total is an estimate either way, and a bar can finish short of the end or reach it early and wait: aduthat can only report whole disk blocks reads high, a subdirectory the serving pod cannot read is missing from the total, and both ends count directory entries at whatever their own filesystem charges for one - 4 KiB on ext4 against a couple of hundred bytes on APFS, which on a tree of many small directories is a visible fraction rather than a rounding error. What is already at the destination is not counted - an overwrite opens at zero rather than at yesterday's copy - but a whole folder copied over a copy of itself is the case this cannot measure: almost nothing new lands, so its bar ends well short even though the copy is complete. -
sopens a shell at the directory the remote pane is showing (or at the mount point, from the PVC row directly). The exec lands in a real pod, so it passes the sameshellguardrail asson that pod's row - a rule that denies shells in prod is not defeated by reaching the pod through a claim it mounts, and a denied shell is refused before a helper pod is created rather than after.
Listings are read with ls -A -l over kubectl exec, so the pod's image needs
a shell, ls, and head; transfers additionally need tar, as kubectl cp always
does. An entry ls cannot stat still appears, with an unknown size and a
warning, rather than blanking the whole directory. The helper-pod image,
lifetime, and resources are configurable:
[pvc_explore]
image = "busybox:1.37" # helper-pod image
ttl = "30m" # how long it lives before deleting itself
cpu_request = "100m"
cpu_limit = "100m"
memory_request = "128Mi"
memory_limit = "128Mi"The helper sets its limits explicitly and equal to its requests, so a namespace
LimitRange cannot fill in a default limit that breaks its
maxLimitRequestRatio. Raise the limits for faster listings of large
directories, or change any value your namespace policy requires. An empty value
leaves that field unset.
Only a Bound filesystem claim can be browsed: an unbound one has no volume
behind it, and a volumeMode: Block one has no filesystem. A listing is a
point-in-time read, not a watch: r re-reads both panes. Both panes cap one
directory at 5,000 entries - on the volume side by head inside the pod, so a
spool directory is never streamed out in full. An entry nothing could stat
still lists, with ? for its size.
The helper pod runs as whatever user its image defaults to, because reading a
volume's contents generally needs root. It drops all capabilities and sets
allowPrivilegeEscalation: false and seccompProfile: RuntimeDefault, which
satisfies the baseline Pod Security Standard - but not restricted, which
also requires runAsNonRoot. In a namespace enforcing restricted the helper
pod is rejected; browse through a pod that already mounts the claim instead.
:dir [path] lists a local directory under the header, so a manifest from
your repository is one keystroke from the cluster you are looking at. Without
a path it opens the directory sofka was started from; ~ and relative paths
work. Only folders and .yaml, .yml, and .json files are listed, folders
first. Hidden entries and anything that is neither a regular file nor a
folder, such as a named pipe called x.yaml, are skipped. A folder with a
kustomization.yaml, kustomization.yml, or Kustomization file is marked
kustomize.
enteropens a folder, or shows a manifest as a read-only document.eopens the manifest in$KUBE_EDITORor$EDITOR, suspending the TUI.ashows akubectl diffof the selection. Nothing is applied until you pressaagain in that diff and confirm. When nothing would change, the status line says so and no diff opens.ddeletes the objects the selection describes, with--ignore-not-found, after confirmation.
Both start by rendering the selection once with
kubectl apply --dry-run=client -o json: a manifest with -f, a folder with
-f <folder> -R, and a kustomization with -k. A delete renders with
--validate=false, so a manifest holding only apiVersion, kind, and
metadata.name works. The rendered objects stay in memory and reach the diff,
the apply, and the delete on stdin (-f -), so a rendered Secret is never
written to disk. An edit made after the diff opened is not applied; press a
again to see it. Leaving the diff drops the preview.
Rendering fills in each object's namespace, using the context's default for
objects without one. Guardrails match the apply and delete actions against
every rendered object's resource and namespace, like the same action on a table
row. max_bulk counts every object a rule matches across the whole selection,
whatever its kind. A selection with a kind sofka has not discovered is refused,
since its guardrails could not be matched reliably. Every command is
pinned to the current context with --context. Apply and delete are blocked in
read-only mode and recorded in :journal.
- Read-only mode, declarative guardrails, action-aware authorization
(
:can-i), and a session-local action journal (:journal) with optional file persistence. See Safety.
- Plugins - shell-out commands bound to key chords, scoped per resource, with terminal/popup/background output modes, confirmation and dangerous flags, read-only declarations, rich placeholders, and bulk execution over marked rows. The official reviewed catalog supports cluster-independent search, description, checksum-verified installation, explicit update and rollback, offline listing and caching, withdrawal, and safe removal. See Plugins.
- Bookmarks - saved navigation commands on a chord and in the palette.
- Workspaces - a named set of views for one task, cycled with
Tab. - Skins - built-in Catppuccin, Gruvbox, Solarized, Nord, Dracula, Tokyo
Night, One Dark, Rosé Pine, Rosé Pine Dawn, Monokai, and Flexoki palettes,
auto dark/light detection,
and per-swatch hex overrides. Every semantic color (row status, severity
badges, headers, borders) is derived from the active palette, so one skin
change lands everywhere at once. The
:skinpicker opens on the active skin and previews each skin as you move;enterkeeps it,escrestores the previous one. - Config file (TOML or YAML) with per-cluster and per-context overrides and live
:reload. See Configuration.
- Diagnostic bundles (
:bundle,:bundle-save) - a redacted incident bundle for the selection as one Markdown document. See Diagnostic bundles. - Snapshots (
:snapshot,:snapshots) - capture the current table view to text, JSON, or YAML, then browse and open saved captures. See Snapshots. - Runtime diagnostics (
:info, orsofka info) - version and build, config sources, live context/cluster/API server and Kubernetes revision, discovery with warnings for unread API groups, Metrics API status, watch error and reconnect counts, API request latency per class, active skin, loaded plugins and views, and the state/log/snapshot/bundle directories. The connected Kubernetes revision also stays visible in the main header. Identifiers, paths, and counts only, never credentials, tokens, or Secret values. See Runtime diagnostics. - Update notifications (
:check-update, orsofka check-update) - sofka checks GitHub for a newer release once a day at startup. A newer release appears in the header and on the status bar with the upgrade command for the install method: Homebrew, Nix, Cargo, winget, or a download link for distro packages and other installs.:infoshows the latest known release and its notes link. sofka never downloads or installs a release itself. Setupdate_check = falseto turn off the startup check. See Base options. - Structured logging (
[logging], orSOFKA_LOG=debug) - sofka's own session log as logfmt lines under the state directory, with every value redacted on the way in and writes off the UI thread. Off by default. See Structured logging.
:sanitizedeletes the pods a namespace has finished with - completed jobs, failed and evicted pods, and optionally the wedged ones. It ships with sofka and needs no runtime onPATH; the adapter is the sofka binary.statesselectsterminal(the default),stuck, orall, based on application container state and Pod phase. Specific table reason labels do not add deletion categories.dry_run=truereports without deleting. It confirms before running, is blocked in read-only mode, and matches guardrails asplugin:sanitize. It never deletes a pod that is terminating, still has a running container, or was replaced since the scan. The scope is the current namespace - all namespaces when the view is.-l/-ffilter terms narrow the scan server-side; a filter it cannot reproduce exactly makes it refuse rather than delete more than the table shows. See Sanitize pods.
- Package discovery reads
plugins/*/plugin.tomlfrom the sofka configuration directory. Packages reload with:reload. - Named commands and key chords start adapters without changes to sofka's source code. A package can contain several commands with separate scopes, inputs, and safety settings.
- Validated inputs supply named arguments with types, defaults, choices, and limits.
- Plugin input form shows all inputs at once when a command starts from its
key chord or without arguments and an input has no default, or the command sets
prompt = "always". Fields show their type and limits,←/→cycle choices and booleans, and errors appear under the field. - JSON reports show text sections and tables in a searchable document.
- Live plugin activity floats over popup/report runs with a spinner, elapsed
time, and a bounded, scrollable plain-text stderr tail.
Eschides without cancelling,Ctrl+Alt+Ttoggles the panel, and focusedCtrl+Ccancels the process group. The toggle is configurable with[keys.global].plugin_activity.:plugin-activityopens the current run or report;Enterin a completed panel opens its report.↑/↓,PgUp/PgDn,Home/Endscroll;Gfollows the tail. Visible completion opens the report; hidden completion notifies without moving focus and retains the report for reopening. Background runs stay unobtrusive. - Shared execution limits output and concurrency.
It cancels processes on timeout, navigation, or
:plugin-cancel. - Plugin kubeconfig reload lets a successful report request a fresh context list and open the context selector. The user selects the context before sofka reconnects, including when the context name stays the same.
- Safety controls apply read-only mode, confirmation, and guardrails to plugins. Load-test plugins require a network-load declaration.
- Managed port-forwards supply a local endpoint for a selected pod or service.
- Local checks validate package manifests and reports without a cluster.
Workload STATUS shows Progressing until the controller observes the current
specification and an active rolling update reaches its target. StatefulSet
partitions and OnDelete strategies retain their update semantics. Deployment
READY compares ready replicas with the desired count from the specification.
An active rollout with some ready replicas shows Progressing even when
Available=False. A workload with no ready replicas shows Unavailable. A
failed rollout shows Stalled.
Popups wrap long text within the terminal margins. Confirmation and input popups
use PgUp and PgDn to scroll text that does not fit. Action keys stay visible.