π‘οΈ kube-guard
Give your Claude Code agent kubectl β with a seatbelt.
A PreToolUse hook that classifies every kubectl/helm command by blast radius and allows, asks, or denies it before it runs. Reads flow through; production deletes don't.
The problem
An AI agent with kubectl access is one hallucinated command away from kubectl delete namespace prod, a drain, an apply to the wrong context, or a get secret -o yaml that leaks credentials into the transcript. Permission prompts help β until you run in acceptEdits or --dangerously-skip-permissions and rubber-stamp everything.
"Your AI agent should not have direct kubectl access." β and yet you want it to help.
The solution
kube-guard is a pure Claude Code plugin (hook + skill + command, zero dependencies) that sits in front of the agent's shell. Every Bash command is parsed; any kubectl/helm invocation is classified and gated:
| Class | Examples | Default verdict (strict) |
|---|---|---|
| READ | get, describe, logs, top, rollout status, helm list | β allow |
| WRITE | apply, scale, patch, edit, rollout restart, helm upgrade | β οΈ ask (confirm) |
| DESTRUCTIVE | delete, drain, taint, replace --force, helm uninstall | π deny |
| HIGH-RISK | exec, run, cp, port-forward, config view, get secret -o yaml | π deny |
Plus cross-cutting guards: protected contexts/namespaces (anything matching prod, production, *-prod, kube-system, β¦) escalate every mutation to deny, and the guard fails closed β if it can't verify a command (eval, pipe-to-shell, subshells, intra-word quoting like k'ubectl', unknown verbs), it asks or denies rather than guessing.
A few things the classifier is deliberately careful about:
--dry-runis a safe preview.apply/delete/scale β¦ --dry-run=server|clientmutate nothing, so they're recognised as no-ops and allowed (but--dry-run=nonereally applies, so it isn't).- Secret dumps are hard to sneak past.
get secret -o yamlis blocked and so are-oyaml(no space), comma lists (secrets,configmaps), fully-qualified names (secret.v1.core/x),--template/--go-template, andhelm get values/manifest/all. - RBAC privilege escalation is gated.
kubectl auth reconcile,create clusterrolebinding β¦ --clusterrole=cluster-admin, andapply --pruneare escalated, not waved through as ordinary writes. - When it blocks, it explains. A denied command comes back with the reason and a concrete next step (
--dry-run=server,/klease,-o name, β¦) so the agent self-corrects instead of retrying blind.
agent β kubectl delete ns prod β π DESTRUCTIVE on protected context β denied (logged)
agent β kubectl apply -f d.yaml β β οΈ WRITE β asks you to confirm
agent β kubectl get pods β β
READ β allowed
Install
/plugin marketplace add andresleecom/kube-guard
/plugin install kube-guard@kube-guard
Requires kubectl on your PATH (which you already have). Zero npm dependencies.
Why it's different
It gates execution, even when you've turned permissions off:
- A
deny/askfrom kube-guard applies regardless of permission mode β even with--dangerously-skip-permissions, a protected-context delete still stops. - It catches kubectl anywhere in a command (
cd x && kubectl delete β¦, env-prefixes, pipes), not just as the first word. - It resolves your current context so guards work even when the command omits
--context. - Every decision is written to a private, gitignored audit log (
.claude/kube-guard/audit.jsonl), with secrets redacted.
| kube-guard | k8sgpt | kagent | KubeShark (skill) | |
|---|---|---|---|---|
| Gates command execution (allow/ask/deny) | β | β (read-only diag) | β (RBAC only) | β (advisory) |
| Protected-context / blast-radius guard | β | β | β | β |
| Per-context and per-namespace policies | β | β | β | β |
| Per-resource allow/denylist (never delete nodes/CRDs/β¦) | β | β | β | β |
| Temporary context leasing (prod for 1 cmd / N min) | β | β | β | β |
| Blocks secret dumps & exec | β | n/a | β | β |
| Works in IDE, no in-cluster agent | β | β | β | β |
| Audit log | β | β | partial | β |
| Zero dependencies | β | β | β | β |
Configuration
Config is layered (later wins): plugin defaults β ~/.claude/kube-guard.config.json (global, every project) β <project>/.claude/kube-guard.config.json β KUBE_GUARD_MODE env. Set a posture once, globally, and it holds everywhere.
Levels (postures)
Every command is judged at the level of its target context (the --context flag, or your live current context):
| Level | Reads | Writes | Destructive | High-risk |
|---|---|---|---|---|
readonly | allow | deny | deny | deny |
strict (default) | allow | ask | deny | deny |
standard | allow | ask | ask | deny |
audit | allow | allow | allow | allow (logged) |
Multiple clusters β per-context policies
Different clusters, different postures. Map context globs to levels:
// ~/.claude/kube-guard.config.json
{
"defaultMode": "strict",
"contextPolicies": [
{ "match": ["*prod*", "*live*", "my-prod-cluster"], "level": "readonly" },
{ "match": ["*stag*"], "level": "strict" },
{ "match": ["kind-*", "minikube", "*dev*"], "level": "audit" }
],
// optional: give specific namespaces their own posture (composed strictest
// with the context level, so a guarded namespace holds even on a lax cluster)
"namespacePolicies": [
{ "match": ["prod", "*-prod"], "level": "readonly" }
],
// optional: tighten-only rules per resource kind (never loosen the verdict)
"resourceRules": [
{ "kinds": ["node", "namespace", "pvc", "*.cattle.io"],
"verbs": ["delete", "drain", "taint"], "verdict": "deny" }
],
"protectedNamespaces": ["kube-system", "prod", "*prod*"],
"allowExec": false, // set true to downgrade exec/cp/run from deny β ask
"allowSecretRead": false // set true to downgrade secret dumps β ask
}
Unlisted contexts fall back to defaultMode. Config is accumulated across layers for the protection arrays (contextPolicies/namespacePolicies/protected*), so a global prod guard can't be silently dropped by a project config. Legacy protectedContexts (a flat list) still works and maps to readonly. Run /kube-guard doctor to print the effective config and catch typo'd keys/levels (which would otherwise weaken the posture silently).
namespacePoliciesβ same shape ascontextPolicies, but matched against the target namespace; the effective posture is the stricter of the context level and the namespace level.resourceRulesβ{ kinds, verbs, verdict }rules that can only tighten a verdict (e.g. "neverdeletenodes/namespaces/PVCs/CRDs"), never loosen it. With no rules configured, behaviour is unchanged.
Switch & lease (multi-cluster, safely)
/kctxβ list contexts with their level and switch safely; kube-guard confirms when you enter a guarded cluster and lets dev/local through freely. Solves the "I thought I was on staging but I was on prod" footgun./kleaseβ the context leash: temporarily relax areadonly(prod) cluster for one command or N minutes, then it auto-reverts. Lease only the exception you need:# prod, but writable (with confirmation) for the next single command: node "$CLAUDE_PLUGIN_ROOT/scripts/lease.mjs" my-prod-cluster --once --level strict
Inspect and operate with /kube-guard:
/kube-guardβ show the active policy and recent decisions./kube-guard doctorβ validate the config + install (Node/kubectl, effective config, current context's level, active leases, and warnings for typo'd keys/levels)./kube-guard summarizeβ roll up the audit log (counts by verdict/class, denies by context, top denied commands; supports--since 24h,--deny-only,--context <glob>).
Or dry-run a single command through the classifier without executing it:
node "$CLAUDE_PLUGIN_ROOT/scripts/explain.mjs" "kubectl delete ns prod".
FAQ
Does it slow down every Bash command? Negligibly β the hook fast-exits unless the command contains kubectl/helm.
What if kube-guard itself errors? It fails closed: an internal error returns ask, never a silent allow.
Can the agent bypass it? Obfuscation (eval, | sh, $(...), backtick command-substitution, base64, unknown verbs) is treated as unverifiable and denied/asked β and relevance is decided after tokenization, so intra-word quoting (k'ubectl' delete) can't hide the tool name either.
Can I let the agent preview risky changes? Yes β --dry-run=server/client is treated as a read-only preview and allowed, even on a readonly context. That's the safe way to let it show you a diff before you lease the real change.
Does it send anything anywhere? No. Everything is local; the only outbound calls are the kubectl reads you'd run anyway.
Windows? Yes β pure Node.js hooks, no bash/jq.
Security
See SECURITY.md for the threat model. kube-guard is a guardrail, not a sandbox: it reduces blast radius but does not replace Kubernetes RBAC. Combine it with least-privilege credentials.
Roadmap
- GitOps remediation: propose fixes as reviewable PRs instead of mutating the cluster.
- Cost / right-sizing recommendations with dollar impact.
- Incident loop: triage β fix β PR β postmortem.
-
require dry-run + diffbefore apply; guardWrite/Editof dangerous manifests. - Blast-radius preview before destructive ops (count affected resources).
- Per-context policies & multi-cluster awareness (v0.2.0).
- Context leasing β temporary, auto-reverting prod access (v0.2.0).
- Per-namespace policies & per-resource-kind rules.
-
--dry-runawareness β previews are recognised and allowed (the enabling half of "require dry-run"). - Config validation +
/kube-guard doctor; queryable audit log via/kube-guard summarize.
Contributing
Issues and PRs welcome. The plugin is deliberately small and dependency-free β the classifier (scripts/classify.mjs) is pure and covered by node --test, which runs in CI across {Windows, macOS, Linux} Γ Node {18, 20, 22}. Add a failing case to test/classify.test.mjs first. See CONTRIBUTING.md for the house rules.
License
MIT Β© Andres Lee