Skip to content

Agent touched protected files / pushed to git

An autonomous agent conveniently edits code — until it touches what silently breaks prod: a DB migration, terraform, .github/workflows, .env. Such a diff passes tests, merges, and ships — and you find out from an incident.

The protection is simpler than it seems: let any touch of a protected path send a push. We don’t block the agent’s work — we just put a human in the loop before the change ships. And optionally, block it too.

Approach 1: pre-push hook blocks and alerts

Section titled “Approach 1: pre-push hook blocks and alerts”

pre-push fires before sending to the remote — the perfect point to catch protected files and, if needed, refuse the push entirely. Drop it into .git/hooks/pre-push (or into a shared core.hooksPath):

#!/usr/bin/env bash
# .git/hooks/pre-push — blocks the push if protected paths were touched
set -euo pipefail
PROTECTED=(
'db/migrations/'
'terraform/' # *.tf, *.tfvars
'.github/workflows/'
'.env'
'secrets' # secrets.yml, k8s secrets, ...
'Dockerfile'
'infra/'
)
# what goes to the remote: the local..remote range from git's stdin
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
# want to block? uncomment; want alert only? leave it as is
# echo "Protected files in push. Review required." >&2
# exit 1
fi

exit 1 stops the push entirely — the agent physically can’t send the change until you’ve looked at it. If you prefer “warn but let it through” — keep exit 0 (the push goes out, the alert comes to you).

Approach 2: post-commit for lightweight awareness

Section titled “Approach 2: post-commit for lightweight awareness”

Sometimes blocking a push is overkill: the agent commits by the dozen, and what matters to you is the mere fact that it “touched migrations.” post-commit is cheap and doesn’t get in the way:

#!/usr/bin/env bash
# .git/hooks/post-commit — only notifies, blocks nothing
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

Lower priority (6, not 9): this is “for your information,” not “stop.” A link to the specific diff can be attached via msgextras — if you run a self-hosted git with a web interface, add a URL like https://git.example.com/repo/commit/$sha, and the push becomes clickable.

Approach 3: a filesystem watcher around protected directories

Section titled “Approach 3: a filesystem watcher around protected directories”

A git hook catches the moment of a commit/push. But an agent working locally (a codegen CLI, an IDE agent) edits files before the commit — and you’d like to know right away. Put a watcher on a list of protected paths:

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 # don't push on every autosave keystroke
class Guard(FileSystemEventHandler):
def __init__(self):
self._last = {} # path -> ts of the last alert
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: one path — at most once per 10s
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()

Without watchdog — a polling variant on os.stat().st_mtime: every 5 seconds compare the mtime of protected files, and on a change send a push. Slower and coarser, but dependency-free and works anywhere:

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

A debounce/flag here is mandatory: without it, the editor’s autosave and terraform fmt will bury you in pushes.

  • the branch and short SHA (or the file path — for the watcher);
  • the list of exactly the protected files, not the whole diff;
  • the first line of the commit message — it often makes the agent’s intent clear;
  • a clickable link to the diff via msgextras, if git is reachable over the web.