Перейти к содержимому

Агент тронул защищённые файлы / запушил в git

Автономный агент удобно правит код — пока не тронет то, что ломает прод молча: миграцию БД, terraform, .github/workflows, .env. Такой diff проходит тесты, мержится и уезжает — а вы узнаёте по инциденту.

Защита проще, чем кажется: пусть любое касание защищённого пути шлёт 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'
done
done
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 1
fi

exit 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, requests
from watchdog.observers import Observer
from 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 = cur

Debounce/флаг тут — обязателен: без него автосейв редактора и terraform fmt завалят вас пушами.

  • ветка и short-SHA (или путь к файлу — для watcher-а);
  • список именно защищённых файлов, а не весь diff;
  • первую строку commit-message — часто по ней сразу ясно, что задумал агент;
  • кликабельную ссылку на diff через msgextras, если git доступен по web.