Я поставил перед агентом перехватчик разрушительных команд и шестнадцать дней писал в журнал всё, что агент делал. Хотел получить красивую статистику предотвращённых катастроф. Получил единицу.

Дальше — что было в журнале и почему единица оказалась интереснее, чем то, на что я рассчитывал.

Зачем вообще считать

Историй про агентов, снёсших чужие данные, хватает.

В апреле агент Cursor удалил продакшн‑базу PocketOS за девять секунд. Ни взлома, ни prompt injection: агент увидел несоответствие креденшелов, нашёл токен с подходящими правами и выполнил одну GraphQL‑мутацию к API Railway. Мутация снесла том — вместе со всеми бэкапами, потому что Railway хранил их там же. Последняя пригодная копия оказалась трёхмесячной давности.

В декабре на r/ClaudeAI собрал полторы тысячи апвоутов пост «Claude CLI удалил мой домашний каталог». Человек попросил прибраться в старом репозитории, агент собрал команду удаления с лишним ~/ на конце.

Проблема этих историй в том, что они рассказываются постфактум и только тогда, когда что‑то сломалось. По ним нельзя понять ни как часто такое случается, ни — что важнее для практики — сколько будет стоить защита в повседневной работе. Инструмент, который дёргает пять раз в день, снесут в первую же неделю, даже если он однажды спас бы вам базу.

Измерений я нигде не нашёл. Поэтому померил у себя.

Из чего состоит работа агента

Шестнадцать дней, четыре сессии, 1496 зафиксированных действий (снимок журнала на 24 августа):

Действие

Сколько

Доля

выполнение bash‑команд

853

57,0%

правки файлов

244

16,3%

чтение файлов

124

8,3%

запись новых файлов

97

6,5%

остальное (поиск, задачи, MCP)

178

11,9%

Больше половины — bash. Не правки кода, ради которых агента вроде бы и зовут, а команды в оболочке: sshpsqlcurldocker, установка пакетов, деплой.

У этого есть неприятное следствие. Встроенные механизмы отката — /rewind в Claude Code, Checkpoints в Cursor — работают на уровне файловых правок. Изменённый исходник они вернут прекрасно. Что произошло внутри psql или на том конце ssh, они не видят и вернуть не могут. Строка, удалённая из базы, отправленный запрос к платёжному API, прошедший деплой — всё это вне их досягаемости, а это больше половины того, чем агент занят.

Ещё цифра: из 853 команд уникальных 836. Агент почти не повторяется. Читать такой поток глазами невозможно физически, и именно поэтому люди включают автоодобрение — а дальше уже не важно, что там предлагается выполнить.

Шестьдесят перехватов, из которых настоящий один

В журнале перехватчика 60 записей. Цифра для лендинга отличная. Разбираем.

45 записей оказались тестовыми фикстурами. Это payload'ы из моего же набора тестов — DROP TABLE users;psql -c "DROP DATABASE prod;" и ещё четыре десятка похожих. Они пишутся в журнал при каждом прогоне тестов.

Отдельно скажу, как я на этом чуть не погорел. Первый фильтр, который я написал для разделения, искал в тексте команды упоминания самого инструмента — пути к тестам, имена скриптов. Он поймал четырнадцать записей и пропустил все сорок пять фикстур: они же выглядят как обычные опасные команды, ничего «тестового» в тексте нет. Отсеклись только когда я сверил журнал напрямую со списками из run_tests.py. Не сверь я — писал бы сейчас статью с цифрой, завышенной в шестьдесят раз.

14 записей — ложные срабатывания. Про них ниже, это самое интересное.

Одна запись — настоящая.

Одна за 853 команды. 0,12%.

Пятнадцатое августа

ssh -i ~/.ssh/... root@... "sudo -u postgres psql -c 'DROP DATABASE klyvo;' \
  && sudo -u postgres psql -c 'DROP ROLE klyvo;'"

Заблокировано жёстко, без запроса подтверждения.

Что происходило: я подключал новый сервис к PostgreSQL и обнаружил, что база с нужным именем уже занята. Снести и создать заново выглядело очевидным решением — я был уверен, что это остатки моего же старого прототипа.

База принадлежала другому проекту, про который я успел забыть. Бэкапов у неё не было.

Агент тут ни в чём не виноват. Он выполнял ровно то, о чём я попросил, и делал это хорошо: собрал команду, добавил удаление роли, отправил одной строкой по ssh. Проблема была в скорости — между «а снесу‑ка я её» и необратимым действием не оставалось паузы, в которую влезает мысль «стоп, а чья это база вообще».

Вот на что такая защита годится: она возвращает эту паузу. Никакого противодействия взбунтовавшемуся ИИ — просто полсекунды задержки там, где их съела автоматизация.

Четырнадцать раз, когда перехватчик мешал

Теперь неприятное. У всех четырнадцати ложных срабатываний одна природа, и она смешная.

python3 - <<'PY'
p="README.md"; t=open(p).read()
bad = "`terraform destroy`)., а также ..."

Скрипт правит опечатку в документации. Ничего не удаляет. Но в тексте команды есть слова terraform destroy, а движок сигнатурный — он читает текст и не отличает исполнение от упоминания.

Остальные тринадцать такие же: патч, дописывающий правила в файл правил; heredoc с описанием того, что надо блокировать; удаление одноразовой тестовой базы rm -f local.db. Перехватчик срабатывал ровно тогда, когда я писал про разрушительные команды.

Обычной работе — сборкам, миграциям, деплою, правкам кода — он за шестнадцать дней не помешал ни разу.

Чинится ли это? Нет. Отличить исполнение от упоминания сигнатурами нельзя, для этого нужно понимать смысл команды, а не её текст. Из той же природы следует и обратная беда: обход тривиален. "DR" "OP TABLE" не совпадёт ни с одним паттерном. Base64 тоже. find . -name '*.db' -deleteсделает работу rm, не содержа rm.

Так что честная формулировка: это защита от спешки, а не от умысла. Против агента, который торопится и не думает о последствиях, работает. Против агента, который целенаправленно обходит проверку, — нет.

Проверка на себе: PocketOS я бы не поймал

Раз уж я начал статью с чужого инцидента, логично прогнать его через свои же правила.

curl -X POST https://backboard.railway.app/graphql/v2 \
  -d '{"query":"mutation{volumeDelete(id:\"prod\")}"}'

Пропущено. Как и railway volume deleteflyctl volumes destroyaws rds delete-db-instance.

Правила покрывали SQL, CLI баз данных, миграционные фреймворки, NoSQL, docker и kubernetes. Из облачных провайдеров — Supabase, Turso и D1, и на этом всё. Целого класса «разрушительный вызов к API хостинга» не существовало, хотя именно так и убили PocketOS.

Ровно это и показывает, откуда берутся такие правила. Их пишут по следам чужих аварий: сначала кто‑то теряет базу, потом появляется паттерн. Моя модель угроз этого класса просто не содержала — я о нём не подумал, пока не прочитал разбор. Приятного в этом мало, но притворяться, что бывает иначе, смысла нет.

Что я из этого вынес

Главный вопрос при установке такой штуки — не сколько катастроф она предотвратит, а сколько раз в день она вас дёрнет. По моим данным в обычной работе — ноль. Один перехват за 853 команды звучит скромно ровно до того момента, пока не вспомнишь, что этот один стоил бы мне чужой базы без бэкапов.

Отдельно про наблюдение без блокировок: журнал сессии сам по себе оказался полезнее, чем я ожидал. На вопрос «что вообще произошло за эти три часа» иначе ответить нечем — контекст сессии не сохраняется, а история shell не покажет, что агент читал и правил.

И две вещи, которые важнее перехватчика. Первая: в истории PocketOS фатальной была не мутация, а бэкапы в удаляемом томе; никакая проверка команд этого не компенсирует. Вторая: посмотрите, что именно откатывает ваш инструмент. Если больше половины действий агента — bash, а откат работает по файлам, он закрывает меньшую часть происходящего, и знать об этом лучше заранее.


Журнал собран моим инструментом — Klyvo, хук перед выполнением команды, локально, без сетевых вызовов, MIT. Правила по хостинг‑провайдерам, которых не хватило в разборе выше, я добавил уже после.

Но интереснее мне другое. У кого‑нибудь есть похожие журналы? Я искал и нашёл только пересказы инцидентов, а измерений — ни одного. Если вы что‑то подобное вели, поделитесь цифрами, особенно долей ложных срабатываний: одна выборка от одного человека — это не данные, это анекдот с таблицей.