ГлавнаяБлогЗащитные ограждения для ИИ-агентов: мой опыт с Claude Code
AI / Нейросети

Защитные ограждения для ИИ-агентов: мой опыт с Claude Code

Узнайте, как настроить защитные ограждения для Claude Code, чтобы предотвратить опасные действия ИИ-агента. Практические примеры и готовые правила.

Al
Редакция Algolitalgolit.ru
12 мин чтения21 августа 2026 г.

Почему ИИ-агент однажды попытался сделать force-push в main

Три месяца я использую Claude Code как основного помощника для разработки. Он пишет Ansible-плейбуки, на которые у меня ушла бы неделя. Он читает кодовую базу быстрее меня. Он действительно хорош. Но однажды он захотел сделать force-push в main — и у него была очень веская причина.

Остановитесь на секунду. Это ключевой момент. Ребейз завис, и force-push решил бы проблему. Каждое звено в цепочке рассуждений агента было логичным. Он не был небрежен, не галлюцинировал, не «дрейфовал». Он принял локально правильное решение с глобальными последствиями — именно ту ошибку, которую человек-ревьюер замечает хуже всего, потому что диф выглядит нормально.

Агент видел команду, но не видел кратер, который она оставит.

Что я перестал делать

Сначала я пытался проверять всё: каждый диф, каждую команду, держа руку на Ctrl-C, как отец у лестницы с малышом. Это не масштабируется. Причина проста: объём проверки растёт вместе с объёмом того, что пишет агент. А этот объём растёт только в одну сторону — вверх.

Поэтому я изменил подход: вместо проверки того, что агент делает, я записал, что ему нельзя делать никогда. И хорошая новость: этот список оказался коротким. Не «коротким для политики безопасности», а коротким, как записка на салфетке.

Вот мой список:

  • Секрет, прочитанный час назад, встраивается в исходный файл.
  • Ребейз завис, и самый быстрый путь к зелёному терминалу — git push --force origin main.
  • rm -rf "$BUILD_DIR/" выполняется на машине, где BUILD_DIR никогда не была задана.
  • Версия вручную правится в package-lock.json, потому что это файл, где видно номер версии.
  • Падающий тест тихо получает .skip, и CI зеленеет.
  • Кто-то запускает cat .env, чтобы «просто посмотреть, какие переменные есть».

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

Как Claude Code позволяет сказать «нет»

Многие не знают, что в Claude Code есть хук PreToolUse. Он срабатывает до любого вызова инструмента. Ваш скрипт получает весь контекст через stdin:

{
  "session_id": "abc123",
  "cwd": "/home/rabih/app",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "git push --force origin main"
  }
}

И вы можете отказать:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Это force-push в main, общую ветку."
  }
}

Самое удивительное: строка permissionDecisionReason — агент её читает и реагирует. Если сказать «заблокировано», он пожмёт плечами и попробует чуть другой синтаксис, как кот, проверяющий закрытую дверь. Если сказать «измени манифест и запусти pnpm add» — он сделает именно это, с первой попытки, без споров.

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

Поэтому каждое моё правило должно отвечать на два вопроса: что не так и что делать вместо этого.

Тринадцать правил

Все они живут в репозитории claude-guardrails.

ПравилоБлокирует
secrets-never-land-in-sourceЛитералы, похожие на секреты, в исходном коде
secret-files-stay-out-of-contextЧтение .env, *.pem, ~/.aws/credentials в сессию
secrets-are-not-stagedgit add -A в репозитории, где .env не в .gitignore
shared-branches-are-not-rewrittengit push --force в main, develop, release/*
uncommitted-work-is-not-discardedgit reset --hard, git clean -fd, git stash drop
verification-hooks-are-not-bypassed--no-verify, HUSKY=0, --no-gpg-sign
unexpanded-variables-in-destructive-pathsrm -rf "$DIR/" где $DIR может быть пустым
remote-code-is-not-piped-to-a-shellcurl ... | sh
committed-migrations-are-immutableРедактирование уже закоммиченной миграции
destructive-sql-needs-a-whereБезграничные DELETE/UPDATE, ad-hoc TRUNCATE
cluster-targets-are-explicitДеструктивный kubectl без --context
tests-are-not-silencedДобавление .skip, @Disabled, continue-on-error: true
lockfiles-are-generated-not-editedРучное редактирование package-lock.json и подобных

Ноль зависимостей. Нечего настраивать. Node читает JSON и иногда говорит «нет».

Четыре неожиданных открытия

1. Чтение секрета хуже, чем его запись

Моим первым порывом было защитить запись — не дать ключу попасть в файл. Но потом я подумал и понял, что всё наоборот. Путь записи проходит через код-ревью: кто-то рано или поздно посмотрит диф. А путь чтения не имеет ничего. Когда агент запускает cat .env, чтобы проверить, какие переменные есть, он получает разумный ответ на разумный вопрос — и все значения из файла оказываются в транскрипте. Транскрипты хранятся, синхронизируются, иногда вставляются в баг-репорт добрым человеком.

На диске ничего не меняется, git diff пуст, а ваши учётные данные уже покинули здание. Поэтому правило блокирует чтение и предлагает вместо этого: grep -o "^[A-Z_]*=" .env — тот же вопрос, тот же ответ, но без части, которая портит вам неделю.

2. «Уже применено» — неизвестно, «уже закоммичено» — известно

Я хотел правило, которое запрещает редактировать миграцию, уже выполненную базой данных. Проблема: хук не знает, что выполнила ваша продакшн-база. Это Node-скрипт с JSON-объектом, он не может позвонить в Postgres. Но он может задать git один вопрос:

execFileSync('git', ['ls-files', '--error-unmatch', '--', pathspec], { cwd, stdio: 'ignore' });

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

3. Опасный git add — тот, который выглядит безобидно

git add .env — это нормально, честно. Он видимый, он прямо в истории, вы его заметите. А вот git add -A в репозитории, где забыли добавить .env в .gitignore — он тихо добавит файл в индекс вместе с сорока другими, коммит будет называться «add feature», никто не посмотрит, и всё окажется на GitHub.

Поэтому это правило вообще не сопоставляет команду по шаблону. Оно спрашивает git, что реально попадёт в индекс при массовом добавлении:

execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, encoding: 'utf8' })

Мне тихо приятно, что файлы из .gitignore никогда не появляются в этом выводе. Значит, в правильно настроенном репозитории правило полностью и навсегда молчит. Оно говорит только с теми репозиториями, у которых есть проблема. Правило, которого никто не замечает, — правило, которое никто не удалит. Это свойство ценнее самой проверки.

4. Одно правило просто признаёт поражение

kubectl delete pod api-7f9d. Какой это кластер? Я не знаю. Вы не знаете. Агент не знает. Хук точно не знает, потому что ответ живёт в конфигурационном файле, которого нет в полезной нагрузке.

Каждое другое правило в этом репозитории считывает намерение из вызова инструмента. Это не может. Поэтому оно делает единственное честное: отказывает, пока вы не укажете --context и не заставите команду явно сказать, что она собирается менять. Оно не блокирует ошибку, оно блокирует двусмысленность — команду, чей транскрипт не зафиксирует, что она сделала. Думаю, это самое полезное правило в наборе, и оно единственное, которое работает, признавая, что не видит.

Три правила, которые я принял перед тем, как допустить чужие ограждения

Если этот репозиторий вообще работает, большинство правил в нём рано или поздно напишут незнакомцы. Это меняет задачу полностью.

Сломанное правило не должно блокировать вызов инструмента

Кто-то допустит баг. Если его баг сломает мой git push, вся идея умрёт. Поэтому каждое правило обёрнуто в собственный try/catch, и исключение трактуется как «нет мнения» с ворчанием в stderr. Да, это значит, что упавшее правило открывает доступ — для инструмента безопасности это звучит непростительно, пока вы не представите альтернативу: одна неудачная правка — и никто в мире не сможет коммитить, пока её не откатят. Плагин удалят, а удалённый плагин не защищает ничего. Открытый отказ позволяет остаться установленным. Установленность — это вся игра.

Тишина означает разрешение

Диспетчер выдаёт JSON только для отказа. permissionDecision с радостью примет «allow», что затоптало бы ваши собственные настройки разрешений — а этот плагин не имеет права так делать. У него один голос, и этот голос — «нет».

Точность важнее полноты, и это не близко

Один ложный срабатывание на правильной работе — и плагина нет к обеду. Поэтому каждое правило сопровождается исполняемыми примерами:

examples: {
  blocked: [
    { tool_name: 'Bash', tool_input: { command: 'git push --force origin main' } }
  ],
  allowed: [
    { tool_name: 'Bash', tool_input: { command: 'git push --force-with-lease origin main' } },
    { tool_name: 'Bash', tool_input: { command: 'git push --force origin feature/x' } }
  ]
}

--force-with-lease против --force. .env.example против .env. docs/package-lock.md против package-lock.json. Вот где живут ложные срабатывания, поэтому именно это нужно записывать.

И эти примеры и есть тест-сьют. npm test проходит по каждому правилу и проверяет оба списка. Это дизайн-решение, которым я доволен больше всего, и оно потребовало больше всего времени, чтобы его увидеть. Очевидная версия репозитория имеет папку guards/ и папку test/, и контрибьюторы пишут оба. Но они не пишут. Никто не пишет второй файл. Никогда. Встраивание тестов в определение правила означает, что контрибуция — это один файл, и этот файл недействителен, пока вы не укажете в коде, что он намеренно пропускает.

Заметка о bash, раз кто-то спросит

Это Node, а не shell. Shell-версии были бы втрое короче и зависели бы от jq. Я писал это на Windows. Значительная часть людей, которым это нужно, не сидят в Unix-шелл, а ограждение, которое защищает только разработчиков с уже хорошими инструментами, — довольно бесполезное ограждение. Node поставляется с Claude Code, зависимость уже оплачена. Кстати, весь плагин имеет ноль зависимостей, так что у него нет lockfile — что забавно для проекта, который поставляет правило для lockfile.

Ваша очередь

Единица контрибуции — один файл. Скопируйте guards/_template.js, измените пять вещей, откройте PR. Десять минут, максимум.

module.exports = {
  id: 'your-guard-id',
  title: 'Краткая формулировка правила',
  prevents: 'Конкретная проблема, которая случается, когда никто не смотрит.',
  tools: ['Bash'],
  check(input) {
    // верните { reason } для отказа или null, чтобы не мешать
  },
  examples: {
    blocked: [ /* полезные нагрузки, которые должны быть отклонены */ ],
    allowed: [ /* полезные нагрузки, которые должны проходить */ ]
  }
};

Положите его в guards/ — и он заработает. Никакого реестра обновлять не нужно, диспетчер просто читает директорию.

Одно поле решает, примут ли ваш PR, — это prevents. «Это плохая практика» — не prevents. Если вы не можете закончить предложение «когда это случилось в последний раз, что сломалось…», у вас стилистическое предпочтение, и таким предпочтениям место в вашем собственном CLAUDE.md.

Кстати, вот почему я остановился на тринадцати. Я вижу очертания ещё четырёх правил, которые напрашиваются. Но это уже другая история.

Практический вывод

Прямо сейчас, не откладывая, сделайте три вещи. Во-первых, возьмите мой список из тринадцати запретов и адаптируйте под свой проект — вы удивитесь, сколько из них применимы к вашей кодовой базе. Во-вторых, добавьте хотя бы одно правило, которое блокирует чтение .env — это самое дешёвое и эффективное улучшение безопасности. В-третьих, настройте правило для git push --force с подсказкой использовать --force-with-lease. Эти три шага займут не больше часа, а сэкономят вам неделю боли и, возможно, спасут ваш репозиторий от катастрофы.

#Claude Code#ИИ-агенты#безопасность кода#защитные ограждения#разработка
Al
Редакция Algolit

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

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →