Policy plugin that gates kubectl by kubeconfig context: hard-deny irreversible verbs outside local clusters, ask for the rest.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-kubectl-guard
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:gengwg/dsh-kubectl-guard
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
A DeepSeek Harness policy plugin that gates kubectl by kubeconfig context.
Irreversible verbs against a non-local cluster are denied outright. Recoverable writes ask first. Reads and local clusters are untouched.
It registers no tools of its own — it inspects the command argument of shell tool calls, so it covers whatever the agent runs.
Install
Requires pnpm, which dsh plugin shells out to.
From npm:
dsh plugin --profile web add dsh-kubectl-guard
From source, if you want to hack on it:
git clone https://github.com/gengwg/dsh-kubectl-guard
cd dsh-kubectl-guard
dsh plugin --profile web add "$PWD"
Either way, activation is automatic: the package declares dsh.bundle, so
dsh plugin add joins it to the profile's bundle stack. Restart dsh to load it.
Upgrading from 0.1.x, which installed as a plain dependency: delete the manual
insert entry from ~/.dsh/profiles/<profile>/cordis.patch.yml. Bundle and user
patch layers both apply, so leaving it in loads the guard twice.
Examples
Nothing to invoke. Ask the agent to do its normal work; the guard sits in the tool pipeline and inspects the shell command before it runs.
Blocked, with the cluster name replaced by a per-session pseudonym:
> delete the stuck nginx pod
Error: kubectl-guard: 'delete' is irreversible and ctx#4be1f92a is not a local
cluster. Denied.
Asked, so you approve it in the UI before it runs:
> roll out the new deployment
kubectl-guard: 'apply' writes to ctx#4be1f92a, which is not a local cluster.
[approve] [deny]
Untouched, because reads are not gated:
> what pods are failing in kube-system?
kubectl get pods -n kube-system --field-selector=status.phase!=Running
NAME READY STATUS RESTARTS
api-7d9f8c 0/1 CrashLoopBackOff 14
Untouched, because the context is local:
> wipe the test namespace on my kind cluster
kubectl --context kind-dev delete ns test
namespace "test" deleted
Dry runs are reads, so they pass and give the agent a way to show you a change before asking for it:
kubectl apply --dry-run=server -f deploy.yaml # allowed
kubectl apply -f deploy.yaml # asks
Turn the guard off for one session without editing config:
dsh web --patch <(echo '- id: kubectl-guard
disabled: true')
Behavior
| Command | Non-local context | Local context |
|---|---|---|
get, describe, logs, top |
allow | allow |
auth can-i |
allow | allow |
auth reconcile |
ask | allow |
config use-context, other mutating config subcommands |
ask | ask |
apply, patch, scale, exec |
ask | allow |
delete, drain, evict |
deny | allow |
scale --replicas=0 |
deny | allow |
apply --prune |
deny | allow |
replace --force |
deny | allow |
apply --dry-run=server |
allow | allow |
A context is local only if it matches localContexts. Everything else, including a kubeconfig that cannot be read, is treated as production.
The context is resolved the way the shell would resolve it: an explicit
--context wins, then --kubeconfig, then a KUBECONFIG= assignment written
inline on the same command line, then the ambient environment. That last case
matters -- without it, KUBECONFIG=/path/to/prod kubectl delete ... would be
judged against whatever your shell happened to point at.
Config
config:
localContexts: [minikube, 'kind-*', docker-desktop]
binaries: [kubectl, k]
guardedTools: [bash, pwsh]
showContextNames: false
showContextNames is off by default: blocked-command messages go to the model, and therefore to the LLM provider. With it off the model sees a stable per-session pseudonym like ctx#4be1f92a instead of your cluster's name.
Failing closed
A gate that can be talked around is worse than none. Anything unparseable — sh -c, command substitution, an unterminated quote — is treated as a mutation: denied if the text contains an irreversible verb, asked otherwise. An unknown verb asks rather than allows.
What this is not
This gates a cooperative agent, not an adversary. It reads the command string
the agent asked to run, so anything that hides the binary name from that string
defeats it by construction -- $KUBECTL delete ... with the name only in the
environment, a shell alias resolved at runtime, a base64 round-trip.
Indirection that still contains the literal name is caught: K=kubectl; $K delete pod foo is denied, because any $ expansion alongside a mention of a
guarded binary makes the command opaque, and opaque plus an irreversible verb
is a denial. But treat the guard as a seatbelt against a confused agent, not a
sandbox against a hostile one. If you need the stronger property, take the
credential away rather than filtering the command.
Limitations
- Only
kubectl. helm, argocd and flux are not covered; the verb table is data, so adding them is an edit tosrc/verbs.js. - Pass-through wrappers (
sudo,time,nice, ...) are seen through, but only until a bare-token wrapper argument:timeout 30 kubectl delete ...is not gated, because30ends the wrapper chain. current-contextis read with a line-anchored regex, not a YAML parser. Unreadable or unmatched means production, so the failure direction is safe.- The pseudonym salt is per-process: ids are stable within a session, not across restarts.
- Guards are synchronous, so the deny path does no I/O beyond a cached
readFileSync.
Test
npm test
MIT.
Links
More in this category
toby-bridges/api-relay-audit★ 884
Runs local security audits of AI API relays and LLM proxies from DeepSeek Harness, producing Markdown reports for prompt injection, model substitution signals, tool-call rewriting, error leakage, stream integrity, and profile-gated Web3 risks.
SeaOf0/dsh-redteam-model★ 716
Authorized-security DSH collection: nine work modes (redteam coordinator, pentest, code audit, binary analysis, attack-defense, AV evasion, incident response, cloud security, CTF solving) and fifteen runtime plugins, managed from a settings page with one-click deploy, install, update and uninstall.
howmp/dsh-pentest★ 609
Authorized pentest mode for DeepSeek Harness — exploration chain, assets and findings with a Web view.
PerryLink/dsh-auto-review★ 233
Second-model auto-review on the approval answerer chain: a read-only reviewer subagent returns structured allow/deny verdicts with reasons, fail-closed by default.
ADWMC/helm-d#helmd★ 225
Single-bundle reverse-engineering and pentest security plugin: first-turn tool narrowing, domain routing, and 33 tools covering APK, web, native binary, protocol, malware and LLM samples — with unpacking, license-bypass and anti-analysis case playbooks, an H-CoT evaluation engine (semantic routing, /hcot command), a web workbench with a ledger-driven tool shelf, an on-disk case workflow (auto-persisted evidence chain, E-numbered validated findings, post-compaction resume), GitHub-based external tool discovery, and 361 on-demand reference docs.
NanmiCoder/dsh-auto-mode★ 164
Adds an Auto permission preset between Workspace Write and Full access: routine work stays in the official workspace-write sandbox while the current session model reviews escalation and destructive calls, granting one exact wider access once, asking when the intent is ambiguous, and denying critical paths.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.