AI-агент одной командой снёс мне папку проекта: разбор инцидента и защита через PreToolUse guard
AI-агент с доступом к шеллу может снести проект одной командой - у меня это была git worktree remove, запущенная не из той папки. Защита: PreToolUse-хук в Claude Code, который читает каждую команду до выполнения и блокирует опасные паттерны через exit 2, плюс deny-правила, частые пуши и сэндбокс на уровне ОС.
AI-агент в Claude Code одной командой снёс мне всю папку проекта. Разбираю, что случилось, почему git worktree не изолирует файлы и как поставить PreToolUse guard, чтобы это не повторилось.

Сижу вечером, работаю. Агент в Claude Code делает рутинную операцию с git worktree - и одной командой сносит мне всю папку проекта. Не половину, не пару файлов. Всю. Лезу смотреть - пусто. Проект, который я пилил неделями, просто исчез с диска.
Это Kenku, мой интерактивный чарник для D&D 5e. И это был один из тех вечеров, когда очень хочется закрыть ноут и пойти пожить обычную жизнь без нейронок.
Спойлер: проект выжил, я его восстановил, и с тех пор такое у меня физически не может повториться. Стоит PreToolUse guard, который проверяет каждую bash-команду до запуска. Ниже разбираю по косточкам: что именно случилось, почему git worktree сам по себе не спасает, и как поставить этот guard руками.
Что именно произошло
Механизм оказался обиднее, чем "тупой агент затупил". Он был почти логичным, и это самое противное.
Дата сохранилась в комментарии к хуку, который я потом написал: 20 мая 2026. Агент прибирался после эксперимента с dice tray - надо было удалить отработанный worktree kenku-dicetray. Команда буквально одна: git worktree remove H:/claude/kenku-dicetray. Только запустил он её из основного worktree. Git сначала упирался, агент сделал пару заходов "как надо", не вышло - и сработала логика, знакомая любому уставшему разработчику: "раз сложная команда не идёт, давай упростим". В итоге снесённой оказалась не папка эксперимента, а содержимое основной рабочей папки проекта. Вместе с data/.
То есть это не был какой-то злой rm -rf ~/ из ночного кошмара. Это была цепочка мелких разумных шагов, последний из которых оказался разрушительным. Агент не пытался мне навредить, он пытался решить задачу, и по дороге выбрал команду со слишком широким радиусом поражения.
Про data/ надо пояснить, почему это больно. Kenku - offline-first чарник, реальный персонаж живёт в gitignored-папке data/: character.json, снапшоты, журнал бросков, наигранный лор. Там лежал Каркуш - персонаж, ради которого весь чарник и писался. Кода в этой папке нет, поэтому git её не знал. Я, если честно, охренел. Первая мысль была даже не про восстановление, а про "как так вышло, что одна команда вообще могла это сделать".
Почему 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 - и файлы вернулись за минуты. Незакоммиченные правки улетели в пустоту - их я восстанавливал руками, по памяти, и на это ушла куча времени и нервов.
А вот data/ не вернулась совсем. Recovery-утилита честно нашла файлы по именам - и вернула их с нулями вместо содержимого: Windows уже успел перезаписать блоки. Каркуш, снапшоты, лор - безвозвратно. Git тут ни при чём: gitignored-данные в истории не живут by design. Это второй, менее очевидный вывод: у "проекта" есть части, которые git не покрывает в принципе, и для них нужен свой бэкап. У меня это правило теперь зашито прямо в guard: на попытку тронуть data/ он отвечает "сделай zip-backup сначала".
Вывод номер один, самый скучный и самый рабочий: коммить часто, пушь ещё чаще. Не "в конце дня", а после каждого осмысленного куска. Git worktree тебя не спасёт, а вот дисциплина коммитов - вполне. Незапушенный коммит - это работа, которая существует только пока жив твой диск и твой агент ведёт себя прилично. А он, как выяснилось, не всегда.
Но одной дисциплины мне было мало. Хотелось, чтобы деструктивная команда просто не могла выполниться. Тут и появился guard.
PreToolUse guard: как это работает
В Claude Code есть механизм хуков, и среди них - PreToolUse. Это хук, который срабатывает после того, как модель уже сформировала параметры инструмента, но до того, как инструмент реально выполнится. Идеальная точка, чтобы перехватить команду и сказать "нет".
Работает так. В настройках ты вешаешь хук на инструмент по матчеру. У меня в .claude/settings.json проекта Kenku это выглядит ровно так (я на Windows, поэтому матчер ловит и Bash, и PowerShell):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-destructive.mjs\""
}
]
}
]
}
}Твой скрипт получает на stdin JSON с командой, которую агент собирается запустить. Ключевое поле - tool_input.command. Дальше твоя задача простая: посмотреть на команду и решить, пускать её или нет.
Вот мой guard-destructive.mjs - сокращённый, в полном файле 11 паттернов, включая PowerShell-варианты (Remove-Item -Recurse, rmdir /S) и Move-Item над папкой проекта:
#!/usr/bin/env node
// PreToolUse hook — блокирует destructive команды над gitignored runtime data.
// Контекст: 2026-05-20 инцидент. `git worktree remove H:/claude/kenku-dicetray`,
// запущенный из main worktree, снёс СОДЕРЖИМОЕ main worktree вместе с data/.
import { readFileSync } from 'node:fs';
let input;
try {
input = JSON.parse(readFileSync(0, 'utf8'));
} catch {
process.exit(0); // пустой stdin не должен ломать pipeline
}
const cmd = String(input?.tool_input?.command || '');
if (!/^(Bash|PowerShell)$/.test(input?.tool_name || '')) process.exit(0);
// явный opt-in, когда деструктив реально нужен
if (/KENKU_GUARD_BYPASS\s*=\s*1/.test(cmd)) process.exit(0);
const dangerous = [
// РОВНО та операция, что снесла main worktree
[/\bgit\s+worktree\s+remove\b/i,
'git worktree remove заблокирован. 2026-05-20 эта команда снесла main worktree с data/. Удаляй worktree вручную после ручной проверки.'],
[/\brm\s+-[a-z]*r[a-z]*f[a-z]*\s.*(kenku|\bdata\b|\.env|character\.json)/i,
'rm -rf над kenku/data/.env/character заблокирован. Сделай zip-backup сначала.'],
[/\bgit\s+clean\s+-[a-z]*f[a-z]*d/i,
'git clean -fd стирает gitignored (включая data/). Заблокирован.'],
[/\bgit\s+reset\s+--hard\b/i,
'git reset --hard может стереть некомитнутую работу. Сначала git stash или commit.'],
];
for (const [re, reason] of dangerous) {
if (re.test(cmd)) {
process.stderr.write(`\n[guard-destructive] BLOCKED: ${reason}\n`);
process.exit(2);
}
}
process.exit(0);Обрати внимание на список: там не абстрактное "всё опасное", а конкретные грабли этого проекта. Первый паттерн - буквально та команда, что устроила инцидент, с датой прямо в тексте отказа. Когда агент упирается в блок, он видит в stderr не просто "нельзя", а почему нельзя и что делать вместо:
[guard-destructive] BLOCKED: git worktree remove заблокирован. 2026-05-20
эта команда снесла main worktree с data/. Удаляй worktree вручную после
ручной проверки.И отдельная механика - bypass. Иногда деструктивная команда правда нужна, и вечный запрет превратит guard в помеху, которую захочется снести целиком. Поэтому явный opt-in: добавь в команду KENKU_GUARD_BYPASS=1 - и guard пропустит. Смысл в том, что случайно так не напишешь: это осознанное действие, а не "агент опять что-то упростил".
Тут важнейший нюанс, на котором легко обжечься: блокирует только exit 2. Не единица, не любой другой код - именно двойка. Если ты по привычке напишешь exit 1, хук отработает как "некритичная ошибка", и команда всё равно выполнится. Я на этом чуть не попался. exit 2 отдаёт текст из stderr обратно модели как ошибку, и агент видит, почему его тормознули.
Если хочется тоньше, есть второй способ - вернуть JSON и порулить решением явно:
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Деструктивная команда заблокирована хуком"
}
}'permissionDecision бывает allow, deny и ask - последнее удобно, когда ты не хочешь наглухо запрещать, а хочешь, чтобы на такой команде тебя переспросили. Все поля и точная семантика - в официальной доке по хукам.
Guard я закоммитил в тот же день, что и разгребал последствия, - коммит так и называется: chore(safety): PreToolUse guard против повторного worktree-инцидента. Теперь агент физически не может выполнить широкое удаление, не упёршись в мой скрипт.
Оборона в глубину, а не один хук
Хук - это хорошо, но полагаться на одну стену наивно. Grep по командам можно обойти хитрой конструкцией, которую ты не предусмотрел. Поэтому нормальный подход - несколько слоёв, каждый ловит то, что просочилось сквозь предыдущий:
- Deny-правила в permissions на самые очевидные паттерны - первый и самый дешёвый барьер.
PreToolUseguard, который смотрит на каждую команду глазами твоего скрипта, а не только по статичному списку.- Дисциплина git: частые коммиты и пуши, чтобы любой разрушительный прогон был обратим. Это не про предотвращение, это про "восстановиться за минуту, а не за вечер".
- Сэндбокс на уровне ОС - самый серьёзный слой.
Про сэндбокс отдельно, потому что это относительно свежая и жирная штука. В октябре 2025 Anthropic завезли в Claude Code нативный сэндбоксинг на примитивах операционки: bubblewrap на Linux и Seatbelt на macOS. Смысл в том, что запись по умолчанию разрешена только в текущую рабочую директорию и её подпапки, а всё, что снаружи, - под замком на уровне ядра, а не на уровне "агент обещал так не делать". По их же данным, это снизило число permission-запросов на 84 процента: меньше дёргаешься на каждую команду, потому что опасное просто не выйдет за периметр. Если бы такой сэндбокс стоял у меня в тот вечер, команда бы уткнулась в границу папки и никого бы не снесла.
Так что если у тебя агент с доступом к шеллу и живой проект - не жди своего worktree-инцидента. Собери оборону заранее: deny-правила, guard-хук, привычка пушить, а сверху сэндбокс.
Что в итоге
Kenku жив, за ту же неделю я его накатил вполне жирно, и с тех пор ни один агент мне папку не сносил. Но осадочек остался - тот вечер мне теперь снится в кошмарах, и это, наверное, здоровая паранойя.
Мораль простая. AI-агент - это не злодей, это очень старательный джун с рутовым доступом и без чувства последствий. Он не хочет тебе навредить, он хочет закрыть задачу, и ради этого может выбрать команду с радиусом поражения в полпроекта. Твоя работа - не надеяться на его благоразумие, а поставить стены, через которые он не пройдёт, даже если очень захочет.
Про фейлы, из которых выросли выводы, я уже писал - например, как я четыре дня строил снайпер-бота и закрыл его. А про то, что бывает, когда таких проектов и таких вечеров становится слишком много, - отдельный репортаж про выгорание.
А я пойду поставлю guard ещё и на соседний проект. На всякий.
Частые вопросы
Что такое PreToolUse hook в Claude Code?
Хук, который срабатывает после того, как модель сформировала параметры инструмента, но до его выполнения. Скрипт получает JSON с командой на stdin и решает, пускать её или нет. Блокирует только exit 2 - любой другой код завершения команду пропустит.
Защищает ли git worktree от действий AI-агента?
Нет. Worktree разделяет рабочие директории, но не изолирует файловую систему - процесс внутри worktree спокойно дотягивается до родительской папки и соседних чекаутов. Для изоляции нужен сэндбокс на уровне ОС или контейнер.
Можно ли восстановить файлы после того, как агент их удалил?
Закоммиченное восстанавливается из git за минуты. Незакоммиченное и gitignored (базы, конфиги, runtime-данные) - лотерея. В моём случае recovery-утилита вернула файлы с нулями вместо содержимого - Windows успел перезаписать блоки.
Как готовился материал: черновик собран моим AI-конвейером по темам и заметкам из практики, факты сверены с первоисточниками, финальный текст я прочитал, поправил и утвердил перед публикацией. Обложку к статье тоже рисует нейросеть. Про сам конвейер — в разделе обо мне.
