From k8s-rbac-companion
Generate a least-privilege Kubernetes RBAC manifest (Role/ClusterRole + matching binding) for the codebase at the given path. Use when invoked via `/k8s-rbac-companion:rule <path>` or when the user explicitly asks to scope or generate RBAC / a Role / a ServiceAccount's permissions for a workload with a path argument. Orchestrates the `k8s-rbac-companion:rbac-generator` agent across two phases (discovery, synthesis) and gathers user input between them via `AskUserQuestion`.
How this skill is triggered — by the user, by Claude, or both
Slash command
/k8s-rbac-companion:ruleThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The user requested a rule for the path: `$ARGUMENTS`
The user requested a rule for the path: $ARGUMENTS
$ARGUMENTS is empty or missingRespond exactly with:
The
/k8s-rbac-companion:rulecommand needs a path argument.Usage:
/k8s-rbac-companion:rule <path>Example:
/k8s-rbac-companion:rule ./my-controllerIf you want a more conversational entry point, just say something like "scope an RBAC role for ./my-controller" and Claude will route you to the
k8s-rbac-companion:rbac-generatoragent.
Then stop. Do not proceed without a path.
$ARGUMENTS contains a path — the three-phase orchestrationFirst, emit a brief greeting to the user (exactly one short line — no headers, no bullets):
👋 Hi! I'm k8s-rbac-companion. Scanning
$ARGUMENTSfor Kubernetes API usage — I'll ask you a few questions, then emit a least-privilege Role/ClusterRole + binding as annotated YAML.
Then proceed to the three phases below — discovery (sub-agent), batched ask (AskUserQuestion), and synthesis (sub-agent). Do not deviate from the order. Do not skip phases. Do not attempt the analysis yourself outside these phase boundaries.
Why this structure exists: Claude Code sub-agents run single-shot — they can't pause mid-response to ask the user a question. So the interactive step lives in the skill (here, in the main conversation), via AskUserQuestion, between two stateless sub-agent dispatches.
Spawn the k8s-rbac-companion:rbac-generator sub-agent via the Task tool — pass k8s-rbac-companion:rbac-generator as the subagent_type (plugin agents require the fully-qualified namespaced name; the unqualified rbac-generator will not resolve). Use this prompt:
Mode: DISCOVERY ONLY.
Analyze the codebase at
$ARGUMENTSfor Kubernetes API usage. Run steps D1–D9 from your DISCOVERY mode. Then return a structured discovery summary and stop. Do not ask any questions. Do not synthesize a manifest.Read only source files, package manifests, and deploy manifests (Helm/Kustomize). Do NOT read service-internal prose docs (
README.md,CHANGELOG.md,LICENSE,CONTRIBUTING.md) — they describe what the service does for end users, not how it talks to the Kubernetes API.Lazy-load the
rbac-referenceskill — only invoke it if you hit a non-obvious mapping (dynamic client with a runtime-built GroupVersionResource, controller-runtime informer/owner-ref subresource questions, leader-election/event-recorder implicit needs, an unfamiliar client library, or a subresource you're unsure of). For standard client-go / controller-runtime / Python-client / kubectl calls, your training data plus the mapping tables you already know are sufficient.Your structured summary must include:
- Client / framework (name + how detected, e.g.
controller-runtimefromsigs.k8s.io/controller-runtimein go.mod +client.Get(call sites)- API calls (table: call site
file:line→ method →(apiGroup, resource, verb)). Include subresources as their own rows (pods/log,<resource>/status, etc.).- resourceNames candidates (table: resource → literal object name → call site, for accesses to a specifically-named object via a verb in {get,update,patch,delete}). These tighten the rule; you infer them, the skill does NOT ask about them.
- Namespaces touched (literal namespaces, "own namespace" via downward API/env, all-namespaces, or cluster-scoped). This informs the scope question.
- Cluster-scoped signals (any access to cluster-scoped resources like
nodes/persistentvolumes/CRDs-cluster-scoped,nonResourceURLs, or all-namespace list/watch) — these force a ClusterRole.- Implicit needs (leader election →
leases; event recording →events; informers started →list+watchon their resources).- Speculation candidates (TODO/FIXME near K8s calls implying a future verb/resource — e.g.
// TODO: also watch secrets). For each: file:line, comment text, implied grant. Do NOT bake in.- Cluster version from
list_api_resources/kubectl versionif a cluster MCP is connected (else: "MCP not connected").- Mapping notes (dynamic/unstructured access you can't statically resolve, or any ambiguous method→grant mapping you flagged).
Permitted tools:
Read,Grep,Glob,Skill(to loadrbac-referenceonce, if needed),mcp__plugin_k8s-rbac-companion_kubernetes__list_api_resourcesand__explain_resource(read-only, once each),WebFetch(one-shot, for an ambiguous client-library mapping). Forbidden:Bash, and every MUTATING Kubernetes MCP tool (kubectl_apply,kubectl_create,kubectl_delete,kubectl_patch,kubectl_scale, helm/cleanup,port_forward).
Wait for the agent's return. Read the discovery summary carefully — you'll use it to set the Phase 2 question defaults and pass it back to the agent in Phase 3.
Use the AskUserQuestion tool to ask the user the three baseline questions at once, plus a 4th speculation question only if Phase 1 surfaced speculation candidates. This is the only Claude Code primitive that actually pauses the conversation for structured input — natural-language "wait for the user" instructions don't enforce a pause.
Rule for option ordering: the recommended / safest / most-common option ALWAYS goes first (the UI cursor defaults to the first option). Set the recommendation from the discovery findings, as noted per-question.
Q1 — Scope (Role vs ClusterRole):
Role + RoleBinding in a single namespace. Tightest scope. Correct when the workload only touches namespaced resources in one namespace."nonResourceURLs, or genuine all-namespace access."Q2 — Target ServiceAccount (name + namespace):
<basename> in <namespace>" — description: "Suggested name = the repo directory basename <basename>; namespace = the one discovery saw (or default). Pick Other to type a different name / namespace." (Set <basename> to the basename of $ARGUMENTS; set <namespace> to the discovered namespace or default.)default in <namespace>" — description: "Bind the namespace's default ServiceAccount. Usually a smell — prefer a dedicated SA — but offered for quick local testing."name/namespace. Treat their answer as authoritative.Q3 — Custom Role vs built-in ClusterRole:
(apiGroup, resource, verb) tuples discovery found — nothing more. Tightest, and the rule explains itself."view/edit/admin instead of a custom role. Standardized, but over-grants — view reads everything, edit includes exec + impersonation. I'll ask which one and call out what it over-grants."If the user picks "Bind to a built-in", fire a follow-up AskUserQuestion for which built-in (view / edit / admin; order view first as least-privilege), and skip the custom-role synthesis path — Phase 3 emits only the binding to that built-in.
Q4 (conditional — only if Phase 1 surfaced speculation candidates):
For each candidate, add a question. Leave out first — it's the safer default. Example for a // TODO: also watch secrets at controller.go:88:
// TODO: also watch secrets at controller.go:88, implying a future watch secrets grant. Include it now or leave it out?"get/list/watch on secrets. Covers the planned addition without re-running."Calling AskUserQuestion: the platform limit is 4 questions per call. Plan:
Do not narrate before/after the calls — AskUserQuestion renders its own UI; wrapping it in "I'll now ask…" / "thanks!" just adds noise.
Spawn the k8s-rbac-companion:rbac-generator agent again (same dispatch rule as Phase 1 — fully-qualified subagent_type) with this prompt:
Mode: SYNTHESIS.
Here is the discovery summary from Phase 1:
<paste the full Phase 1 return verbatim>The user answered:
- Scope: <Q1 answer — Role+RoleBinding in ns / ClusterRole+ClusterRoleBinding / ClusterRole+RoleBinding in ns>
- ServiceAccount: in namespace
- Role style: <custom least-privilege | bind to built-in
<view|edit|admin>>- Speculation: <left out | included: …> (per candidate, if any)
Run S1 (resolve every
(apiGroup, resource, verb)againstapi-resource-map.md/ livelist_api_resources, fixing groups perversion-deltas.md), S2 (decide custom-vs-built-in per the user's answer — if built-in, emit only the binding), and S3 (compose the manifest). Emit the manifest for the user's chosen scope. Use the ServiceAccount name/namespace the user gave; name the role/binding after the SA (<sa>-role/<sa>-rolebinding, or<sa>-clusterrole/<sa>-clusterrolebinding).Required output sections, in order:
- The complete manifest —
Role/ClusterRole+ matching binding — as one fenced ```yaml block onrbac.authorization.k8s.io/v1, with a#comment on each rule citing the sourcefile:line(s) that justify it. (For a built-in binding: just theRoleBinding/ClusterRoleBindingreferencingview/edit/admin.)- A per-rule annotation table (rule → grants → justified by file:line).
- "Detected context" block (client/framework, scope, ServiceAccount, role style, cluster version + how known, MCP status, mapping/flag notes).
- How to apply (
kubectl apply -f, with a--dry-run=clientstep first) and how to verify (kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa>) and a negative test.- Do NOT auto-apply. The agent never runs
kubectl_apply/mutating MCP tools — apply is the user's explicit action.Synthesis is text-only composition from the inputs above — no MCP calls. The agent's tool allowlist grants only read-only
list_api_resources/explain_resource, so it cannot apply or dry-run; manifest validation is the user's job via the condensed prompt's--dry-run=clientstep.Forbidden tools:
Write,Edit,Bash, and every mutating Kubernetes MCP tool — the agent is read-only by allowlist.
.yaml AND emit a CONDENSED message (CRITICAL UX)Design note (domain adaptation): redis-companion wrote its artifact to a .md and had the user grep the single-line rule out, because a long ACL line gets mangled by terminal copy-paste. A Kubernetes manifest is multi-line YAML that kubectl apply -f consumes from a file natively — so here the artifact IS the file, written as a directly-appliable .yaml with the annotations carried inline as # comments (kubectl ignores them). No grep extraction, no copy-paste of the manifest body. Same dual-output spirit (full artifact in a file + short apply command in the prompt), correct file format for the domain.
Step 1 — Write ./rbac-<sa>.yaml to the current working directory.
Save the agent's complete manifest (verbatim, including the inline # comments) to ./rbac-<sa>.yaml in cwd, where <sa> is the ServiceAccount name.
Overwrite-safe procedure (mandatory order):
Glob (pattern: rbac-<sa>.yaml) or Read on the path to check whether the file already exists from a prior run.Read on it first (this satisfies the Write tool's same-session read-before-overwrite guard — without it, Write errors out and the user sees a noisy retry).Write with the agent's verbatim manifest.Critical: the file must be a clean, appliable manifest — valid YAML, documents separated by ---, comments only on # lines. The agent's output template already produces this; preserve it exactly.
If Write fails or the user denies the permission prompt, fall back: tell the user the file write didn't happen and surface the manifest inline.
Step 2 — Emit a CONDENSED user-facing message. Do NOT re-emit the agent's full output. Keep it tight:
✅ RBAC generated for ServiceAccount `<ns>/<sa>` (<scope>, <N> rules, least-privilege)
**Manifest:** `./rbac-<sa>.yaml` — open it for the per-rule rationale (inline comments) and detected context.
**Validate (no changes), then apply:**
\`\`\`
! kubectl apply -f ./rbac-<sa>.yaml --dry-run=client
\`\`\`
\`\`\`
! kubectl apply -f ./rbac-<sa>.yaml
\`\`\`
**Verify what the SA can now do:**
\`\`\`
! kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa>
\`\`\`
**Negative test — confirm an out-of-scope action is denied** (should print `no`):
\`\`\`
! kubectl auth can-i delete secrets --as=system:serviceaccount:<ns>:<sa> -n <ns>
\`\`\`
Each one-liner starts with ! so Claude Code runs it directly on paste. Substitute <ns>, <sa>, <scope>, <N> from the answers/manifest, and pick a genuinely out-of-scope verb/resource for the negative test (one the rule does NOT grant).
AskUserQuestion. The user's answer is authoritative; your inference is not.kubectl apply (or any mutating MCP tool). Apply is the user's explicit action via the condensed prompt's commands.*. If discovery flagged fully-dynamic access it couldn't resolve, surface it as a note for the user — do not paper over it with a wildcard grant.npx claudepluginhub mjtrapani/k8s-rbac-companion --plugin k8s-rbac-companionCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.