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 touchedset -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 stdinremote_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
# want to block? uncomment; want alert only? leave it as is # echo "Protected files in push. Review required." >&2 # exit 1fiexit 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 nothingchanged=$(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 || trueLower 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, 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 # 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 = curA debounce/flag here is mandatory: without it, the editor’s autosave and
terraform fmt will bury you in pushes.
What to put in the push
Section titled “What to put in the push”- 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.
Related recipes
Section titled “Related recipes”- AI pull-request review — the next step after the alert.
- Secrets leak — the specific, most dangerous case.
- Agent tool errors — the agent edits the wrong thing due to a tool failure.