{
  "schema": "https://ai-atoms.com/schemas/hook-v1.json",
  "type": "hook",
  "id": "hook/destructive-terraform-guard",
  "version": "1.0.1",
  "name": "Destructive Terraform Guard",
  "description": "Blocks terraform destroy and terraform apply. Opt-in via command-wrappers.toml. Requires explicit bypass via AI_ALLOW_DESTRUCTIVE_TERRAFORM=1. Prevents accidental infrastructure destruction or unreviewed applies. Other tofu/terraform subcommands (plan, init, validate, output) pass through.",
  "event": "PreToolUse",
  "language": "python",
  "trigger": {
    "type": "tool-name",
    "pattern": "Bash"
  },
  "blocking": true,
  "side_effects": [
    "blocks tool call with explanation",
    "bypass via AI_ALLOW_DESTRUCTIVE_TERRAFORM=1"
  ],
  "authored_by": "convergent-systems-key",
  "tags": [
    "governance",
    "terraform",
    "tofu",
    "infrastructure",
    "destructive",
    "guard",
    "claude-code"
  ],
  "lifecycle": "stable",
  "platforms": [
    "linux",
    "macos",
    "windows"
  ],
  "platform_notes": "Logic is cross-platform. Wiring: use 'ai hooks run destructive-terraform-guard' in settings.json — the ai binary discovers Python on each OS. tofu/terraform available on all platforms. Python logic is cross-platform.",
  "script": "#!/usr/bin/env python3\n\"\"\"hooks/destructive-terraform-guard.py — gate `terraform {destroy,apply}`\nper Common.md §2.2. Opt-in via command-wrappers.toml.\n\nBlocks (without bypass env): `terraform destroy`, `terraform apply`.\nOther subcommands pass through.\n\nThe bypass env is AI_ALLOW_DESTRUCTIVE_TERRAFORM=1.\n\nSelf-check:\n  --self-check\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\nimport _lib  # noqa: E402\n\n\nGUARDED = {\"destroy\", \"apply\"}\nBYPASS_ENV = \"AI_ALLOW_DESTRUCTIVE_TERRAFORM\"\nTOOL = \"terraform\"\n\n\ndef argv_from_wrapper() -> list[str]:\n    \"\"\"The args after `terraform`, as JSON in WRAPPED_ARGV (command-wrapper path).\n\n    The wrapper does not forward the tool's argv on the command line — it\n    publishes it in WRAPPED_ARGV so argparse never sees subcommand tokens.\n    \"\"\"\n    try:\n        return json.loads(os.environ.get(\"WRAPPED_ARGV\", \"[]\"))\n    except json.JSONDecodeError:\n        return []\n\n\ndef argv_from_stdin() -> list[str]:\n    \"\"\"The args after `terraform`, parsed from a Claude PreToolUse JSON payload.\"\"\"\n    raw = sys.stdin.read()\n    if not raw.strip():\n        return []\n    try:\n        payload = json.loads(raw)\n    except json.JSONDecodeError:\n        return []\n    cmd = (\n        payload.get(\"command\")\n        or payload.get(\"tool_input\", {}).get(\"command\")\n        or \"\"\n    ) if isinstance(payload, dict) else \"\"\n    parts = cmd.split()\n    return parts[1:] if parts and parts[0] == TOOL else []\n\n\ndef check_invocation(argv: list[str]) -> int:\n    if not argv or argv[0] not in GUARDED:\n        return 0\n    if os.environ.get(BYPASS_ENV) == \"1\":\n        _lib.log(f\"`terraform {argv[0]}` — bypass active ({BYPASS_ENV}=1). Logged.\")\n        return 0\n    _lib.log(f\"blocking — `terraform {argv[0]}` mutates real infrastructure.\")\n    _lib.log(\"Per Common.md §2.2 + §2.4: state what will change, name reversibility, wait for an unambiguous yes.\")\n    _lib.log(f\"To bypass for one session only: {BYPASS_ENV}=1 terraform {argv[0]} ...\")\n    return 1\n\n\ndef main(argv: list[str]) -> int:\n    parser = argparse.ArgumentParser(add_help=True)\n    parser.add_argument(\"--self-check\", action=\"store_true\")\n    parser.add_argument(\"--mode\", choices=[\"claude\", \"wrapper\"], default=None,\n                        help=\"invocation mode (set by the command wrapper)\")\n    parser.add_argument(\"rest\", nargs=argparse.REMAINDER)\n    args = parser.parse_args(argv)\n    if args.self_check:\n        return _lib.self_check_ok()\n    if args.mode == \"wrapper\":\n        return check_invocation(argv_from_wrapper())\n    if args.mode == \"claude\" or not sys.stdin.isatty():\n        return check_invocation(argv_from_stdin())\n    return check_invocation(args.rest)\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n",
  "depends_on": [
    "hook/lib"
  ],
  "category": "devops"
}