AI-агент одной командой снёс мне папку проекта: разбор инцидента и защита через PreToolUse guard
AI-агент в Claude Code одной командой снёс мне всю папку проекта. Разбираю, что случилось, почему git worktree не изолирует файлы и как поставить PreToolUse guard, чтобы это не повторилось.

Сижу вечером, работаю. Агент в Claude Code делает рутинную операцию с git worktree - и одной командой сносит мне всю папку проекта. Не половину, не пару файлов. Всю. Лезу смотреть - пусто. Проект, который я пилил неделями, просто исчез с диска.
Это Kenku, мой интерактивный чарник для D&D 5e. И это был один из тех вечеров, когда очень хочется закрыть ноут и пойти пожить обычную жизнь без нейронок.
Спойлер: проект выжил, я его восстановил, и с тех пор такое у меня физически не может повториться. Стоит PreToolUse guard, который проверяет каждую bash-команду до запуска. Ниже разбираю по косточкам: что именно случилось, почему git worktree сам по себе не спасает, и как поставить этот guard руками.
Что именно произошло
Механизм оказался обиднее, чем "тупой агент затупил". Он был почти логичным, и это самое противное.
Агенту нужно было сделать операцию с worktree. Сначала он сделал не совсем то, что требовалось. Потом попытался сделать как надо, по правилам, и упёрся - не получилось. И тут сработала логика, знакомая любому уставшему разработчику: "раз сложная команда не идёт, давай упростим". Упростил. И снёс не то, что нужно, а всю рабочую папку.
То есть это не был какой-то злой rm -rf ~/ из ночного кошмара. Это была цепочка мелких разумных шагов, последний из которых оказался разрушительным. Агент не пытался мне навредить, он пытался решить задачу, и по дороге выбрал команду со слишком широким радиусом поражения.
Я, если честно, охренел. Первая мысль была даже не про восстановление, а про "как так вышло, что одна команда вообще могла это сделать".
Почему git worktree - это не про изоляцию
У меня в голове была ложная уверенность: раз агент работает в отдельном git worktree, значит он как бы в песочнице, и максимум он мне сломает эту веточку. Оказалось - нет.
Git worktree разделяет рабочие директории, но не изолирует файловую систему. Процесс, запущенный внутри worktree, - это обычный процесс операционки. Он спокойно дотягивается до родительской папки, до соседних worktree, до исходного чекаута. Ему никто не запрещает выйти за пределы своей директории, потому что worktree про удобную работу с ветками, а не про безопасность (разбор этого механизма).
И я такой не один. Если полистать issue-трекер Claude Code, там целый жанр:
- #29082 - агент выполнил
rm -rfпо каталогу Flutter-проекта без спроса. - #29249 - на Windows рекурсивное удаление pnpm-worktree через NTFS junction traversal вынесло пользователю кусок профиля и части других проектов.
- #46444 - авто-очистка worktree тихо удалила примерно 10 дней незакоммиченной работы.
Есть даже отдельный разбор от Docker про класс инцидентов "coding agent снёс мне всё". Так что мой случай - не уникальный фейл криворукого, а системная штука: даёшь агенту шелл, получаешь агента, который однажды напишет в этот шелл что-то не то.
Что реально спасло (и что я всё равно потерял)
Спасло меня банальное: почти всё было закоммичено в git. Локальная история цела, git reset/checkout - и файлы вернулись. Проблема была только с тем, что я не успел запушить: эти изменения улетели в пустоту, их не было ни на удалёнке, ни в рабочей копии. Их я потом восстанавливал руками, по памяти, и на это ушла куча времени и нервов.
Вывод номер один, самый скучный и самый рабочий: коммить часто, пушь ещё чаще. Не "в конце дня", а после каждого осмысленного куска. Git worktree тебя не спасёт, а вот дисциплина коммитов - вполне. Незапушенный коммит - это работа, которая существует только пока жив твой диск и твой агент ведёт себя прилично. А он, как выяснилось, не всегда.
Но одной дисциплины мне было мало. Хотелось, чтобы деструктивная команда просто не могла выполниться. Тут и появился guard.
PreToolUse guard: как это работает
В Claude Code есть механизм хуков, и среди них - PreToolUse. Это хук, который срабатывает после того, как модель уже сформировала параметры инструмента, но до того, как инструмент реально выполнится. Идеальная точка, чтобы перехватить команду и сказать "нет".
Работает так. В настройках ты вешаешь хук на инструмент по матчеру (для шелла это Bash):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}Твой скрипт получает на stdin JSON с командой, которую агент собирается запустить. Ключевое поле - tool_input.command. Дальше твоя задача простая: посмотреть на команду и решить, пускать её или нет.
#!/usr/bin/env bash
command=$(jq -r '.tool_input.command' < /dev/stdin)
# ловим удаления с широким радиусом и опасные операции с worktree
if echo "$command" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*[rf]|worktree[[:space:]]+remove.*--force'; then
echo "Заблокировано guard-хуком: слишком опасная команда. Уточни у человека." >&2
exit 2
fi
exit 0Тут важнейший нюанс, на котором легко обжечься: блокирует только exit 2. Не единица, не любой другой код - именно двойка. Если ты по привычке напишешь exit 1, хук отработает как "некритичная ошибка", и команда всё равно выполнится. Я на этом чуть не попался. exit 2 отдаёт текст из stderr обратно модели как ошибку, и агент видит, почему его тормознули.
Если хочется тоньше, есть второй способ - вернуть JSON и порулить решением явно:
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Деструктивная команда заблокирована хуком"
}
}'permissionDecision бывает allow, deny и ask - последнее удобно, когда ты не хочешь наглухо запрещать, а хочешь, чтобы на такой команде тебя переспросили. Все поля и точная семантика - в официальной доке по хукам.
Именно такую штуку я и прибил гвоздями после инцидента. Теперь агент физически не может выполнить широкое удаление, не упёршись в мой скрипт.
Оборона в глубину, а не один хук
Хук - это хорошо, но полагаться на одну стену наивно. Grep по командам можно обойти хитрой конструкцией, которую ты не предусмотрел. Поэтому нормальный подход - несколько слоёв, каждый ловит то, что просочилось сквозь предыдущий:
- Deny-правила в permissions на самые очевидные паттерны - первый и самый дешёвый барьер.
PreToolUseguard, который смотрит на каждую команду глазами твоего скрипта, а не только по статичному списку.- Дисциплина git: частые коммиты и пуши, чтобы любой разрушительный прогон был обратим. Это не про предотвращение, это про "восстановиться за минуту, а не за вечер".
- Сэндбокс на уровне ОС - самый серьёзный слой.
Про сэндбокс отдельно, потому что это относительно свежая и жирная штука. В октябре 2025 Anthropic завезли в Claude Code нативный сэндбоксинг на примитивах операционки: bubblewrap на Linux и Seatbelt на macOS. Смысл в том, что запись по умолчанию разрешена только в текущую рабочую директорию и её подпапки, а всё, что снаружи, - под замком на уровне ядра, а не на уровне "агент обещал так не делать". По их же данным, это снизило число permission-запросов на 84 процента: меньше дёргаешься на каждую команду, потому что опасное просто не выйдет за периметр. Если бы такой сэндбокс стоял у меня в тот вечер, команда бы уткнулась в границу папки и никого бы не снесла.
Так что если у тебя агент с доступом к шеллу и живой проект - не жди своего worktree-инцидента. Собери оборону заранее: deny-правила, guard-хук, привычка пушить, а сверху сэндбокс.
Что в итоге
Kenku жив, за ту же неделю я его накатил вполне жирно, и с тех пор ни один агент мне папку не сносил. Но осадочек остался - тот вечер мне теперь снится в кошмарах, и это, наверное, здоровая паранойя.
Мораль простая. AI-агент - это не злодей, это очень старательный джун с рутовым доступом и без чувства последствий. Он не хочет тебе навредить, он хочет закрыть задачу, и ради этого может выбрать команду с радиусом поражения в полпроекта. Твоя работа - не надеяться на его благоразумие, а поставить стены, через которые он не пройдёт, даже если очень захочет.
Про фейлы, из которых выросли выводы, я уже писал - например, как я четыре дня строил снайпер-бота и закрыл его. А про то, что бывает, когда таких проектов и таких вечеров становится слишком много, - отдельный репортаж про выгорание.
А я пойду поставлю guard ещё и на соседний проект. На всякий.