Р. Аллахвердиев

Hook: агент не читает секреты и не делает необратимое

PreToolUse-hook для Claude Code на Python: блокирует чтение .env, ключей и .pem, вывод переменных окружения, git push, reset --hard и массовое удаление. Правило, которое нельзя забыть.

опубликовано 9171d00

Работает сclaude-code

Установка
mkdir -p .claude/hooks

Исходники — в тексте ниже, отдельного репозитория у набора нет.

Обложка: Hook: агент не читает секреты и не делает необратимое
Содержание

Зачем он нужен

«Не читай .env» в CLAUDE.md — просьба, и модель может её не выполнить. Hook — код: он срабатывает до вызова инструмента и при коде выхода 2 блокирует действие, а текст из stderr возвращается агенту. Это второй барьер после permissions.deny, и его нельзя «забыть» посреди длинной сессии.

Набор — упрощённая версия хука из моего проекта DeepRisk. Проектные правила (защита каталогов с исходниками книг) я убрал, остались универсальные.

Что блокирует

Что агент пытается сделать Реакция
Прочитать или изменить .env, *.pem, *.key, id_rsa* блок
cat .env, type .env, Get-Content .env блок
printenv, env с конвейером блок
git push блок: пушит человек
git reset --hard, git clean -f… блок
rm -rf /, rm -rf ~, rm -rf ., rm -rf * блок

Как установить

  1. Сохраните guard.py из блока ниже в .claude/hooks/guard.py.
  2. Добавьте в .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Read|Edit|Write",
        "hooks": [{ "type": "command", "command": "python .claude/hooks/guard.py" }]
      }
    ]
  }
}
  1. Проверьте руками: попросите агента прочитать .env и убедитесь, что он получил сообщение «Заблокировано правилами проекта».

guard.py

"""PreToolUse hook: детерминированные запреты проекта.

Код выхода 2 блокирует действие, а stderr уходит агенту. Это второй барьер после permissions.deny:
просьбы в CLAUDE.md можно забыть, а hook срабатывает всегда.
"""
import json
import re
import sys

for _s in (sys.stdout, sys.stderr):
    try:
        _s.reconfigure(encoding="utf-8")
    except Exception:
        pass

SECRET_PATH = re.compile(r"(^|[\\/])(\.env(\..+)?|.*\.pem|id_rsa.*|.*\.key)$", re.I)
BAD_CMD = [
    (re.compile(r"\b(cat|type|more|less|head|tail|Get-Content)\b[^|;&]*\.env\b", re.I), "чтение .env"),
    (re.compile(r"\bprintenv\b|\benv\s*\|", re.I), "вывод переменных окружения"),
    (re.compile(r"\bgit\s+push\b", re.I), "git push (пушит только человек)"),
    (re.compile(r"\bgit\s+(reset\s+--hard|clean\s+-[a-z]*f)", re.I), "необратимая git-команда"),
    (re.compile(r"\brm\s+-rf?\s+(/|~|\.|\*)", re.I), "массовое удаление"),
]


def block(msg: str) -> None:
    print(f"Заблокировано правилами проекта: {msg}. См. CLAUDE.md.", file=sys.stderr)
    sys.exit(2)


def main() -> None:
    try:
        data = json.load(sys.stdin)
    except Exception:
        sys.exit(0)
    tool = data.get("tool_name", "")
    inp = data.get("tool_input", {}) or {}
    path = inp.get("file_path") or inp.get("path") or ""

    if tool in ("Read", "Edit", "Write") and path and SECRET_PATH.search(path):
        block(f"доступ к секретному файлу {path}")
    if tool == "Bash":
        cmd = inp.get("command", "")
        for rx, why in BAD_CMD:
            if rx.search(cmd):
                block(why)
    sys.exit(0)


if __name__ == "__main__":
    main()

Ограничения

  • Это защита от случайных действий агента, а не от целенаправленного обхода: команду можно запутать (base64, переменные, скрипты). Хук работает вместе с permissions.deny, изоляцией окружения и тем, что реальные секреты вообще не лежат в рабочей копии.
  • Регулярные выражения под Windows и POSIX я проверял на своём проекте; на вашем наборе команд добавьте свои запреты и прогоните тесты.
  • Команда python должна быть в PATH. Если нет, укажите полный путь к интерпретатору.