Skip to main content
BlogDevOps

kubectl-ai Guide: Confirmation Gate, Limits and MCP Server Risk

kubectl-ai read from its source: how the confirmation gate classifies commands, what quiet mode and maxIterations do, and why the MCP HTTP server needs OAuth.

Matthews Wong

Software Engineer

October 202612 min read
kubectl-ai Guide: Confirmation Gate, Limits and MCP Server Risk

Key Takeaway

kubectl-ai asks for confirmation before any command its classifier does not mark read-only, but that classifier is a heuristic, and its MCP server mode skips the prompt and listens unauthenticated on every interface by default. Treat the prompt as a convenience, enforce limits with a read-only RBAC identity, and enable OAuth before exposing the HTTP endpoint.

I wanted to know one thing about Google's kubectl-ai before pointing it at a cluster: when it says it will ask before changing anything, what exactly decides what counts as a change? The README says skipPermissions defaults to false and that resource-modifying commands need confirmation. That is a sentence about intent. I wanted the function behind it.

So this guide is built from the source, not the marketing. I read the README, the MCP server documentation, and three Go files from the main branch at commit 2c8ff82 (15 July 2026), then built the binary and a throwaway test harness on my own machine to ask the classifier ten questions and the MCP server one. The latest release at the time of writing is v0.0.31 from March 2026, so main is ahead of it. Everything marked measured below is my own run, and anything I could not confirm is left out.

What kubectl-ai is, and the four ways to run it

kubectl-ai is an open-source Go agent that turns a request such as how is the nginx app doing into kubectl and bash calls, runs them, reads the output and keeps going until the task is done. It installs from a release binary, an install script or Krew (kubectl krew install ai), and because kubectl treats any executable named kubectl- on your PATH as a plugin, kubectl ai works too. Gemini is the default provider, with vertexai, azopenai, openai, grok, bedrock, ollama and llama.cpp also listed in the README.

# Pick one. All three are documented in the README.
curl -sSL https://raw.githubusercontent.com/GoogleCloudPlatform/kubectl-ai/main/install.sh | bash
kubectl krew install ai          # then invoke it as: kubectl ai

# Gemini is the default provider; the key comes from the environment.
export GEMINI_API_KEY=your_api_key_here

kubectl-ai                                   # 1. interactive chat, keeps context
kubectl-ai --quiet "fetch logs for nginx app in hello namespace"   # 2. one-shot
cat error.log | kubectl-ai "explain the error"                     # 3. stdin pipeline
kubectl-ai --ui-type web --ui-listen-address localhost:8888        # 4. web UI

# Local model: small models need the tool-use shim to emit tool calls at all.
kubectl-ai --llm-provider ollama --model gemma3:12b-it-qat --enable-tool-use-shim

It runs four ways: an interactive chat that keeps context, a one-shot run with --quiet, a stdin pipeline, and a web UI. The README labels the project as not an officially supported Google product, and not eligible for Google's open source vulnerability rewards programme. That is worth reading before you give it credentials to anything that matters. The commands below are the ones the README documents.

The confirmation gate, read from the source

Every tool in the agent implements a CheckModifiesResource method that returns one of three strings: yes, no or unknown. The kubectl classifier parses the command with a shell parser, finds the verb and compares it against two hard-coded lists, one of read-only verbs and one of write verbs. A write verb with a --dry-run flag is downgraded to no.

The detail that matters is in the agent loop in conversation.go. The gate does not ask whether a command is a write. It asks whether the answer is anything other than no. So unknown fails closed: a verb the lists do not know, a flag in an unexpected position, a command that is not kubectl, or a pipeline with more than one command all produce a prompt. The code comment on the multi-command case cites the lethal trifecta, which tells you the authors were thinking about exfiltration, not only accidental deletes.

I ran the classifier from a test file placed next to the package. Here is what it returned, and whether the user would have been asked.

// pkg/tools/zz_probe_test.go: dropped next to the package, then
// go test ./pkg/tools -run TestProbe -v
package tools

import "testing"

func TestProbe(t *testing.T) {
	for _, c := range []string{
		"kubectl -n prod get pods",                  // spaced flag before the verb
		"kubectl get secret db -o yaml",             // a read that leaks
		"kubectl auth reconcile -f rbac.yaml",       // verb is "auth"
		"kubectl get pods | xargs rm",               // composite command
	} {
		t.Logf("%-45s => %s", c, kubectlModifiesResource(c))
	}
}
CommandClassifier saysPrompt shown?
kubectl get pods -n prodnoNo, runs at once
kubectl -n prod get podsunknownYes: a spaced flag before the verb cannot be parsed
kubectl delete pod web-1yesYes
kubectl delete pod web-1 --dry-run=clientnoNo, dry-run is treated as a read
kubectl get pods; kubectl delete ns prodyesYes
kubectl get pods | xargs rmunknownYes: composite commands are never trusted
kubectl get secret db -o yamlnoNo, and the Secret lands in the chat context
kubectl auth reconcile -f rbac.yamlnoNo, although it writes RBAC objects
kubectl port-forward svc/db 5432:5432noNo, it opens a tunnel to your database
helm uninstall webunknownYes: not kubectl, so never assumed safe

Where the gate lets things through

The failure I would plan around is the classification of verbs, not the prompt itself. The classifier keys on the first word after kubectl, so auth is a read-only verb because can-i and whoami are reads, and auth reconcile, which applies RBAC, rides in under it. I ran it through the classifier and got no prompt. The same is true for get secret, which is a legitimate read that is also a data exfiltration step, since the output is appended to the conversation and sent to your model provider.

The second trap is in the confirmation menu. It offers three choices: Yes, Yes and do not ask me again, and No. The middle option sets SkipPermissions to true for the rest of the session. One tired keypress at the end of a long debugging chat turns the whole safeguard off, and the next command, however destructive, runs without a question. When you choose No, the model is told that the user declined and that the call is not retryable, so it should not keep pushing the same command at you.

The classifier decides from the text of the command, not from what the command does to the cluster. Reads that expose Secrets, tunnels, and subcommands such as auth reconcile are classified no and run unprompted. Do not rely on the prompt to protect Secrets or RBAC.

None of this makes the gate useless. It catches the common case well, including the careless multi-command pipeline. It means the gate is a speed bump in front of a model, not an authorisation system, and the two should not be confused when someone asks whether the agent is safe to run against production.

Quiet mode, maxIterations and unattended runs

In --quiet mode there is nobody to ask, and the code handles that bluntly. If the model proposes a command the gate would prompt for, the run exits with an error that says RunOnce mode cannot handle permission requests and lists the commands, and points you at --skip-permissions. So a one-shot run is read-only by default, which is a good property for a cron job, and the flag that lifts it is labelled dangerous in the flag help.

maxIterations defaults to 20 and bounds the number of agent loop turns for one task. When it is reached the agent prints Maximum number of iterations reached and stops. It is a loop breaker, not a spending cap, because the counter is reset to zero whenever you type a new query.

The config file lives at ~/.config/kubectl-ai/config.yaml and command-line flags win over it. This is the one I would start from for a read-mostly assistant.

# ~/.config/kubectl-ai/config.yaml
# Flags override this file; this file overrides defaults.
llmProvider: gemini
skipPermissions: false     # the default. Never set true in a file you share.
maxIterations: 20          # loop breaker per task, not a cost cap
kubeconfig: ~/.kube/kubectl-ai-readonly.yaml   # a dedicated identity, not your admin context
uiType: terminal
uiListenAddress: localhost:8888                 # only matters when uiType is web

Pin the kubeconfig explicitly with --kubeconfig, and use a file that only ever holds the read-only identity. An agent that inherits your current context can point at production while you believe it is on staging.

MCP client mode: every external tool prompts

With --mcp-client (available from v0.0.12 per the README), kubectl-ai connects to the servers listed in ~/.config/kubectl-ai/mcp.yaml and offers their tools to the model next to kubectl and bash. The tool wrapper returns unknown from CheckModifiesResource for every external tool, with a comment saying it cannot know what an arbitrary tool does. Combined with the gate above, that means each external tool call asks for approval, which is the right default.

# ~/.config/kubectl-ai/mcp.yaml (list form, as in the README and client guide)
servers:
  # stdio server: kubectl-ai starts it as a child process
  - name: sequential-thinking
    command: npx
    args:
      - -y
      - "@modelcontextprotocol/server-sequential-thinking"

  # HTTP server with a bearer token read from the environment
  - name: custom-api
    url: https://api.example.com/mcp
    auth:
      type: "bearer"
      token: "${MCP_TOKEN}"

# Run with:  kubectl-ai --mcp-client
# Every tool above reaches the model as modifies_resource = unknown, so each call prompts.

It also means the Yes and do not ask me again option now covers tools you did not write. If you answer it once for a harmless documentation lookup, the rest of the session can call a mail or ticketing server without asking. Keep sessions short and keep tool sets small.

One practical snag: the docs disagree on the shape of mcp.yaml. The README and the client guide use a list of entries with a name field, while the example in the server guide uses a map keyed by name. I used the list form, which is the one the client documents.

MCP server mode: no gate, no auth, every interface

kubectl-ai can also be the server. With --mcp-server it exposes its bash and kubectl tools to another client such as Claude Desktop over stdio, and with --mcp-server-mode streamable-http --http-port 9080 it serves MCP over HTTP. In cmd/mcp.go the built-in tool handler decodes the arguments and calls tool.Run directly. I found no call to CheckModifiesResource on that path, so whatever approval happens is the connecting client's job, not kubectl-ai's.

The documentation says the HTTP endpoint listens at http://localhost:9080/mcp and warns that, without authentication, anyone who can reach the port can run kubectl and bash. The code passes the endpoint as a bare port. I started the server and listed its sockets: it was bound to *:19080, not 127.0.0.1. Then I sent an MCP initialize request to my machine's LAN address with no token and got a valid response back. That was measured on macOS with a binary built from the commit above, so check your own build with lsof before you trust the word localhost.

The fix is built in and opt-in. Setting --mcp-auth-issuer turns the endpoint into an OAuth 2.1 resource server that verifies the bearer JWT against the issuer's JWKS, and --mcp-auth-audience becomes mandatory. The audience check is what stops a token issued for a different service by the same authorisation server from working here, which is the confused-deputy case the MCP authorization specification calls out. Until the signing keys load, the endpoint answers 503 rather than letting requests through.

# Wrong: the documented quick start. Bound to every interface in my test,
# and any host that can reach :9080 gets the bash and kubectl tools.
kubectl-ai --mcp-server --mcp-server-mode streamable-http --http-port 9080

# Right: OAuth 2.1 resource server. aud must equal the audience string exactly.
kubectl-ai --mcp-server --mcp-server-mode streamable-http --http-port 9080 \
  --mcp-auth-issuer https://authgate.corp \
  --mcp-auth-audience https://kubectl-ai.corp/mcp

# Verify the server really is locked: expect 401 and a WWW-Authenticate header.
curl -i -X POST https://kubectl-ai.corp/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'

# Discovery document (RFC 9728), served without a token:
curl -s https://kubectl-ai.corp/.well-known/oauth-protected-resource

# Check what the plain server bound to, on the machine that runs it:
lsof -nP -iTCP:9080 -sTCP:LISTEN     # "*:9080" means every interface
ModeWho approves a writeNetwork exposure
Interactive chatYou, per command, unless you pick do not ask againNone, a local terminal
--quiet one-shotNobody: the run aborts on a write unless --skip-permissions is setNone
--mcp-server over stdioThe MCP client, if it prompts; kubectl-ai has no gate on this pathA child process of the client
streamable-http, no auth flagsNobodyBound to all interfaces in my test
streamable-http with the issuer and audience flagsWhoever holds a valid token; still no gateAuthenticated, 401 without a token

OAuth fixes who can call the endpoint. It does not add a confirmation step, and it does not narrow what a caller can run: an authenticated client still gets the bash and kubectl tools. Pair it with a read-only identity, or the token holder has whatever the kubeconfig has.

Put RBAC underneath the gate

The only control that does not depend on string matching is the cluster's own authorisation. Kubernetes ships a user-facing view ClusterRole that allows read-only access to most objects in a namespace, does not allow viewing roles or role bindings, and deliberately excludes Secrets. The RBAC documentation explains the reason: reading Secrets exposes ServiceAccount credentials, which would be a privilege escalation. That single role closes the two worst holes in the probe table: get secret returns a forbidden error, and auth reconcile cannot write.

Run kubectl-ai with a kubeconfig that holds a ServiceAccount bound to that role, then confirm with kubectl auth can-i before the first session. If the model needs to write, give it a separate, namespaced Role for exactly those verbs and switch kubeconfigs on purpose, rather than widening the standing identity.

What to do on Monday

A short list, in the order I would do it, for a team that wants to try kubectl-ai without making it a liability:

  1. Create a dedicated read-only identity bound to the view ClusterRole in the namespaces the assistant needs, and point kubectl-ai at its kubeconfig with --kubeconfig.
  2. Leave skipPermissions at false everywhere, and treat the Yes and do not ask me again answer as a way to disable the safeguard for the whole session.
  3. Use --quiet only for read-only questions, so it fails loudly on a write instead of being given --skip-permissions to make it pass.
  4. Do not run streamable-http mode on a shared network. If another tool must reach it, set --mcp-auth-issuer and --mcp-auth-audience, put it behind TLS, and confirm the 401 with curl.
  5. Add each MCP client server one at a time, and remember that its tools reach the model as unknown and so prompt, until the first do not ask me again.
  6. Check the build you run against the commit you read. This project moves quickly between tagged releases, and the date I read it is the date these findings are good for.

The commands below put steps one, four and six into a form you can run.

# 1. A read-only identity. The view ClusterRole excludes Secrets.
kubectl create rolebinding kubectl-ai-view \
  --clusterrole=view --serviceaccount=acme:kubectl-ai --namespace=acme

# Prove it with the kubeconfig the agent will use, not your own.
kubectl --kubeconfig ~/.kube/kubectl-ai-readonly.yaml auth can-i delete pods -n acme   # expect: no
kubectl --kubeconfig ~/.kube/kubectl-ai-readonly.yaml get secret db -n acme            # expect: Forbidden

# 2. Start the agent on that identity only.
kubectl-ai --kubeconfig ~/.kube/kubectl-ai-readonly.yaml

# 6. If you build from source, record which commit your findings are about.
git -C kubectl-ai log -1 --format='%h %cd'

The rule I take from this: a confirmation prompt that depends on parsing a command is a useful speed bump, and an authorisation boundary that depends on the cluster is the only thing you should call a control. kubectl-ai does the first one well, documents the second one honestly, and leaves the HTTP server open until you close it, so close it before anything else.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Does kubectl-ai ask for confirmation before changing my cluster?

By default, yes. skipPermissions is false, and any command its classifier does not mark as read-only triggers a Yes, Yes and do not ask me again, or No prompt. The classifier works from the command text, so some reads and a few subcommands such as auth reconcile run without a prompt.

What does the Yes, and don't ask me again option do in kubectl-ai?

In the source I read, choosing it sets SkipPermissions to true for the rest of that session. Every later command then runs unprompted, including destructive ones. Choose plain Yes and keep sessions short.

What is maxIterations in kubectl-ai and what is the default?

It limits how many agent loop turns kubectl-ai takes on one task, and it defaults to 20. When the limit is hit the agent prints that the maximum was reached and stops. It is not a cost cap, because the counter resets to zero whenever you enter a new query.

Is the kubectl-ai MCP server safe to expose over HTTP?

Not by default. The streamable-http endpoint is unauthenticated, so anyone who can reach the port can run kubectl and bash, and in my test it bound to all interfaces rather than localhost. Set --mcp-auth-issuer and --mcp-auth-audience to require OAuth 2.1 bearer tokens, and use a read-only kubeconfig.

How do I run kubectl-ai safely against a real cluster?

Give it a dedicated kubeconfig bound to the view ClusterRole, which excludes Secrets, and pass it with --kubeconfig. Keep skipPermissions false and use --quiet only for read-only questions. Treat the confirmation prompt as a convenience on top of RBAC, not a replacement for it.