<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: r</title>
    <description>The latest articles on DEV Community by r (@cyb3rkn1ght).</description>
    <link>https://dev.to/cyb3rkn1ght</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4070144%2F59a9fd8b-dfca-429e-aedf-6fdafc4dc641.jpg</url>
      <title>DEV Community: r</title>
      <link>https://dev.to/cyb3rkn1ght</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cyb3rkn1ght"/>
    <language>en</language>
    <item>
      <title>Read-only by construction: why instructions aren't a security boundary for an AI agent in a Kubernetes cluster</title>
      <dc:creator>r</dc:creator>
      <pubDate>Sat, 22 Aug 2026 07:03:17 +0000</pubDate>
      <link>https://dev.to/cyb3rkn1ght/read-only-by-construction-why-instructions-arent-a-security-boundary-for-an-ai-agent-in-a-17d0</link>
      <guid>https://dev.to/cyb3rkn1ght/read-only-by-construction-why-instructions-arent-a-security-boundary-for-an-ai-agent-in-a-17d0</guid>
      <description>&lt;p&gt;I keep running into the same setup: take an LLM, give it access to &lt;code&gt;kubectl&lt;/code&gt; or the k8s API, write something like "you can only read, never delete or change anything" into the system prompt or a connected skill, and consider the problem solved. I've been through this myself, and at some point I realized: that's not a security boundary, it's a polite request.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical risk: you've probably heard the story — in July 2025, a Replit agent deleted SaaStr's production database despite an explicit instruction not to touch anything. Not Kubernetes, not MCP, but the same pattern: the instruction "don't touch this" was right there in context — there was just nobody to enforce it except the model itself. Giving an agent write access to a k8s cluster means assembling exactly the same setup that already cost SaaStr their database.&lt;/p&gt;

&lt;p&gt;I'm far from the first to write about this, and plenty of read-only MCP servers have shown up recently. What's surprising is how often "read-only" gets implemented wrong. For Kubernetes MCP servers specifically, "read-only" is often just an environment variable that filters the &lt;code&gt;tools/list&lt;/code&gt; response, not the absence of a function in the registry. That's exactly how &lt;code&gt;mcp-server-kubernetes&lt;/code&gt; (20K weekly npm downloads) was built: the &lt;code&gt;ALLOW_ONLY_READONLY_TOOLS&lt;/code&gt; flag hid mutating tools from the list, but &lt;code&gt;tools/call&lt;/code&gt; still accepted &lt;code&gt;kubectl_delete&lt;/code&gt; directly, bypassing the filter. The result was &lt;strong&gt;CVE-2026-46519&lt;/strong&gt;, CVSS 8.8 — the exact principle this article is about, taken all the way to a real exploit: a function hidden from the list isn't the same as a function that doesn't exist. And it's not just community projects — &lt;code&gt;Azure/mcp-kubernetes&lt;/code&gt;, Microsoft's own official Kubernetes MCP server, is built the same way: &lt;code&gt;--access-level readonly|readwrite&lt;/code&gt; instead of the absence of mutating tools in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why instructions don't work as a boundary
&lt;/h2&gt;

&lt;p&gt;A model is not a sandbox. If &lt;code&gt;delete_pod&lt;/code&gt; or &lt;code&gt;scale_deployment&lt;/code&gt; is in its list of available tools, it can technically call it regardless of what the system prompt says. For example, an attacker doesn't need cluster access for this — an ordinary HTTP request with a spoofed header value like User-Agent is enough. Nginx, or the pod itself, will log it verbatim: Kubernetes just captures the container's stdout/stderr, with no sanitization at all. Later, someone (or the assistant itself) asks the model to "check this pod's logs" — a completely innocent request — and the model reads that line as part of its context, with no way to tell it apart from the system prompt. Same story with an instruction from a connected skill or another plugin, which ends up in context just as "trusted"; with a jailbreak; with an ordinary hallucination while trying to "fix" a problem you only asked it to explain. An instruction — whether in the system prompt, in a skill, or in a container log — is data the model interprets, not code that constrains it.&lt;/p&gt;

&lt;p&gt;So the only boundary that actually holds is which tools exist at all in the registry it has access to. If the &lt;code&gt;delete_pod&lt;/code&gt; function doesn't exist, it doesn't matter what a prompt injection, a jailbreak, or the model itself in a fit of "helpfulness" says — there's nothing to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in the tool registry
&lt;/h2&gt;

&lt;p&gt;Take a concrete example: an MCP server for Kubernetes. It makes sense to register only read tools in it — &lt;code&gt;list_pods&lt;/code&gt;, &lt;code&gt;list_deployments&lt;/code&gt;, &lt;code&gt;get_yaml&lt;/code&gt;, &lt;code&gt;get_events&lt;/code&gt;, &lt;code&gt;read_pod_logs&lt;/code&gt;, &lt;code&gt;start_pod_log_stream&lt;/code&gt;, and so on, on the order of thirty of them. And not a single &lt;code&gt;delete_*&lt;/code&gt;, &lt;code&gt;scale_*&lt;/code&gt;, &lt;code&gt;exec_*&lt;/code&gt;, &lt;code&gt;apply_*&lt;/code&gt;, or &lt;code&gt;port_forward_*&lt;/code&gt; — not because they're switched off by some flag, but because those functions simply don't exist in the code.&lt;/p&gt;

&lt;p&gt;All mutating operations — scale, rollout restart, delete, cordon/drain — live on a separate, human-only path in a design like this: through a GUI with a confirmation dialog, through a CLI with an explicit flag or a y/n prompt, doesn't matter which — what matters is that a human confirms it, and only then does a direct call to the Kubernetes API happen, with no model and no AI tool registry involved at all. These are two separate code paths, not one gated by an allowed/forbidden flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  One server, two transports
&lt;/h2&gt;

&lt;p&gt;A separate problem shows up when an AI assistant is available in two forms: as an embedded panel inside a larger tool, and as a standalone binary for external MCP clients (Claude Desktop, Claude Code, etc.). The temptation is to throw together a separate tool set for the embedded version. Over time the two sets drift apart, and one ends up with an extra tool the other doesn't have.&lt;/p&gt;

&lt;p&gt;More reliable: bring up the same MCP server in both cases and talk to it for real over the MCP protocol, just over different transports — in-memory for the embedded version, stdio for the external client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcpserver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;serverTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientTransport&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewInMemoryTransports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serverTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"desktop-assistant"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// the same ListTools any external MCP client would call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means there's physically one server and one list of read-only tools in the codebase — not an original plus a separate copy for the GUI that someone forgets about. In practice, this closes off exactly one class of bug: the one where, six months into a refactor, a tool gets added to one list and someone forgets the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-only doesn't mean "nothing is visible"
&lt;/h2&gt;

&lt;p&gt;Read-only solves the state-mutation problem, but not the problem of leaking data that's already sitting in the cluster. If the kubeconfig has access to read a Secret in a namespace, then in theory so does the model, by calling &lt;code&gt;get_yaml&lt;/code&gt;. This has to be fought at the data layer, not the prompt layer: Secret values get redacted before the YAML ever reaches the tool response —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;redacted&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and the AI (whether the embedded assistant or an MCP client) physically never sees the decrypted value, because it's replaced with a placeholder before the YAML string is even formed. It's also worth rejecting an &lt;code&gt;apply&lt;/code&gt; if the YAML still contains &lt;code&gt;&amp;lt;redacted&amp;gt;&lt;/code&gt; — otherwise the placeholder could accidentally overwrite a real value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this approach doesn't solve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The model can still &lt;strong&gt;read&lt;/strong&gt; a lot of data you already have RBAC access to. Read-only limits what it can &lt;strong&gt;do&lt;/strong&gt;, not what it can &lt;strong&gt;see&lt;/strong&gt; within the same permissions.&lt;/li&gt;
&lt;li&gt;Read-only by itself doesn't limit load on the API server from a chatty tool-calling loop either — there are just reasonable measures here (a cap on iterations per conversation, a byte limit on tool results, timeouts on log reads, capped and idle-reaped streams), not some cryptographic guarantee.&lt;/li&gt;
&lt;li&gt;If it matters that the prompt and whatever the tools read never leave the machine at all, that's a separate setup (a local model via Ollama/vLLM/LM Studio), not a consequence of the read-only architecture itself.&lt;/li&gt;
&lt;li&gt;A compromised registry or altered tool descriptions are a separate story: tool poisoning works against read-only tools too, if the model trusts an instruction inside a description the same way it trusts the system prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole approach described here is simple exactly because it deliberately doesn't solve the more general problem — giving an agent any write access at all. If that's genuinely needed (for production debugging with the ability to actually fix something, say), that's a fundamentally different, much heavier architecture: whitelisting specific commands, rate-limiting, role-based restrictions, an immutable audit log with alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't tied to a specific model
&lt;/h2&gt;

&lt;p&gt;Since the boundary is the absence of a function, not model behavior, the guarantee works the same regardless of which engine you use — the Anthropic API, an existing Claude Code/Codex CLI login, or a local model via Ollama. You don't have to trust any particular vendor's alignment — the tool registry is the same one for everyone who connects to it, no matter who they are.&lt;/p&gt;




&lt;p&gt;If you want to look at the concrete Go implementation, it's open — I added an MCP server to a tool I originally built for myself, and later open-sourced. Repository: &lt;a href="https://github.com/cyb3rKn1ght/nereida" rel="noopener noreferrer"&gt;https://github.com/cyb3rKn1ght/nereida&lt;/a&gt; (&lt;code&gt;cmd/nereida-mcp&lt;/code&gt; has a separate README about the MCP server itself).&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>ai</category>
      <category>security</category>
      <category>mcp</category>
    </item>
    <item>
      <title>nereida — a local multi-cluster Kubernetes client I built because on-call kept beating me</title>
      <dc:creator>r</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:21:08 +0000</pubDate>
      <link>https://dev.to/cyb3rkn1ght/nereida-a-local-multi-cluster-kubernetes-client-i-built-because-on-call-kept-beating-me-22l3</link>
      <guid>https://dev.to/cyb3rkn1ght/nereida-a-local-multi-cluster-kubernetes-client-i-built-because-on-call-kept-beating-me-22l3</guid>
      <description>&lt;p&gt;It's 2am, an alert fires, and I'm three &lt;code&gt;kubectl config use-context&lt;/code&gt; commands deep trying to remember which cluster the broken pod is actually in. My AWS session from an hour ago has expired, so first I have to re-run &lt;code&gt;saml2aws&lt;/code&gt;, wait for the browser SSO round-trip, and &lt;em&gt;then&lt;/em&gt; start looking for the pod. By the time I've found it, tailed its logs, and confirmed it's not the same one that OOMKilled twenty minutes ago, I've burned five minutes on tooling instead of the incident.&lt;/p&gt;

&lt;p&gt;I'm a backend dev who does on-call, not a platform engineer, and I got tired of this specific flavor of friction. I'd used k9s before and liked the idea — one place for everything — but mid-incident I don't want to be recalling which keybinding does what on top of the actual incident. I wanted something just as simple, minus the command-memorizing, that also logged me into AWS/EKS without a separate &lt;code&gt;saml2aws&lt;/code&gt; dance, and let me point an LLM at the mess in &lt;strong&gt;read-only&lt;/strong&gt; mode so it could help me reason through what's broken without any risk of it accidentally deleting, scaling, or draining something. So I built &lt;strong&gt;nereida&lt;/strong&gt;: all of that, in one window, with tables and buttons instead of a command palette to memorize.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcyb3rKn1ght%2Fnereida%2Fv0.1.0%2Fdocs%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcyb3rKn1ght%2Fnereida%2Fv0.1.0%2Fdocs%2Fdemo.gif" alt="nereida demo" width="560" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No commands to memorize.&lt;/strong&gt; Everything's a table, a click, or a confirmation dialog — cluster switching, log tailing, exec, restarts, all of it. If you've used k9s and liked the "one place for everything" idea but never quite internalized its keybindings, this is the same idea as a GUI instead of a TUI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One view across clusters.&lt;/strong&gt; Pick any set of kubeconfig contexts and see Pods, Deployments, StatefulSets, DaemonSets, Jobs, CronJobs, Services, Ingresses, ConfigMaps, PVCs, HPAs, Events, Nodes, and Secrets in unified tables — every row tagged by cluster, so you're not tabbing between windows to figure out where something lives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8z5elp7i3eq37e6mtud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8z5elp7i3eq37e6mtud.png" alt="Pods across two clusters in one table" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One log stream for a workload.&lt;/strong&gt; Merge logs from every pod behind a Deployment, StatefulSet/DaemonSet, or label selector — with follow, tail/since, timestamps, and previous/init/ephemeral container support. No more opening five terminal tabs to watch five replicas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Femv895ij4mrfl9wlf977.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Femv895ij4mrfl9wlf977.png" alt="Events merged across clusters" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash context where you need it.&lt;/strong&gt; Recovered pods show their previous termination right beside the restart count — &lt;code&gt;3 · OOMKilled (5m ago)&lt;/code&gt; — instead of making you dig for it. Nodes surface active memory, disk, and PID pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS/EKS SSO without the CLI dance.&lt;/strong&gt; Native Keycloak SAML SSO login discovers every AWS role your SAML assertion grants and the clusters reachable from each, then writes credentials and kubeconfig entries directly — connect clusters under different roles side by side without one role switch affecting clusters already connected under another. No &lt;code&gt;saml2aws&lt;/code&gt;, no AWS CLI. I could have just fixed my &lt;code&gt;saml2aws&lt;/code&gt; config instead, but that still leaves you re-running a separate CLI step before you can even open the tool you actually need — this way logging in &lt;em&gt;is&lt;/em&gt; opening the tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev6u87rgdo3lfucbu3dz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev6u87rgdo3lfucbu3dz.png" alt="Connect AWS dialog" width="799" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rest of the toolbox:&lt;/strong&gt; exec into a container with a real terminal, port-forward with a panel of open tunnels, lifecycle actions (scale, rollout restart/pause/resume, rollback, delete, CronJob trigger/suspend/resume, node cordon/uncordon/drain — each with a confirmation and pinned to the resource UID so a stale name can't hit the wrong object), per-resource YAML editing behind a diff confirmation, and view/context/namespace selections that survive restarts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zt6adfj6uwgrh51ymay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zt6adfj6uwgrh51ymay.png" alt="Pod detail view" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back to that 2am page: this is what it looks like now. One table shows which of the two clusters the pod is actually in, its previous termination already reads &lt;code&gt;OOMKilled&lt;/code&gt; instead of me digging for it, and the AWS session is already valid because logging in happened inside the app, not as a prerequisite CLI step before I could even start looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other hook: a read-only AI assistant and MCP server
&lt;/h2&gt;

&lt;p&gt;Separate from the core client, nereida ships an optional AI assistant for diagnosing clusters, plus a standalone &lt;a href="https://github.com/cyb3rKn1ght/nereida/blob/main/cmd/nereida-mcp/README.md" rel="noopener noreferrer"&gt;read-only MCP server&lt;/a&gt; you can point any MCP client at. Bring your own engine — Anthropic API key, your existing Claude Code or Codex CLI login, or any OpenAI-compatible endpoint, including local models via Ollama/vLLM/LM Studio.&lt;/p&gt;

&lt;p&gt;The important part: both are &lt;strong&gt;read-only by construction&lt;/strong&gt;. They only register fetch/list/log-streaming tools — there's no scale, delete, drain, edit, or exec tool for a model to call, so there's no path for an AI assistant to change cluster state, however it's prompted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0yv7whsfdcckg3lqluj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk0yv7whsfdcckg3lqluj.png" alt="AI assistant panel" width="799" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No accounts, no telemetry, no hosted backend
&lt;/h2&gt;

&lt;p&gt;nereida runs entirely on your machine using &lt;a href="https://github.com/kubernetes/client-go" rel="noopener noreferrer"&gt;client-go&lt;/a&gt; — the same library &lt;code&gt;kubectl&lt;/code&gt; uses — against your existing kubeconfig, credentials, and RBAC. There's no nereida server in between, no telemetry, no account to create. Secret values stay redacted in the YAML view until you explicitly hit Reveal, and AI tools only ever see redacted values.&lt;/p&gt;

&lt;p&gt;The one exception: if you turn on the AI assistant, that turn's prompt and whatever its read-only tools fetch (resource lists, redacted YAML, events, log excerpts) go to whichever model endpoint you configured. Point it at a local model and nothing leaves your machine. Nothing is sent unless you use the AI features — full details in &lt;a href="https://github.com/cyb3rKn1ght/nereida/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's rough
&lt;/h2&gt;

&lt;p&gt;I'd rather undersell this than oversell it: nereida is early-stage. It's macOS-first (Apple Silicon and Intel, built and used daily there) — Linux x86-64 is best-effort and hasn't had real-world mileage from anyone but me yet, and Windows currently compiles but isn't packaged. The macOS builds are also unsigned for now (no Apple Developer Program cert yet), so first launch needs one workaround — see below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing it
&lt;/h2&gt;

&lt;p&gt;Prebuilt macOS and Linux builds are attached to each &lt;a href="https://github.com/cyb3rKn1ght/nereida/releases" rel="noopener noreferrer"&gt;release&lt;/a&gt;. Since the macOS build is unsigned, first launch needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-cr&lt;/span&gt; /path/to/nereida.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Right-click → Open no longer bypasses this on macOS Sequoia+. If you'd rather skip Terminal: open the app once to trigger the block, then go to &lt;strong&gt;System Settings → Privacy &amp;amp; Security&lt;/strong&gt;, find where it says &lt;code&gt;"nereida" was blocked...&lt;/code&gt;, and click &lt;strong&gt;Open Anyway&lt;/strong&gt;.)&lt;/p&gt;

&lt;p&gt;To build from source (needed for Windows, or the standalone MCP server): Go 1.26+, Node 20.19+/22.12+, and the &lt;a href="https://wails.io/docs/gettingstarted/installation" rel="noopener noreferrer"&gt;Wails CLI&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
wails build   &lt;span class="c"&gt;# app bundle ends up in build/bin/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why "nereida"?
&lt;/h2&gt;

&lt;p&gt;Nereids are sea nymphs from Greek mythology who protected sailors and calmed storms. Kubernetes means "helmsman" in Greek — the name felt fitting for something meant to make incidents calmer.&lt;/p&gt;




&lt;p&gt;It's &lt;a href="https://github.com/cyb3rKn1ght/nereida/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;Apache-2.0&lt;/a&gt; and on &lt;a href="https://github.com/cyb3rKn1ght/nereida" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I built it to solve my own on-call friction, but if it saves you a few minutes mid-incident too, I'd love to hear about it — and if it's useful, a star helps other people find it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
