Features
In the order you'll meet them: open a cluster, look at a list, open a thing, act on it — then the plumbing that makes it work.
Every kind, two ways to find it.
The sidebar has two taxonomies of the same catalog. Workloads groups things the way every desktop client does, so nothing moves on day one. API groups mirrors kubectl api-resources, with the exact command as each link's tooltip. Toggle between them; nothing refetches.
Type in the sidebar to narrow it. Fuzzy, prefix-aware, and it matches group names too — cert-manager.io keeps that whole section. The order never changes under you.
In the docs: Browse.
Your operators' types, without a plugin.
Sternpost knows the ~35 kinds every cluster has. It cannot know that yours also serves Orders, Applications and RabbitmqClusters — so it asks on connect and adds them to the sidebar, one group per API group, in both taxonomies.
Columns come from the CRD's own additionalPrinterColumns — what the operator's author decided matters, including the ones kubectl hides behind -o wide. The detail page walks the spec and status and hangs the CRD's schema off each row as a tooltip, which is usually the only documentation those types have.
And it follows the pointers. An Order says it's controlled by its CertificateRequest; a Certificate links to its Secret and its ClusterIssuer; a Pod that Argo created says so. Only when the link would be right — a wrong link is worse than none.
In the docs: Custom resources.
Watches, not polling.
Kubernetes has native subscriptions, so Sternpost uses them: list once, then hold a watch and apply changes as they arrive. kubectl get -w is a thin wrapper over the same endpoint — Sternpost speaks it directly rather than shelling out.
When a watch drops — network, expired credentials — it reconnects with backoff and says so. Watches start when you first look at a kind and are dropped after five minutes idle, so the app doesn't pay for what you're not looking at.
In the docs: How Sternpost works.
One word on how it's doing.
Every detail page opens with a word — Running, Rolling out 2/3 updated, Complete, Failed, CrashLoopBackOff, Cordoned, Suspended — the number behind it, and a dot in the matching tone, in a card with the object's age and the image it runs. Chosen to match what kubectl rollout status and kubectl get would say. Conditions took three rows of True (MinimumReplicasAvailable) to say "Available".
The word is live on its own: on every change to the cluster it refetches that one word, without re-reading the rest of the page. The pods table and the rollout history below do the same for their rows.
In the docs: Resource pages.
The page knows what it's connected to.
The word sits in a card with the two things you'd ask next: how old the object is, and the image it runs — the first one, with Show more for a pod with sidecars. On a Deployment or a CronJob the age carries a second number, 260d • 2d: how long it has existed, and when it last did something. The line above the name says what you're looking at — Service • production — so a Pod and its Deployment never read the same.
Beside the details, Related resources. A Deployment's page lists the Services in front of it, the Ingresses routing to them, the HPA scaling it, the ConfigMaps and Secrets it mounts and the Nodes its pods run on; a Service lists its pods and the Ingresses naming it; a Node lists what runs on it, followed up to the Deployment. Each name is a link. Four generic rules over the object's own pointers and selectors, not a table of kinds.
A pod's page has a Containers section: every init container, sidecar and app container in start order, with its state, restarts, last exit, how long it ran and its image — live, like the rest of the page. Switch to Timeline to see each run on one time axis, back-off and all. Logs and Shell on a row open the panel below on that container.
Every page ends with Events, folded like Labels and Annotations: open it for what the cluster has said about this object, newest first, Warnings marked. Read when you open it, never polled; a refresh button reads it again.
In the docs: Resource pages.
A Deployment's logs, not a pod's.
Open a Deployment, StatefulSet, DaemonSet, ReplicaSet or Job and get the logs of every pod behind it, merged, each line tagged with the pod it came from. Mute a pod and the lines already on screen filter too. Up to twenty pods per stream. A rollout is followed to its new pods, and a workload with one pod names it once in the toolbar, as a link, not on every line.
Streams by default, from the API server's follow endpoint. Autoscroll follows only if you were already at the bottom — it never yanks you back down while you're reading. A log opens with its last 200 lines from each container — a setting, up to 5,000, under Settings → Logs — and searching the entire log looks past them.
Search as you type through the lines on screen, or search the entire log the cluster still has and get back only the matches. Between two matches that were far apart sits a gap row saying how many lines it hides; open it twenty lines at a time, from either end.
A pod with several containers lists them beside the lines, in start order; click one to read its log, fold the list away when you want the width. The log opens on the container that matters: one that's crashing or can't pull its image, else the init step running right now, else the app container.
Open in window puts the log in its own window with no sidebar and no command palette, so it can live on the other monitor. Open in Console streams it into a file and hands it to your system's log viewer — on macOS, Console keeps showing new lines as they arrive, with its own search. Download full log saves every pod's full log as one file, each line tagged the same way.
JSON logs read like logs.
Most services write one JSON object per line, and a wall of braces doesn't read at a glance. The Structured view lays each line out as time, level and message, then the other fields as dimmed key=value, the level in its colour. Banners, tracebacks and anything else that isn't JSON stay where they came, as text.
Which key is the time and which the level depends on the logger, so Sternpost works it out per container, from the lines themselves. 20 formats are built in, plus a generic one for any other JSON: pino, bunyan, winston, zap, logrus, slog, zerolog, klog, structlog, loguru, Serilog, ECS, OpenTelemetry, nginx and Envoy access logs among them. A pod whose sidecar logs differently gets a reader for each container. The { } button at the start of the toolbar holds the view, the format and the minimum level; it wears a small check when a format was detected, and its tooltip names it.
- Click a line to open it: every key as a tree, the stack trace with its line breaks back, Copy raw and Copy pretty JSON.
- Double-click it, or press ⇧ Shift + ↵ Enter, for the inspector beside the log: the whole object as a tree or pretty JSON, a filter for keys, and ↑ Up/↓ Down to the previous or next line. In Raw, ⌥ Option-click a line.
- Set a minimum level and the lines below it fold into thin separators you can open. Search entire log keeps to the level too.
- Any key can be a column. The view, the format, the columns and the level are remembered per workload and container.
- New format from this line opens the format editor over the log, in the window you're in: a Name, autocomplete, a live preview, and the log is drawn with it the moment you save. Settings → Logs holds them all: clone a built-in, import and export them, or import a JSON Schema you already have. Yours sit under Yours in the Format select.
- Nothing to install, no jq piped in. Structured is the default and Raw is one switch away, without a line lost or fetched again. Prefer Raw? Settings makes it the default.
In the docs: Structured logs, Log formats, Log format reference.
A shell in the container.
The second tab on a pod: a terminal over the API server's exec endpoint. No kubectl on PATH, auth from your kubeconfig, so exec plugins work here too. The pod's containers are listed beside it, as they are beside the log.
Auto finds bash, then ash, then sh. Or pick one. Whichever it is, you get a prompt with the shell-integration mark iTerm2 and VS Code use, so your terminal knows where each command starts.
Multi-byte characters that straddle frames, keystrokes that must not reorder, a tab switch that must not kill the shell — handled.
In the docs: Terminal.
Port forwards that survive a rollout.
A rule listens on a local port and tunnels each connection through the API server to a Pod, a Service or a workload. Three things neither the CLI nor the desktop clients built on it do:
- The pod is chosen per connection. kubectl port-forward svc/x — and every client that wraps it — picks a pod at start and dies with it. Here a Service's selector is evaluated as each connection opens and a Ready pod is chosen. A rollout costs you only the connections that were open.
- A refused connection is a fact about that connection. The rule stays up and shows the kubelet's own words. Only a rule that can't listen goes red.
- Rules survive a restart. Enabled rules come back when the app does.
- And when one can't listen — usually another app took its port while Sternpost was closed — the page says so above the table, and Fix names the process holding the port, offers a free one in one click, or stops that process after a confirmation. Each rule has an edit button for its ports anyway.
- Add one from any workload's page, or from ⌘ Cmd + K: type forward, pick the pod, deployment or service, pick the port.
In the docs: Port forwarding.
The number, and what it's a share of.
CPU and memory in every pod and node table, printed like kubectl top — with a line under each for its share of the limit (or request, or a node's allocatable). Green under 60%, amber to 90%, red past it. 317Mi says nothing on its own; the line says whether that's a third of the limit or all of it, and says it to peripheral vision down forty rows.
Detail pages get a Metrics section: from Prometheus, thirty minutes of CPU, memory, disk and network with the limit drawn as a line; from metrics-server, a gauge each for CPU and memory. Works with either; if neither is there, the table still renders.
In the docs: Metrics.
Restart, scale, run now. Confirmed, attributable.
Restart is kubectl rollout restart — the same annotation kubectl uses, so the two don't leave separate marks. Scale shows the change (3 → 5) and won't send the count you're already at. Run now on a CronJob creates a Job the way kubectl create job --from would, named so you can tell it apart in the list.
Every action confirms first, in one dialog that recaps cluster, kind, namespace and name. A rollout on production shouldn't be one stray click away. Every write carries fieldManager=sternpost.
Which verbs ⌘ Cmd + K offers is a setting. The buttons next to the thing stay.
In the docs: Actions.
⌘ Cmd + K, then the name.
Find a resource by name anywhere in the cluster and jump to it. Fuzzy — smrtkiosk finds smartkiosk-1. Indexed in memory from the watch cache, rebuilt when the cluster changes, not per keystroke.
The same box runs commands: restart, scale, forward a port. Type the verb, pick the target, confirm. Type logs or terminal and land on a pod's log or a shell into it, full screen. Type settings, then the name of what you're after — dark, fuzzy, tail — and Enter takes you to that row, lit up.
Lists have their own filter over the rows already on screen: substring, regex or fuzzy. A half-typed regex is a message, not a blank page.
In the docs: Command palette, Keyboard shortcuts.
Restart it from wherever you are.
A system-wide shortcut brings the palette up over whatever you're doing, with or without a Sternpost window open. Cluster and namespace scope sit right above the field. Pick a thing and it opens in Sternpost.
Commands run right there. Type restart, scale or delete, pick the target, confirm in the panel — done, and you never left your editor or opened the app.
In the docs: The palette from anywhere.
Back to where you were.
‹ and › in the header step through the pages this window has been on, and the keyboard does the same — the Command key with the arrow keys on a Mac, Ctrl with them elsewhere — except while a field has the caret, where the arrows keep moving it. Change the keys, like every shortcut, in Settings → Keyboard shortcuts.
Hold, right-click or ⌥ Option-click either arrow for the list: the pages ahead, the one you're on, marked, then the pages behind, the one you just left first — each with its kind and namespace under the name. Arrow keys move through it, Enter goes. How many pages it keeps is under Settings → App; twenty to begin with.
Settings counts too: each section you open is a step, so ‹ returns to the section you were on instead of leaving Settings.
In the docs: Keyboard shortcuts.
Columns that measure themselves.
The table looks at what's in each column and sizes it: a column of short statuses is narrow, a column of image names is wide, and nothing wraps mid-word. No horizontal scroll until there genuinely isn't room.
Pods, ReplicaSets and Jobs have a Controlled by column: the ReplicaSet, StatefulSet, DaemonSet, Job, Deployment or CronJob behind each row, one click away. Sort by it to group rows by what controls them, or type a controller's name into the filter to see only what it runs.
In the docs: Resource lists.
Your kubeconfig, referenced, not copied.
Add a cluster by pointing at a kubeconfig and a context. The first time you open Add cluster, Sternpost asks before it reads the file and reads nothing until you say yes. What it keeps is the path and the context name — it copies nothing out, and the credentials inside are used only for the clusters you add. Paste YAML instead and your kubeconfig is never opened; the paste is written 0600 under the app's data dir and deleted with the cluster.
Give each cluster a colour. It tints the status bar so you always know which one you're in, and the contrast is worked out per colour so the text stays readable.
The connectivity dot sits inside the cluster's name, not across the bar from it. In the status bar, next to the name: the Kubernetes version the API server runs; click it for About this cluster — build, platform, node count, context and API server.
Settings → Privacy & permissions lists every choice you've made — the terms, the kubeconfig, version info, agents — and is where you change your mind.
In the docs: Clusters.
No cluster to hand? Run a fake one.
Add cluster → Try a fake cluster demo downloads a small Kubernetes control plane and runs it on your Mac, on 127.0.0.1 only, filled with a product team's worth of workloads to click around in: a real API server with pretend nodes, nothing on the network. The dialog says exactly what it will do first — which files, from where, how big, and the checksum each is held to — and one button does it.
Remove on the Clusters page puts it all away. Experimental, and macOS on Apple silicon only for now; nothing in its pods really runs, so logs, terminals, port forwards and usage columns stay empty.
In the docs: Try it without a cluster.
Develop next to prod.
Open any page in a second window. Each window keeps its own cluster and its own namespace scope, so one can sit on develop while the other watches prod, whether they're two clusters or two namespaces on one.
Pick a cluster that already has a window and Sternpost takes you to it instead of opening it again. Settings → Windows decides what a plain click does.
In the docs: Windows.
Works for namespace-scoped users.
Pick several namespaces and Sternpost makes one scoped request each, in parallel — not one cluster-wide list filtered on your machine. That's what a user without cluster-wide permission can actually do, and it doesn't drag the whole cluster over the wire to show two namespaces. If some are forbidden, you get the rows you can see and a note naming the rest.
Search covers them too. With an account that can't read the whole cluster, pick your namespaces in Scope and ⌘ Cmd + K watches up to eight of them one at a time instead of finding nothing. Settings → Search says which kinds are searched this way, and which of your namespaces the account can't read.
In the docs: Namespaces.
Light, dark, or whatever the OS says.
Settings → Appearance: Light, Dark, or System to follow the OS. The whole app changes at once — every window, the manifest editor, the terminal and the window's own title bar — and the choice is remembered for the next launch.
The accent follows your system's colour, or one you pick, with Sternpost's own orange one click away; the tables come in four densities. Every screenshot on this site exists in both schemes, and you're seeing the one your machine asked for.
In the docs: Settings.
When something breaks, it says what.
A page that can't load says which kind of failure it is — nothing at this address, a page that couldn't load, the app's own server not answering — and offers the way out: try again, go home, reload or relaunch. An error outside a page, in a click, a stream or a timer, is a small notice in the corner. A window or app crash is noticed too and offered the same way at the next start; Debug → Errors lists them.
Each one has Copy details and Report on GitHub, which opens an issue with the record already in it for you to read before posting. Three things are replaced first: the account name in file paths, anything that looks like a credential, and e-mail addresses. Cluster and resource names stay — they're what a report is about, and you see the issue before it's posted. Nothing is ever sent on its own: no crash reporter, no analytics.
For everything that isn't an error, Feedback in the status bar opens a new GitHub issue with your Sternpost version filled in.
In the docs: Troubleshooting.
What it doesn't do (yet)
- Custom resources aren't in ⌘ Cmd + K and aren't watch-backed.
- Port forwards are TCP and loopback. Open connections end when the app quits; the rules come back with it.
- Lists stop at 10,000 rows and say so.
- The fake cluster demo is macOS on Apple silicon only, and nothing in its pods runs.
Try it on a real cluster.
Or on a fake one: the app can download and run a small KWOK cluster on your Mac to click around in, nothing on the network. Experimental, Apple silicon only for now. How it works