Разрабатывая Yulo, приложение для подготовки к экзамену telc b1 по немецкому языку, я стал всё больше замечать, что обновления пакетов выходят быстрее, чем я успеваю их ставить. В эпоху атак на цепочку поставок с использованием ИИ важно не обновляться просто потому, что прилетело очередное обновление. Но также важно и не затягивать с ними, ведь в ваших пакетах может скрываться спящий эксплойт, ждущий подходящего момента для удара.
Проблема: `install` — это фича для выполнения произвольного кода
Экосистемы пакетов, от которых мы все зависим, за последние несколько лет наглядно показали, насколько всё может быть плохо. В сентябре 2025 года chalk и debug, входившие в партию из 18 пакетов с суммарным объёмом более двух миллиардов скачиваний в неделю, начали поставлять криптоклиппер. Это произошло после того, как аккаунт одного из мейнтейнеров в npm зафишили через фейковое письмо со сбросом 2FA.
Спустя несколько дней червь Shai-Hulud самостоятельно прошёлся по сотням пакетов: его post-install скрипт воровал токены npm с каждой машины, на которую попадал, и использовал их для публикации новых заражённых версий самого себя. А за пару недель до этого компрометация Nx занесла на машины разработчиков post-install пейлоад, который заставлял локально установленные ИИ-CLI для кодинга вроде Claude и Gemini искать кошельки и учётные данные для сбора и выгрузки. Этот последний случай должен заставить каждого владельца агентов насторожиться: наши собственные агенты вербуются в качестве взломщиков. Сценарий всегда один: выходит вредоносная версия, наносит ущерб в течение нескольких часов или дней, а затем её засекают и удаляют.
Исходя из этого, я решил вообще не ставить обновления, пока не будет выполнен ряд правил. И эти правила я решил зашить в Claude skills, чтобы с ними разбирались мои агенты.
AI Agent Skills: паранойя в виде конфига
В Claude Code скилл (skill) — это просто markdown-файл с инструкциями, которые агент загружает, когда задача соответствует условиям. Это даёт мне возможность зафиксировать свою выстраданную паранойю *один раз* и применять её *каждый божий раз* с помощью исполнителя, который никогда не устаёт, не косячит в пятницу вечером и не думает: «да ладно, и так сойдёт».
На данный момент я написал два скилла: package-update-js и package-update-php. Один для моих проектов на TypeScript, другой — для Laravel PHP. Инструменты в них отличаются, но подходы одинаковые. Вот эти правила.
Правило 1: Сначала исследование, потом действия
Первая фаза — это вообще не обновление. Агент должен исследовать инциденты безопасности в цепочке поставок за последние шесть месяцев: скомпрометированные пакеты, кампании тайпсквоттинга и подозрительные передачи прав на пакеты (именно так хакнули event-stream в 2018 году, когда отзывчивый незнакомец предложил помощь в поддержке проекта, а затем тихо вшил стилер биткоин-кошельков для двух миллионов пользователей в неделю).
Агент сверяет эти данные с фактическим списком зависимостей проекта и дополнительно прогоняет встроенный аудит реестра ( composer audit, данные npm advisory). Каждый пакет получает вердикт: SAFE, AFFECTED или INVESTIGATE.
И только после готовности этого отчёта начинается обновление.
Rule 2: Семь дней карантина
Это правило я бы вытатуировал на всей экосистеме, если бы мог: никогда не устанавливайте версию, с момента релиза которой прошло меньше семи дней. Некоторые пакетные менеджеры, такие как pnpm и Bun, уже добавили поддержку минимального возраста релиза для удобного соблюдения этого правила.
Скомпрометированные релизы почти всегда живут недолго. Отравленные версии chalk и debug продержались около двух часов, прежде чем npm их удалил. Вредоносные релизы Nx прожили четыре часа. Вредоносный релиз выходит, кто-то это замечает, его удаляют. Задержка по возрасту релиза означает, что окно атаки проходит полностью мимо вас. Каждый из описанных выше инцидентов прошёл бы мимо меня, даже не задев рабочую машину — и не потому, что я такой бдительный, а потому что версии были бы слишком «свежими» для установки.
В моих проектах на pnpm это настраивается одной строкой в pnpm-workspace.yaml (значение задаётся в минутах, поэтому 10080 — это семь дней):
minimumReleaseAge: 10080
Начиная с pnpm 11 однодневный карантин вообще включен по умолчанию, а у Bun есть аналогичная опция minimumReleaseAge в bunfig.toml. У Composer нет прямого аналога, поэтому PHP-скилл проверяет это вручную: он считывает дату релиза каждой кандидатной версии, и всё, что моложе семи дней, пропускается с пометкой «слишком свежее, проверить позже».
Есть одно исключение, работающее в обратную сторону: релиз, закрывающий известную CVE, накатывается немедленно, независимо от карантина. Уязвимость, с которой вы уже живёте в продакшене — это больший риск, чем трехдневная версия пакета.
Правило 3: Скрипты не ранят, пока я не разрешу
Каждое обновление Composer запускается с флагом --no-scripts. Post-install скрипты — это классический вектор доставки пейлоада, который даёт практически полный доступ к шеллу на вашей машине в автоматическом режиме. Именно так Nx собирал SSH-ключи и кошельки, и именно так Shai-Hulud находил токены npm для дальнейшего распространения.
Скилл устанавливает пакет, ждёт прохождения проверок и запускает скрипты с регенерацией автозагрузки только после того, как обновление было осмысленно изучено.
В JS-экосистеме с этим получше, чем в Composer. pnpm отказывается запускать build-скрипты зависимостей, пока вы явно не внесёте их в белый список. Поэтому в моём pnpm-workspace.yaml указаны только два пакета, которым разрешено что-либо выполнять при установке:
allowBuilds: esbuild: true sharp: true
Всё остальное устанавливается, но никогда не исполняется. В этом же файле задано blockExoticSubdeps: true, что запрещает транзитивным зависимостям пролезать из случайных git-репозиториев или по URL-адресам тарболов в обход официального реестра. Bun придерживается аналогичной позиции и по умолчанию не запускает скрипты жизненного цикла для сторонних зависимостей.
Правило 4: Запинивайте всё намертво
Никаких ^, никаких ~, никаких диапазонов. Каждая зависимость зафиксирована на точную версию, а обновления происходят через точечные команды вида composer require package:123 или pnpm add package@123, и никогда — через ковровое обновление. Мой pnpm-workspace.yaml подкрепляет это настройкой savePrefix: “”, благодаря которой pnpm по умолчанию сохраняет точные версии вместо диапазонов с ^. Это даёт два плюса: делает каждое изменение версии видимым в diff (ничего не съезжает тихо при следующей перегенерации локфайла) и гарантирует, что агент никогда «случайно» не подтянет то, на что я не давал согласия.
Правило 5: По одному за раз с проверкой после каждого шага
Скиллы запрещают обновлять всё скопом. Каждый пакет или группа пакетов экосистемы (так как вещи вроде ядра Laravel или семейства TanStack должны обновляться синхронно) обновляются по отдельности, после чего сразу следует проверка: проверка типов для TypeScript, PHPStan и полный прогон тестов для PHP. Если что-то ломается, мы точно знаем, какое именно обновление к этому привело. Мажорные версии никогда не применяются без демонстрации breaking changes и получения явного «да» от меня.
Невероятная эффективность зафиксированных правил
Вот что меня действительно удивило: агент, следующий этим скиллам, проводит обновления безопаснее, чем я когда-либо делал это вручную. Раньше я пролистывал чейнджлоги, когда уставал. Я ни разу в жизни не проверял дату релиза пакета перед его установкой. И уж точно за всю свою карьеру я регулярно ранил `composer update` с включенными скриптами.
Скилл не пропускает шаги просто потому, что возможность пропустить шаг в него не заложена. Теперь мои обновления происходят каждую неделю, а проверки на атаки полностью делегированы.
Ознакомиться со скиллами можно здесь.
Комментарии (0)
Пока нет комментариев — будьте первым.