{
  "schema": "https://ai-atoms.com/schemas/hook-v1.json",
  "type": "hook",
  "id": "hook/destructive-gh-guard",
  "version": "1.0.1",
  "name": "Destructive GitHub CLI Guard",
  "description": "Blocks high-blast-radius, irreversible gh CLI operations. Denies without explicit --force-i-mean-it confirmation: gh repo delete, gh release delete, gh secret delete, and gh auth logout. All other gh subcommands pass through.",
  "event": "PreToolUse",
  "language": "python",
  "trigger": {
    "type": "tool-name",
    "pattern": "Bash"
  },
  "blocking": true,
  "side_effects": [
    "blocks tool call with explanation and bypass instructions"
  ],
  "authored_by": "convergent-systems-key",
  "tags": [
    "governance",
    "gh-cli",
    "destructive",
    "guard",
    "claude-code"
  ],
  "lifecycle": "stable",
  "platforms": [
    "linux",
    "macos",
    "windows"
  ],
  "platform_notes": "Logic is cross-platform. Wiring: use 'ai hooks run destructive-gh-guard' in settings.json — the ai binary discovers Python on each OS. gh CLI available on all platforms. Python logic is cross-platform.",
  "script": "#!/usr/bin/env python3\n\"\"\"hooks/destructive-gh-guard.py — gate destructive `gh` operations\nper Common.md §2.2.\n\nBlocks (without --force-i-mean-it confirmation):\n\n  gh repo delete <repo>\n  gh release delete <tag>\n  gh secret delete <name>\n  gh auth logout\n\nThese are the high-blast-radius / irreversible operations on the\ndefault `gh` surface. Other subcommands pass through.\n\nSelf-check:\n  --self-check\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\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 = [\n    (\"repo\", \"delete\"),\n    (\"release\", \"delete\"),\n    (\"secret\", \"delete\"),\n    (\"auth\", \"logout\"),\n]\n\n# Escape hatch: the user can set AI_ALLOW_DESTRUCTIVE_GH=1 for a single\n# session to bypass these checks. Setting this is itself a §2.2 action\n# and is logged.\nBYPASS_ENV = \"AI_ALLOW_DESTRUCTIVE_GH\"\n\n\ndef check_invocation(argv: list[str]) -> int:\n    \"\"\"argv is the args AFTER `gh`.\"\"\"\n    if len(argv) < 2:\n        return 0\n    a, b = argv[0], argv[1]\n    if (a, b) not in GUARDED:\n        return 0\n\n    if os.environ.get(BYPASS_ENV) == \"1\":\n        _lib.log(f\"`gh {a} {b}` — bypass active ({BYPASS_ENV}=1). Logged.\")\n        return 0\n\n    _lib.log(f\"blocking — `gh {a} {b}` is a §2.2 destructive operation.\")\n    _lib.log(\"Per Common.md §2.2 + §2.4: name what will be destroyed, snapshot if reversible, wait for an unambiguous yes.\")\n    _lib.log(f\"To bypass for one session only: AI_ALLOW_DESTRUCTIVE_GH=1 gh {a} {b} ...\")\n    return 1\n\n\ndef argv_from_wrapper() -> list[str]:\n    \"\"\"The args after `gh`, 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    import json\n    try:\n        return json.loads(os.environ.get(\"WRAPPED_ARGV\", \"[]\"))\n    except json.JSONDecodeError:\n        return []\n\n\ndef from_claude_payload() -> int:\n    import json\n    raw = sys.stdin.read()\n    if not raw.strip():\n        return 0\n    try:\n        payload = json.loads(raw)\n    except json.JSONDecodeError:\n        return 0\n    cmd = (\n        payload.get(\"command\")\n        or payload.get(\"tool_input\", {}).get(\"command\")\n        or \"\"\n    ) if isinstance(payload, dict) else \"\"\n    if not cmd.strip().startswith(\"gh \"):\n        return 0\n    return check_invocation(cmd.split()[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    parser.add_argument(\"rest\", nargs=argparse.REMAINDER)\n    args = parser.parse_args(argv)\n\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 (args.mode is None and not sys.stdin.isatty()):\n        return from_claude_payload()\n    return check_invocation(args.rest)\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n",
  "depends_on": [
    "hook/lib"
  ],
  "category": "governance"
}