SkillsHooksPromptsAgentsPersonasModelsPoliciesToolsTemplatesBundlesCategoriesStart here
← Hooks
Hk hookdevopsblockingstable

Destructive kubectl Guard

Blocks destructive kubectl operations. Opt-in via command-wrappers.toml. Denies kubectl delete, kubectl drain, and kubectl cordon without the bypass env AI_ALLOW_DESTRUCTIVE_KUBECTL=1. All other kubectl subcommands pass through.

id hook/destructive-kubectl-guardv1.0.1by convergent-systems-key
Event
PreToolUse
Trigger
tool-nameBash
Language
python
Side effects
  • blocks tool call with explanation
  • bypass via AI_ALLOW_DESTRUCTIVE_KUBECTL=1
Platforms
linuxmacoswindows
Notes
Logic is cross-platform. Wiring: use 'ai hooks run destructive-kubectl-guard' in settings.json — the ai binary discovers Python on each OS. kubectl available on all platforms. Python logic is cross-platform.
Depends on
hook/lib

Script · destructive-kubectl-guard.py

#!/usr/bin/env python3
"""hooks/destructive-kubectl-guard.py — gate destructive `kubectl`
operations per Common.md §2.2. Opt-in via command-wrappers.toml.

Blocks (without bypass env): `kubectl delete`, `kubectl drain`,
`kubectl cordon`. Other subcommands pass through.

The bypass env is AI_ALLOW_DESTRUCTIVE_KUBECTL=1.

Self-check:
  --self-check
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib  # noqa: E402


GUARDED = {"delete", "drain", "cordon"}
BYPASS_ENV = "AI_ALLOW_DESTRUCTIVE_KUBECTL"
TOOL = "kubectl"


def argv_from_wrapper() -> list[str]:
    """The args after `kubectl`, as JSON in WRAPPED_ARGV (command-wrapper path).

    The wrapper does not forward the tool's argv on the command line — it
    publishes it in WRAPPED_ARGV so argparse never sees subcommand tokens.
    """
    try:
        return json.loads(os.environ.get("WRAPPED_ARGV", "[]"))
    except json.JSONDecodeError:
        return []


def argv_from_stdin() -> list[str]:
    """The args after `kubectl`, parsed from a Claude PreToolUse JSON payload."""
    raw = sys.stdin.read()
    if not raw.strip():
        return []
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError:
        return []
    cmd = (
        payload.get("command")
        or payload.get("tool_input", {}).get("command")
        or ""
    ) if isinstance(payload, dict) else ""
    parts = cmd.split()
    return parts[1:] if parts and parts[0] == TOOL else []


def check_invocation(argv: list[str]) -> int:
    if not argv or argv[0] not in GUARDED:
        return 0
    if os.environ.get(BYPASS_ENV) == "1":
        _lib.log(f"`kubectl {argv[0]}` — bypass active ({BYPASS_ENV}=1). Logged.")
        return 0
    _lib.log(f"blocking — `kubectl {argv[0]}` mutates cluster state.")
    _lib.log("Per Common.md §2.2 + §2.4: state what will change, name reversibility, wait for an unambiguous yes.")
    _lib.log(f"To bypass for one session only: {BYPASS_ENV}=1 kubectl {argv[0]} ...")
    return 1


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(add_help=True)
    parser.add_argument("--self-check", action="store_true")
    parser.add_argument("--mode", choices=["claude", "wrapper"], default=None,
                        help="invocation mode (set by the command wrapper)")
    parser.add_argument("rest", nargs=argparse.REMAINDER)
    args = parser.parse_args(argv)
    if args.self_check:
        return _lib.self_check_ok()
    if args.mode == "wrapper":
        return check_invocation(argv_from_wrapper())
    if args.mode == "claude" or not sys.stdin.isatty():
        return check_invocation(argv_from_stdin())
    return check_invocation(args.rest)


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
governancekubectlkubernetesdestructiveguardclaude-code
Author convergent-systems-key. Catalog data license CC-BY-4.0.