Skip to content

Latest commit

 

History

History
1308 lines (1226 loc) · 85.7 KB

features.md

File metadata and controls

1308 lines (1226 loc) · 85.7 KB

Features

The full list. For how sofka compares to k9s, see vs k9s.

Shell completion

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.

Import from k9s

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), use pipes, 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, and portForwardAddress.

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.

Node drain options

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 0 for unlimited waiting. Use durations such as 30s, 5m, or 1h30m. 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.

HTTPRoute paths

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.

Core navigation

  • 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-container annotation, 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 - or Unknown. 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-trend and node-memory-trend metric 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 - F toggles 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 = true to enable line wrapping at startup. The w key 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 is all when all namespaces are selected. Set terminal_title = false to disable title changes. Sofka clears the title on exit.

  • Compact startup - set compact_mode = true to start with a one-line header and no footer. Ctrl-E toggles the layout for the session; reloads and context switches preserve it.

  • Optional header - set hide_header = true in 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-token with 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-resumption for 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 - core pods wins over pods.metrics.k8s.io. Discovered short names also work for custom resources, such as :md for 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 an apiVersion that is not v1. Sofka does not load that group. It shows a warning at startup and a flash on the first screen. :info shows the group and the reason.

  • Namespace switcher (n) opens with the cursor on the active namespace. current marks the active namespace, including <all>. context default marks 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 *-crons selects 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 an unresolved label. Press ctrl-r to 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 press ctrl-r to 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. :namespace and :namespaces accept the same argument; all and * 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, :ns opens 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 legacy kubernetes.io/role value and removes duplicate roles. Node STATUS adds SchedulingDisabled when 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 bytes in binary units scaled to the value, like 76.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, including SchedulingGated. 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 the Lost PVC 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 support format = "cpu" and format = "memory" for millicore and Mi/Gi display. Path columns accept regex to 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 CRD additionalPrinterColumns automatically. 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. w toggles 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 :layout lists 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"].drill is configured. esc goes back. Workload pod selection includes both matchLabels and matchExpressions. Drill-down, logs, Explain, and diagnostic bundles apply all requirements, including In, NotIn, Exists, and DoesNotExist. 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 default switches context, resource, and namespace together without changing kubeconfig's current-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 @context is 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 / k and ↑ / ↓ scroll one line. ctrl-f, PgDn, and space move forward one page; ctrl-b and PgUp move back one page. Each page uses the visible content height. g / Home go to the top; G / End go 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 as table, keeps that whole section. esc clears the filter first, then closes help. q or ? closes help and returns to the previous screen.

  • Picker paging - PgDn and PgUp move one visible page through every list picker, including :ctx and :ns, and keep working while a picker filter is being typed.

  • Half-page scrolling - half_page_down and half_page_up move half a page in tables, document views, logs, help, and pickers. They are unbound by default; see key bindings to map them, for example to ctrl-d and ctrl-u.

  • Filtering (/) with matched-character highlighting: contiguous text ("text" keeps spaces), a|b for either name, ~text fuzzy match, /re/ regular expression (all case-insensitive except fuzzy's smart case), !text inverse match (also !"text" and !/re/), local label key and value search (label:text, label:"text", label:/re/, and !label:text), -l/-f label 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 Ready and ScaledDown.
    • 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 Ready condition that is not True.
    • Argo CD Applications: health other than Healthy, sync other than Synced, or terminating. ApplicationSets: any status other than Synced, or terminating.

    The table title shows [faults] while the filter is on. It works with the text filter and current namespace or drill scope. Press Ctrl+Z again to turn it off. The setting stays on for these views during the session and does not filter other resource types. Configured Ctrl+Z bookmark, 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+ArrowDown extend or reduce a range from a fixed starting row. Separate marks remain selected when the range contracts. ctrl-space marks every row from the last space mark to the cursor, so a block can be marked after jumping with /, PgDn, or G. Range marks also work with combined pod logs.

  • Copy to clipboard - c copies the selected resource's name; Y opens 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 tries clip.exe from PATH to copy to the Windows clipboard, including inside tmux. This requires Windows interoperability. If clip.exe is missing or fails, sofka tries the other clipboard tools, then OSC 52.

  • Save to a file - ctrl-s in 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-s in logs saves the shown lines as .log. Both write to save_dir, the temp directory by default. See Document views.

  • RBAC-aware palette browse - the empty : list hides kinds you cannot list. An explicit search checks the full discovery catalog, because some delegated authorizers return incomplete rule reviews.

  • Resource type on context switch - :ctx keeps 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 ctx and sofka contexts open the picker before connecting to a cluster. Enter connects to the selected context and opens its configured default resource, or pods. --context NAME selects the initial context in the picker. An unknown name returns an error. -n and -A apply 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, 1 to 9 select 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. 0 selects all namespaces in both places. The header's Namespace: 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/-A override 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) selects AGE with the same direction as the sort picker. Press it again to invert. If AGE is absent, the current sort stays active.

  • Default sort - [views."*"].sort sets a global initial sort, with resource-specific overrides. Sort choices are saved per kind by default. Set remember_sort = false to 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 :reload and 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 - Backspace or Ctrl-H removes 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 = true to 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_lines tunes 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, not mouse_scroll_lines. Set mouse = false to start with the terminal's native mouse behavior everywhere. Use :mouse to 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.

Metrics and health

  • 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 the utilization thresholds and sortable, so "which node is full" is one glance and one S. %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 decimal k, P, and E, and binary Pi and Ei, 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 shows 0m, 0Mi, or 0%. 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, or Failed when the last scheduled run did not succeed.

  • Storage deletion status shows Terminating for 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, or l jumps from a finding to the pod, its events, or its logs. After opening evidence, esc returns to Explain before another esc returns to the table. Opening the view or pressing r reads 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 with esc or q cancels 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; w clips 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, so ctrl-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. O shows them too. Ctrl+Z judges each row by its kind's faults rule. Kinds missing from API discovery are skipped.

  • Namespace resources (:ns-resources [ns], R on 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: one limit=1 list per kind read off remainingItemCount (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 a no access row and any other failure shows its error, so neither reads as empty. ⏎ opens the kind's own view in that namespace and esc returns, counting again. r counts 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, while target = "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: :users and :groups list subjects from RoleBindings across all namespaces and ClusterRoleBindings. Enter opens directly bound rules. Use :policy u:alice, :policy g:developers, or :policy s:namespace/account to open a subject by name. Bare :policy uses 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 as escalate or *, are listed in OTHER. A row marked (named) applies only to the objects in RESOURCE 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. Press t to switch to the per-rule list. The choice is kept for the session. Set rbac_rule_list = true to 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 on r; Esc returns. 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-i for 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/d show its YAML or describe. Relations are data: a built-in table for core kinds, extended per CRD with [[views."…".refs]] and children. A ref whose target kind is a field of the object (an ExternalSecret's secretStoreRef.kind) reads it with kind_path from a list of candidate kinds; group_path tells apart candidates that share a kind by their API group. Reverse lookups stay in the row's namespace unless the rule says cluster. Press c in 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. c starts another search; r repeats 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.

GitOps and Helm

  • 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. No flux binary needed. Works with bulk multiselect. The flux-operator kinds (ResourceSet, ResourceSetInputProvider, FluxInstance) get the same menu. They have no spec.suspend, so suspend sets the fluxcd.controlplane.io/reconcile annotation to disabled. Resume sets it to enabled and requests a reconcile, as flux-operator resume does. 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 sets reconcile.fluxcd.io/requestedAt and reconcile.fluxcd.io/forceAt to 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 composes releaseName/storageNamespace): ⏎ shows a revision's values, y the rendered manifest, d the NOTES, r rolls back.
  • HelmRelease chart version (i) - prompts for spec.chart.spec.version, pre-filled with the current version, and patches it. A HelmRelease that uses spec.chartRef gets its version from the referenced OCIRepository or HelmChart, so i points 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 removes spec.syncPolicy.automated and stashes the original value (including prune/selfHeal/allowEmpty) as a base64 annotation so resume restores it exactly; ApplicationSet suspend sets applicationsSync to create-only (no none mode exists) and stashes the original value the same way. Refresh sets the argocd.argoproj.io/refresh annotation to normal, so the controller re-compares the Application against Git and then clears it; hard refresh sets it to hard, which also drops the repo-server manifest cache. Both also set argocd.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 its prune if configured, just as the next periodic reconcile would. Sync-now patches the top-level operation field. Sync-with-prune does the same with prune: true, so resources no longer in Git are deleted; it asks first and is gated by prune guardrails. A plain sync never prunes, even when syncPolicy.automated.prune is set, because that only applies to automated syncs. No argocd binary 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, the dependsOn edges, and ready status. Each item is a finding you can ⏎ into. Opening the view or pressing r reads the original resource again, then follows its current owner labels, source, and dependencies. A missing or replaced resource produces a warning. These reads require get access. Only the latest requested report can update the findings. Closing the view with esc or q cancels 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 has spec.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's spec.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 the resourceset.fluxcd.controlplane.io/name label. An object a FluxInstance applied finds it from fluxcd.controlplane.io/name, as flux-operator trace does.
  • 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 in status.resources[] with its own sync and health, and a summary of what is blocking - a suspended sync policy, a ComparisonError, 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 at argocd app diff for 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 with controller.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, no argocd binary. The headline names how long the current health has held, from status.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 is Degraded or Missing and 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 in status.resources[].health only when controller.resource.health.persist is 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). esc at 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. A destination.server URL matches the context whose cluster has that server. A destination.name matches 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 entry aws eks update-kubeconfig named by ARN resolves for an Argo cluster registered as eks-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. c on a managed resource expands what it owns, indented underneath - Deployment to ReplicaSet to Pod, CronJob to Job to Pod - read from the children's own ownerReferences. Child kinds come from the same rules the adjacent view uses, so a [views."…"] children entry 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's Complete or Failed. Superseded ReplicaSets scaled to zero that own no pods are left out, otherwise revisionHistoryLimit buries 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: the argocd.argoproj.io/tracking-id annotation (preferred, since it is exact) or the app.kubernetes.io/instance label (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. r re-reads the resource and follows its tracking metadata again. These reads require get and list access. Opened on an ApplicationSet directly, there is no owning Application to walk up to, so the view instead names its configured generators - unwrapping a matrix or merge generator to what it actually combines rather than reporting just "matrix" - and lists the Applications it produced, straight out of status.resources[], each one a finding you can ⏎ into. The ApplicationSet table view carries its own curated columns: STATUS from the ErrorOccurred / ResourcesUpToDate conditions, GENERATORS, and APPS - 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, like helm list. ⏎ opens the full revision history (helm history); on a revision, ⏎ shows user-supplied values, y the rendered manifest, d the NOTES.txt. r rolls back and ctrl-d uninstalls - those two shell out to the real helm binary, 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, like kubectl 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, deployed or superseded, the images, and the kubernetes.io/change-cause annotation. ⏎ reads the live workload and diffs its pod template against the selected revision's. r rolls the workload back to that revision after confirmation, like kubectl rollout undo --to-revision; the deployed revision 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. The rollback guardrail 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 a scale subresource 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.

Actions

  • CSR approve and deny (V / N on :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 a client auth cert from kubernetes.io/kube-apiserver-client, since that grants cluster access. Requests that already have an Approved, Denied or Failed condition are skipped. The decision is written through the approval subresource, so it needs the update verb on certificatesigningrequests/approval and approve on 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 the approve and deny guardrails. The table shows NAME, SIGNER, REQUESTOR, CONDITION and AGE; CONDITION reads Pending, Approved, Denied or Failed, with ,Issued once the certificate is set.
  • CronJob controls (t) - trigger now (creates a Job from the jobTemplate, like kubectl create job --from), suspend, resume.
  • Background port-forwards (f/F to start, :pf to manage) plus saved forwards that show up in :pf even while stopped, with optional autostart. Pressing f on a pod or service opens a picker listing the manifest's declared ports. Press enter to start the selected mapping, or e to edit only its local port. The edit prompt contains the current local port; esc returns to the same picker row. Mappings whose local port is already bound show in 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 (t on a pod, or t in the container picker for one container) - download from or upload to a pod via kubectl cp, off-thread with a completion flash. t on a pod uses its default container and names it in the menu and prompts. Uploads are gated by the transfer guardrail and read-only mode.
  • PVC explore (x on a PVC, or :pvc-explore) - a two-pane browser over a volume's contents, with s for 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 a kubectl.kubernetes.io/default-container annotation shows only that container, as kubectl does; a switches between it and every container. Press T to enter a duration or tail for the current kubelet log view. Lines with timestamps are sorted by time. Press t to 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+Z in 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. Press Ctrl+Z again to show all retained lines. The filter resets when a new log view opens. Copy and save use the filtered lines.
  • JSON log display (J in logs) cycles raw, record, and indented JSON. Record view shows each structured log record on one row: time, level, message, then key=value fields 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 (← / → or h / l in 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. w wraps lines instead and resets the offset.
  • Log markers (m in 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 are OutOfSync or not Healthy. A cluster without the Flux or Argo CD CRDs shows — rather than a zero. Contexts come from config or space in the :ctx switcher. ⏎ 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 same OutOfSync / not-Healthy criteria 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 deskribe library. Enable it with --experimental-describe or [experimental] native_describe = true in 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), with ctrl-f / ctrl-b (or PgDn / 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 - e in the YAML or describe view edits the displayed resource with kubectl 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 - e in the decoded Secret view (x) opens the values as plain-text stringData in $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 - m shows or hides metadata.managedFields in 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 - :diff shows a unified diff of the live object against its last-applied-configuration. When that annotation is missing - as it is for every Flux- or Helm-managed object, which nothing ever kubectl 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.

PVC explore

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 the pvc-explore guardrail action, and always confirms, naming the image and the namespace. The helper carries both a sleep and activeDeadlineSeconds, 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-clean removes 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-explore guardrail, recorded in :journal.
  • Missing tools trigger built-in recovery. If the first listing fails because sh, ls, or head is 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 the pvc-explore guardrail still apply. For an in-use ReadWriteOnce claim, the helper is scheduled on the consumer's node. An occupied ReadWriteOncePod claim cannot use a second pod. Recovery reports that restriction without creating a helper. A static subPath is preserved; a subPathExpr mount 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 with pwd -P that 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: GNU ls writes 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. busybox ls - 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.

  • c copies 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 through kubectl cp, are blocked in read-only mode, match the pvc-upload guardrail action, and are refused up front when the mount is readOnly. A download that would overwrite a local file confirms first. kubectl cp splits its arguments on the first :, so a name containing one is refused with an explanation rather than a filespec must match the canonical format from 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 cp reports 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 one kubectl exec that 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 by t on a pod - with typed paths and no row to draw on - reports itself.

    Quitting sofka while an upload is running leaves that du loop 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 the tar that kubectl cp already 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 du for a directory, so the pod needs du as well as the ls a listing needs and the tar a 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: a du that 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.

  • s opens 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 same shell guardrail as s on 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.

Manifest browser

: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.

  • enter opens a folder, or shows a manifest as a read-only document.
  • e opens the manifest in $KUBE_EDITOR or $EDITOR, suspending the TUI.
  • a shows a kubectl diff of the selection. Nothing is applied until you press a again in that diff and confirm. When nothing would change, the status line says so and no diff opens.
  • d deletes 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.

Safety

  • Read-only mode, declarative guardrails, action-aware authorization (:can-i), and a session-local action journal (:journal) with optional file persistence. See Safety.

Extensibility

  • 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 :skin picker opens on the active skin and previews each skin as you move; enter keeps it, esc restores the previous one.
  • Config file (TOML or YAML) with per-cluster and per-context overrides and live :reload. See Configuration.

Diagnostics

  • 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, or sofka 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, or sofka 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. :info shows the latest known release and its notes link. sofka never downloads or installs a release itself. Set update_check = false to turn off the startup check. See Base options.
  • Structured logging ([logging], or SOFKA_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.

Bundled plugins

  • :sanitize deletes 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 on PATH; the adapter is the sofka binary. states selects terminal (the default), stuck, or all, based on application container state and Pod phase. Specific table reason labels do not add deletion categories. dry_run=true reports without deleting. It confirms before running, is blocked in read-only mode, and matches guardrails as plugin: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/-f filter 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.

External plugin packages

  • Package discovery reads plugins/*/plugin.toml from 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. Esc hides without cancelling, Ctrl+Alt+T toggles the panel, and focused Ctrl+C cancels the process group. The toggle is configurable with [keys.global].plugin_activity. :plugin-activity opens the current run or report; Enter in a completed panel opens its report. ↑/↓, PgUp/PgDn, Home/End scroll; G follows 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.

See Create a plugin package.

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.

Popup text

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.