Агент тронул защищённые файлы / запушил в git
Автономный агент удобно правит код — пока не тронет то, что ломает прод
молча: миграцию БД, terraform, .github/workflows, .env. Такой diff
проходит тесты, мержится и уезжает — а вы узнаёте по инциденту.
Защита проще, чем кажется: пусть любое касание защищённого пути шлёт push. Не блокируем работу агента — просто ставим человека в известность до того, как изменение уедет. А при желании — и блокируем.
Шаблон 1: pre-push хук блокирует и алёртит
Заголовок раздела «Шаблон 1: pre-push хук блокирует и алёртит»pre-push срабатывает до отправки в remote — идеальная точка, чтобы
поймать защищённые файлы и, если надо, вообще не пустить push. Кладём в
.git/hooks/pre-push (или в общий core.hooksPath):
#!/usr/bin/env bash# .git/hooks/pre-push — блокирует push, если тронуты защищённые путиset -euo pipefail
PROTECTED=( 'db/migrations/' 'terraform/' # *.tf, *.tfvars '.github/workflows/' '.env' 'secrets' # secrets.yml, k8s secrets, ... 'Dockerfile' 'infra/')
# что уходит в remote: диапазон local..remote с stdin git'аremote_sha=$(git rev-parse HEAD)base=$(git rev-parse '@{push}' 2>/dev/null || echo "")range=${base:+"$base..$remote_sha"}changed=$(git diff --name-only ${range:-HEAD~1..HEAD})
hit=""for f in $changed; do for p in "${PROTECTED[@]}"; do [[ "$f" == *"$p"* ]] && hit+=" $f"$'\n' donedone
if [[ -n "$hit" ]]; then curl -fsS -m 5 "$NOTIFLY_URL/message?token=$NOTIFLY_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"title\":\"🛡️ Push тронул защищённые файлы\", \"message\":\"Ветка $(git symbolic-ref --short HEAD):\n$hit\nПроверь diff перед выкаткой.\", \"priority\":9}" >/dev/null || true
# хочешь блокировать — раскомментируй; хочешь только алёрт — оставь как есть # echo "Защищённые файлы в push. Ревью обязательно." >&2 # exit 1fiexit 1 полностью останавливает push — агент физически не сможет
отправить изменение, пока вы не глянете. Если предпочитаете «предупредить,
но пустить» — оставьте exit 0 (push уходит, push прилетает вам).
Шаблон 2: post-commit для лёгкой осведомлённости
Заголовок раздела «Шаблон 2: post-commit для лёгкой осведомлённости»Иногда блокировать push — перебор: агент коммитит десятками, вам важен
сам факт «тронул миграции». post-commit дёшев и не мешает работе:
#!/usr/bin/env bash# .git/hooks/post-commit — только уведомляет, ничего не блокируетchanged=$(git diff-tree --no-commit-id --name-only -r HEAD)protected=$(echo "$changed" | grep -E 'db/migrations/|\.tf$|\.github/workflows/|\.env|secrets' || true)
[[ -z "$protected" ]] && exit 0
sha=$(git rev-parse --short HEAD)curl -fsS -m 5 "$NOTIFLY_URL/message?token=$NOTIFLY_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"title\":\"✏️ Коммит в защищённых путях\", \"message\":\"$sha: $(git log -1 --pretty=%s)\n$protected\", \"priority\":6}" >/dev/null || trueПриоритет ниже (6, а не 9): это «к сведению», а не «стой». Ссылку на
конкретный diff можно подложить через msgextras — если у вас
self-hosted git с web-интерфейсом, добавьте URL вида
https://git.example.com/repo/commit/$sha, и push станет кликабельным.
Шаблон 3: файловый watcher вокруг защищённых директорий
Заголовок раздела «Шаблон 3: файловый watcher вокруг защищённых директорий»Git-хук ловит момент коммита/пуша. Но агент, работающий локально (codegen-CLI, IDE-агент), правит файлы до коммита — и вам хочется знать сразу. Ставим watcher на список защищённых путей:
import os, time, requestsfrom watchdog.observers import Observerfrom watchdog.events import FileSystemEventHandler
PROTECTED_DIRS = ["db/migrations", "terraform", ".github/workflows", "infra"]PROTECTED_FILES = {".env", "secrets.yml"}DEBOUNCE_SEC = 10 # не слать push на каждый keystroke автосейва
class Guard(FileSystemEventHandler): def __init__(self): self._last = {} # path -> ts последнего алёрта
def on_any_event(self, event): if event.is_directory: return path = event.src_path name = os.path.basename(path) if not (name in PROTECTED_FILES or any(d in path for d in PROTECTED_DIRS)): return
now = time.time() if now - self._last.get(path, 0) < DEBOUNCE_SEC: return # debounce: один путь — не чаще раза в 10 сек self._last[path] = now
notify("👀 Агент правит защищённый файл", f"{event.event_type}: {path}\nПроверь, что он делает.", priority=7)
def notify(title, message, priority): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5)
obs = Observer()obs.schedule(Guard(), ".", recursive=True)obs.start()try: while True: time.sleep(1)except KeyboardInterrupt: obs.stop()obs.join()Без watchdog — polling-вариант на os.stat().st_mtime: раз в 5 секунд
сравниваем mtime защищённых файлов, при изменении шлём push. Медленнее и
грубее, но без зависимостей и работает где угодно:
import os, time, requests
WATCH = ["db/migrations", "terraform/main.tf", ".env", ".github/workflows"]
def snapshot(paths): seen = {} for root in paths: if os.path.isdir(root): for dp, _, files in os.walk(root): for f in files: p = os.path.join(dp, f) seen[p] = os.stat(p).st_mtime elif os.path.exists(root): seen[root] = os.stat(root).st_mtime return seen
prev = snapshot(WATCH)while True: time.sleep(5) cur = snapshot(WATCH) changed = [p for p in cur if cur[p] != prev.get(p)] if changed: notify("👀 Изменились защищённые файлы", "\n".join(changed[:10]), priority=7) prev = curDebounce/флаг тут — обязателен: без него автосейв редактора и terraform fmt завалят вас пушами.
Что положить в push
Заголовок раздела «Что положить в push»- ветка и short-SHA (или путь к файлу — для watcher-а);
- список именно защищённых файлов, а не весь diff;
- первую строку commit-message — часто по ней сразу ясно, что задумал агент;
- кликабельную ссылку на diff через
msgextras, если git доступен по web.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- AI-ревью пул-реквеста — следующий шаг после алёрта.
- Утечка секретов — частный, самый опасный случай.
- Ошибки tool-ов у агента — агент правит не то из-за сбоя tool-а.