Безопасность

Hermes Agent спроектирован по принципу эшелонированной защиты (defense-in-depth). На этой странице разобраны все рубежи безопасности — от подтверждения команд до изоляции контейнеров и авторизации пользователей в мессенджерах.

Обзор

Модель безопасности состоит из восьми уровней:

  1. Авторизация пользователей — кто вообще может писать агенту (allowlist-списки, парный доступ через DM)
  2. Подтверждение опасных команд — человек в контуре принятия решений для деструктивных операций
  3. Безопасность записи файлов — запретный список и опциональная песочница записи для write_file/patch
  4. Изоляция контейнеров — песочница Docker/Singularity/Modal с усиленными настройками
  5. Фильтрация MCP-учётных данных — изоляция переменных окружения для подпроцессов MCP
  6. Сканирование контекстных файлов — обнаружение промпт-инъекций в файлах проекта
  7. Изоляция между сессиями — сессии не могут получить доступ к чужим данным или состоянию; пути хранения cron-задач защищены от атак path traversal
  8. Санитизация входных данных — параметры рабочей директории в бэкендах терминальных инструментов проверяются по allowlist, чтобы исключить shell-инъекции

Подтверждение опасных команд

Перед выполнением любой команды Hermes сверяет её со списком опасных шаблонов. При совпадении пользователь должен явно подтвердить выполнение.

Режимы подтверждения

Система подтверждений поддерживает три режима, которые задаются через approvals.mode в ~/.hermes/config.yaml:

approvals:
  mode: smart                     # smart | manual | off
  timeout: 300                    # секунд ожидания ответа пользователя (по умолчанию: 300)
  cron_mode: deny                 # deny | approve — поведение cron-задач при встрече с опасной командой
  single_query_mode: deny         # deny | approve — поведение одноразовых сессий (-q) при встрече с опасной командой
  unattended_mode: deny           # deny | approve — поведение сессий webhook/API при встрече с опасной командой
  mcp_reload_confirm: true        # /reload-mcp спрашивает подтверждение перед сбросом кэша инструментов MCP
  destructive_slash_confirm: true # /clear, /new, /reset, /undo запрашивают подтверждение перед сбросом состояния

Полный набор ключей:

КлючПо умолчаниюЧто регулирует
modesmartПолитика подтверждения для опасных shell-команд — см. таблицу ниже.
timeout300Сколько секунд Hermes ждёт ответа на запрос подтверждения, прежде чем сработает таймаут.
cron_modedenyКак ведут себя cron-задачи в фоновом режиме, когда срабатывает запрос на опасную команду. deny блокирует команду (агент должен найти другой путь решения); approve автоматически подтверждает всё в контексте cron.
single_query_modedenyКак ведут себя одноразовые сессии hermes chat -q, когда срабатывает запрос на опасную команду. Сессия -q выполняет один ход и завершается — отвечать на запросы подтверждения некому; deny блокирует команду (агент должен найти другой путь решения), approve автоматически подтверждает всё в контексте single-query. Аналогично cron_mode.
unattended_modedenyКак ведут себя сессии на автономных программных платформах (webhook, msgraph_webhook, api_server), когда срабатывает запрос на опасную команду. На этих платформах некому ответить на /approve, поэтому вместо ожидания полного таймаута подтверждения deny блокирует команду мгновенно (агент должен найти другой путь решения), а approve автоматически подтверждает всё в автономном контексте. Исключение: сессия api_server, чей клиент умеет ответить на карточку (/v1/runs и потоковые chat completions через POST /v1/runs/{id}/approval), запрос на подтверждение всё же получает. Аналогично cron_mode.
mcp_reload_confirmtrueЕсли включено, /reload-mcp спрашивает подтверждение перед пересборкой набора инструментов MCP. Пересборка инвалидирует кэш промпта провайдера (схемы инструментов живут в системном промпте), поэтому следующее сообщение заново отправит все входные токены полностью. Пользователи, нажавшие Always Approve, переключают этот ключ в false.
destructive_slash_confirmtrueЕсли включено, деструктивные слэш-команды сессии (/clear, /new, /reset, /undo) запрашивают подтверждение перед сбросом состояния разговора. Диалог с тремя вариантами (Approve Once / Always Approve / Cancel), реализованный через нативные кнопки да/нет в Telegram, Discord и Slack; в остальных случаях — текстовый резервный вариант. Пользователи, нажавшие Always Approve, переключают этот ключ в false. TUI также учитывает этот параметр для своего модального окна /clear, /new и /reset; HERMES_TUI_NO_CONFIRM=1 принудительно пропускает это модальное окно независимо от настройки.
РежимПоведение
smart (по умолчанию)Использует вспомогательную LLM для оценки риска. Низкорисковые команды (например, python -c "print('hello')") автоматически подтверждаются только для этого конкретного вызова. По-настоящему опасные команды автоматически отклоняются. Неоднозначные случаи эскалируются к ручному запросу.
manualВсегда запрашивает подтверждение пользователя для опасных команд.
offОтключает все проверки подтверждения — эквивалентно запуску с флагом --yolo. Все команды выполняются без запросов.
внимание

Установка approvals.mode: off отключает все запросы безопасности. Используйте только в доверенных окружениях (CI/CD, контейнеры и т. п.).

Режим YOLO

Режим YOLO обходит все запросы подтверждения опасных команд для текущей сессии. Его можно активировать тремя способами:

  1. Флаг CLI: запустите сессию с hermes --yolo или hermes chat --yolo
  2. Слэш-команда: введите /yolo во время сессии, чтобы включить/выключить режим
  3. Переменная окружения: установите HERMES_YOLO_MODE=1

Команда /yolo работает как переключатель — каждое использование меняет режим на противоположный:

> /yolo
  ⚡ YOLO mode ON — all commands auto-approved. Use with caution.

> /yolo
  ⚠ YOLO mode OFF — dangerous commands will require approval.

Режим YOLO доступен как в CLI-сессиях, так и в сессиях шлюза. Команда /yolo действует только на ту сессию, в которой её набрали. Режим сохраняется вместе с сессией и переживает перезапуск: чат на платформе обмена сообщениями держит его через перезапуски шлюза, а TUI и приложение Desktop — при повторном открытии или возобновлении сессии. На платформе обмена сообщениями /new и /resume начинают следующий разговор уже с включённым подтверждением.

Когда YOLO активен, Hermes показывает два постоянных визуальных напоминания, чтобы было сложно забыть, что запросы подтверждения обойдены:

  • Красная строка-баннер в начале сессии, если YOLO уже активен: ⚠ YOLO mode — all approval prompts bypassed. Скрывается, когда YOLO выключен, чтобы не загромождать стандартный баннер.
  • Фрагмент ⚠ YOLO в строке статуса на всех уровнях ширины, обновляется в реальном времени при переключении YOLO (как в текстовом рендерере с форматированием, так и в его текстовом резервном варианте).
опасно

Режим YOLO отключает все проверки безопасности опасных команд для сессии — кроме жёсткого блок-листа (см. ниже). Используйте только тогда, когда вы полностью доверяете генерируемым командам (например, хорошо протестированные скрипты автоматизации в одноразовых окружениях).

Для деструктивных слэш-команд сессии (/clear, /new / /reset, /undo, /quit --delete — /exit --delete является алиасом) CLI также запрашивает подтверждение перед выполнением. См. Слэш-команды — запросы подтверждения для деструктивных команд.

Ограничение на жизненный цикл шлюза под супервизором

У инструмента терминала есть отдельный, неотключаемый предохранитель против остановки и перезапуска шлюза изнутри его же процесса под супервизором. Самоперезапуск способен убить инструмент до того, как он закончит, и загнать всё в петлю «супервизор — автоподъём». Ни одобрение пользователем, ни режим YOLO, ни force=True этот предохранитель не обходят.

Предохранитель также отказывает командам, убивающим процессы по образу интерпретатора, под которым работает шлюз, — taskkill /F /IM python.exe, taskkill /FI "IMAGENAME eq python.exe", Stop-Process -Name python, pkill -9 python3, killall python, pkill -f python, а также убийствам по имени вроде pgrep python | xargs kill: шлюз под супервизором — это буквально процесс python, и такая команда кладёт его (вместе с текущим ходом самого агента). Проходят команды, нацеленные на процесс, которым владеет агент: id proc_* фоновой задачи (process(action="kill", …)) или явный PID (taskkill /F /PID <pid>, kill <pid>). Другие имена образов (taskkill /F /IM notepad.exe) не затрагиваются. Предохранитель действует под каждым сгенерированным лаунчером — юнитом systemd, plist launchd, run-скриптом s6 и Windows Scheduled Task — через экспортируемый ими маркер HERMES_SUPERVISED_CHILD.

На macOS выполнение команд launchctl submit и launchctl bootstrap ограничено независимо от метки задачи. Это осторожное ограничение на регистрацию — оно рассчитано поймать косвенных помощников перезапуска с нейтральными метками, а не проверить целевой plist. Отклоняются и независимые задачи по расписанию с RunAtLoad=false и без ключа KeepAlive; при этом отказ не доказывает, что задача пользуется KeepAlive или управляет Hermes.

Для разрешённого обслуживания LaunchAgent берите отдельную оболочку вне работающего шлюза. Часть независимых команд load и unload сейчас проходит проверки по метке, но это не исключение, подтверждённое по целевому объекту, и не поддерживаемый способ обойти отказ на bootstrap. Чтение через launchctl print операцией жизненного цикла не считается. После внешнего обслуживания различайте plist на диске и загруженную задачу: проверьте plist и перечитайте загруженное расписание, прежде чем докладывать об активации.

Отказ инструмента означает, что через этот вызов команда не выполнилась. Отказ ассистента такой вызов сделать — отдельное решение модели; смена модели политику предохранителя в терминале не меняет.

Жёсткий блок-лист (неотключаемый минимум)

Некоторые команды настолько катастрофичны — необратимая очистка файловой системы, форк-бомбы, прямая запись в блочные устройства, — что Hermes отказывается их выполнять независимо от:

  • включённого --yolo / /yolo
  • approvals.mode: off
  • cron-задач, работающих в фоновом режиме approve
  • явного нажатия пользователем «разрешить всегда»

Блок-лист — это минимальный уровень защиты, который стоит даже ниже --yolo. Он срабатывает до того, как команду вообще увидит слой подтверждения, и обойти его флагом нельзя. Актуально покрываемые шаблоны (список не исчерпывающий; синхронизирован с tools/approval.py::UNRECOVERABLE_BLOCKLIST):

ШаблонПочему это жёсткий блок
rm -rf / и очевидные вариацииСтирает корень файловой системы
rm -rf --no-preserve-root /Явный вариант «да, я имею в виду именно корень»
:(){ :|:& };: (форк-бомба bash)Загружает хост до перезагрузки
mkfs.* на смонтированном корневом устройствеФорматирует работающую систему
dd if=/dev/zero of=/dev/sd*Обнуляет физический диск
Передача недоверенных URL через пайп в sh на уровне корня файловой системыСлишком широкий вектор атаки удалённого выполнения кода, чтобы его можно было подтвердить

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

Нижний уровень защиты закрывается и на команде, чьи shell-кавычки не удаётся разобрать (grep 'unterminated): ошибка гласит malformed executable payload. Кавычки оцениваются по команде ровно в том виде, как она написана, поэтому допустимые для shell экранирования внутри шаблона в кавычках (grep -o "[^\"]*" file) повреждёнными не считаются, а экранированная кавычка перед разделителем (echo "a\"b"; reboot) не прячет команду, которая идёт следом.

Пользовательские правила запрета (approvals.deny)

Жёсткий блок-лист фиксирован и зашит в код. approvals.deny — его редактируемый пользователем аналог: список glob-шаблонов, которые безусловно блокируют совпадающие терминальные команды — до того, как будут учтены --yolo, /yolo и approvals.mode: off. Используйте его, чтобы получить «yolo с исключениями»: «пусть агент делает всё, кроме вот этих конкретных вещей — никогда».

approvals:
  deny:
    - "git push --force*"
    - "*curl*|*sh*"
    - "dd if=* of=/dev/*"

Детали:

  • Шаблоны — это глобы fnmatch (*, ?, [...]), сопоставляемые без учёта регистра и со всем текстом команды, и с отдельными кандидатами на исполняемую команду. git push --force* совпадёт с git push --force origin main, но не с git push origin main.
  • Сопоставление выполняется по тем же нормализованным/деобфусцированным вариантам команды, которые использует детектор опасных шаблонов, поэтому простые трюки с кавычками (git pu""sh --force) не проскочат мимо правила.
  • Кандидаты на исполняемое сохраняют буквальный путь и совпадают ещё и по базовому имени: sudo * покрывает и /usr/bin/sudo -n id, и ./sudo -n id. А вот правило с конкретным путём вроде /usr/bin/sudo * правилом для любого файла с именем sudo не становится.
  • Разбор с учётом кавычек вытаскивает команды после присваиваний, ведущих перенаправлений, ;, &&, ||, конвейеров, групп, подстановок команд и обычных переходов if/then/else/do. Из пускателей поддерживаются sudo, env, command, exec, nohup, setsid, time, nice, timeout, stdbuf, ionice, chrt, taskset и chroot. Известные операнды опций пропускаются; поиск через command -v и -V выполнением не считается. Полезная нагрузка shell -c разбирается рекурсивно. Буквальные строки «исполняемое плюс аргументы» в env -S и --split-string разбираются по правилам кавычек и экранирования GNU (включая \_ как границу слова и \c как завершение), а остальные аргументы команды дописываются в конец; знаки препинания оболочки внутри этих аргументов остаются данными, пока их не заберёт настоящий shell -c. Значения env -a и --argv0 — это аргументы, а не имена исполняемых. Комментарии оболочки и комментарии в GNU split-string кандидатов на исполняемое не создают.
  • В дополнительных кандидатах на исполняемое пробелы между словами сжимаются, а содержимое аргументов в кавычках и пути в аргументах сохраняются. Поэтому точное правило вроде git status совпадёт и с env git\tstatus; echo done (где \t — табуляция). А вот упоминания в кавычках, скажем echo 'sudo -n id', командами не считаются. Прежние глобы по всему вводу — например, *sudo* — по-прежнему намеренно совпадают с упоминанием где угодно.
  • Кавычки в YAML: всегда заключайте шаблоны в кавычки. Ведущая одиночная * без кавычек — это YAML-алиас, и парсинг завершится ошибкой; символы {, ! и : тоже имеют собственное значение в YAML. Одинарные кавычки безопаснее всего для shell-подобного содержимого.
  • Пользовательские правила запрета применяются ко всем терминальным бэкендам, включая изолированные контейнеры, и срабатывают раньше любых сокращённых путей одобрения, специфичных для бэкенда.
  • Заблокированная команда возвращает агенту ошибку BLOCKED с указанием не повторять попытку и не переформулировать запрос. Ничего не выполняется.

Как и остальная конфигурация подтверждений, изменения вступают в силу немедленно (кэш конфигурации привязан к времени изменения файла) — перезапуск сессии не требуется.

заметка
Модель угроз

Правила запрета — это политика по shell-командам, а не полноценный интерпретатор оболочки и не песочница возможностей на уровне ОС. Нормализация не раскрывает произвольные переменные (включая раскрытие ${NAME} в GNU env -S), псевдонимы, функции, переименованные бинарники, скрипты, программы-интерпретаторы и не разбирает любую грамматику оболочек и пускателей — например, синтаксис шаблонов case, склеенные опции пускателей или опции внутри строки env -S. Не считайте правило запрета по базовому имени гарантией, что до возможности не доберутся другим путём. Для настоящего ограничения берите права ОС и изолированный бэкенд с подобающе урезанными монтированиями, учётными данными и доступом в сеть. На настроенный режим подтверждений и на пустой список запретов по умолчанию это поведение сопоставления не влияет.

Таймаут подтверждения

Когда появляется запрос на опасную команду, у пользователя есть настраиваемое время на ответ. Если ответ не получен в течение таймаута, команда по умолчанию отклоняется (fail-closed).

Истёкший запрос заново открыть нельзя: ожидающая запись отбрасывается, а агенту сообщают, что в пределах этого хода повторять попытку самостоятельно не нужно. Если операцию всё-таки нужно выполнить, отправьте новое сообщение с такой просьбой (например, «go ahead and run that now») — агент сделает новый вызов инструмента, тот поднимет новую карточку подтверждения, и одобрение «once» будет относиться только к этому вызову. Таймаут не считается отказом, так что повторная просьба ничем не наказывается.

Настройте таймаут в ~/.hermes/config.yaml:

approvals:
  timeout: 300  # секунд (по умолчанию: 300)

Что вызывает запрос подтверждения

Следующие шаблоны вызывают запросы подтверждения (определены в tools/approval.py):

ШаблонОписание
rm -r / rm --recursiveРекурсивное удаление
rm ... /Удаление в корневом пути
chmod 777/666 / o+w / a+wПрава доступа, открытые на запись всем/остальным
chmod --recursive с небезопасными правамиРекурсивное открытие прав на запись всем/остальным (длинный флаг)
chown -R root / chown --recursive rootРекурсивная смена владельца на root
mkfsФорматирование файловой системы
dd if=Побайтовое копирование диска
> /dev/sdЗапись в блочное устройство
DROP TABLE/DATABASESQL DROP
DELETE FROM (без WHERE)SQL DELETE без условия WHERE
TRUNCATE TABLESQL TRUNCATE
> /etc/Перезапись системной конфигурации
systemctl stop/restart/disable/maskОстановка/перезапуск/отключение системных служб
kill -9 -1Завершение всех процессов
pkill -9Принудительное завершение процессов
Шаблоны форк-бомбФорк-бомбы
bash -c / sh -c / zsh -c / ksh -cВыполнение shell-команды через флаг -c (включая комбинированные флаги вроде -lc)
python -e / perl -e / ruby -e / node -cВыполнение скрипта через флаг -e/-c
curl ... | sh / wget ... | shПередача удалённого содержимого в shell через пайп
bash <(curl ...) / sh <(wget ...)Выполнение удалённого скрипта через подстановку процесса
tee в /etc/, ~/.ssh/, ~/.hermes/.envПерезапись чувствительного файла через tee
> / >> в /etc/, ~/.ssh/, ~/.hermes/.envПерезапись чувствительного файла через редирект
xargs rmxargs с rm
find -exec rm / find -deleteFind с деструктивными действиями
cp/mv/install в /etc/Копирование/перемещение файла в системную конфигурацию
sed -i / sed --in-place в /etc/Правка системной конфигурации «на месте»
pkill/killall hermes/gatewayЗащита от самозавершения
gateway run с &/disown/nohup/setsidПредотвращает запуск шлюза в обход менеджера служб
docker stop/kill/restart, docker compose down/stop/kill/restartЖизненный цикл контейнеров (ловятся и глобальные флаги, и docker-compose)
docker -H/--host/--context, DOCKER_HOST=/DOCKER_CONTEXT=Перенаправление демона Docker — команда уходит на другой, часто удалённый демон
docker context useПереключает демон по умолчанию для всех последующих команд docker
podman --remote/-r/--url/--connection/--identity, CONTAINER_HOST=Перенаправление на удалённый демон Podman
инфо

Обход в контейнере: при работе в бэкендах docker, singularity, modal, daytona или vercel_sandbox проверки опасных команд пропускаются, потому что сам контейнер служит рубежом безопасности. Деструктивные команды внутри контейнера не могут навредить хосту.

Процесс подтверждения (CLI)

В интерактивном CLI опасные команды показывают встроенный запрос подтверждения:

  ⚠️  DANGEROUS COMMAND: recursive delete
      rm -rf ~/old-project

      [o]nce  |  [s]ession  |  [a]lways  |  [d]eny

      Choice [o/s/a/D]:

Четыре варианта:

  • once — разрешить только это единственное выполнение
  • session — разрешить этот шаблон до конца сессии
  • always — добавить в постоянный allowlist (сохраняется в config.yaml)
  • deny (по умолчанию) — заблокировать команду

Процесс подтверждения (шлюз/мессенджеры)

В мессенджерах агент отправляет детали опасной команды в чат и ждёт ответа пользователя:

  • Ответ yes, y, approve, ok или go подтверждает выполнение
  • Ответ no, n, deny или cancel отклоняет команду

Переменная окружения HERMES_EXEC_ASK=1 автоматически устанавливается при запуске шлюза.

Постоянный allowlist

Команды, подтверждённые с опцией «always», сохраняются в ~/.hermes/config.yaml:

# Permanently allowed dangerous command patterns
command_allowlist:
  - rm
  - systemctl

Эти шаблоны загружаются при запуске и молча подтверждаются во всех будущих сессиях.

Записью может быть точный текст команды, shell-глоб (podman *) или ключ правила опасного шаблона вроде script execution via heredoc (тот, что показывается в запросе подтверждения). Ключи правил действуют на всех поверхностях, включая автономные: задача cron, запуск hermes chat -q или сессия вебхука под cron_mode/single_query_mode/unattended_mode: deny всё равно выполнит команду, чей распознанный ключ правила есть в command_allowlist.

Настройка обязана быть списком строк. Старые установки, где список хранился строкой YAML или JSON в кавычках, при загрузке этот список восстанавливают и пишут предупреждение — перезапишите его через hermes config edit. Прочие испорченные значения игнорируются с предупреждением и посимвольными одобрениями не становятся никогда. Сама загрузка ваш файл конфигурации не переписывает.

совет

Используйте hermes config edit, чтобы просмотреть или удалить шаблоны из вашего постоянного allowlist.

осторожно

Список читается при старте Hermes. Шаблон, который вы удалили, пока сессия уже идёт, остаётся одобренным в этой сессии до тех пор, пока она в следующий раз не запишет файл (когда вы снова ответите always на запрос) или пока вы не перезапустите Hermes. Если вы удалили его из соображений безопасности — перезапустите.

Разбор истории подтверждений (hermes approvals suggest)

Вместо того чтобы отвечать на один и тот же запрос из сессии в сессию, прошлые решения о подтверждении можно превратить в предложения для allowlist:

hermes approvals suggest            # dry run — prints a numbered proposal
hermes approvals suggest --apply 1,3  # merge picks into command_allowlist
hermes approvals suggest --json     # machine-readable output

Команда просматривает базу сессий (~/.hermes/state.db) в поисках команд, классифицированных как опасные и всё-таки выполненных — то есть подтверждённых вами, — сводит их в шаблоны (git push * либо ключ опасного класса для составных команд) и ранжирует по частоте подтверждений:

Proposed command_allowlist additions (from approval history, last 90 days):

  1. git push *    — approved 14x
  2. docker restart/stop/kill (container lifecycle)    — approved 9x (class key)

Правила безопасности:

  • Ничего не применяется само — обычный запуск работает только на чтение; в config.yaml пишет лишь явный --apply N[,M...].
  • Деструктивные классы не предлагаются никогда, сколько бы раз их ни подтверждали: рекурсивные удаления, sudo, запись на диски и устройства, правка учётных данных и системных конфигов, конвейер в shell, SQL DROP/TRUNCATE, убийство процессов и все жёстко заблокированные классы исключены полностью. rm -rf build/, подтверждённый сто раз, всё равно не породит запись про rm.
  • Предложения, уже покрытые вашим command_allowlist, пропускаются.
  • Учётные данные внутри разобранных команд маскируются (ghp_…, URL вида bot<id>:<token>, присваивания KEY=value, bearer-токены) — и в печатаемых примерах e.g., и в выводе --json, тем же редактором, что и в терминальном выводе. Замаскированный текст никогда не становится шаблоном allowlist: команда, чей глоб включал бы учётные данные (TOKEN=… git …), предлагается под ключом своего опасного класса. Сама база сессий по-прежнему хранит команду в том виде, в каком она выполнялась.

Полезные флаги: --days N (окно истории, по умолчанию 90), --min-count N (минимум подтверждений для попадания в список, по умолчанию 2), --limit N и --db PATH.

Безопасность записи файлов

Прежде чем write_file или patch доберутся до диска, Hermes сверяет целевой путь с запретным списком и с опциональной песочницей. Заблокированная запись сразу возвращает агенту ошибку — запроса подтверждения нет, и обойти его из интерфейса чата нельзя. Модель при этом может заявить, что правка удалась; когда display.file_mutation_verifier включён (по умолчанию), верьте подписи верификатора изменений файлов, а не итоговой фразе ассистента.

Защищённые пути (блокируются всегда)

Эти категории запрещены всегда, даже когда HERMES_WRITE_SAFE_ROOT не задан:

КатегорияПримеры
Хранилища учётных данных ОС~/.ssh/ (ключи, authorized_keys), ~/.aws/, ~/.kube/, /etc/sudoers, ~/.netrc
Хранилища секретов Hermes.env, .anthropic_oauth.json, auth/google_oauth.json, кэш Bitwarden (cache/bws_cache.json, cache/bws_cache.enc.json), vault/, browser-profile/, mcp-tokens/, pairing/ внутри HERMES_HOME (активный профиль и глобальный корень). Управляющие файлы (auth.json, config.yaml, webhook_subscriptions.json) запрещены для чтения, но остаются доступными для записи.
Пути NT/пространства устройств Windows\??\..., \\.\..., \\?\UNC\..., \\?\GLOBALROOT... — отклоняются и на чтение, и на запись на любой платформе. В Windows одно лишь разрешение такого пути (например, \??\UNC\host\share) запускает исходящую SMB-аутентификацию и может утечь NTLM-хеш пользователя; к тому же эти префиксы обходят обычную нормализацию путей. Обычные локальные пути расширенной длины (\\?\C:\...) и простые UNC-шары (\\server\share) не затрагиваются.

Файлы .env, .env.local, .env.production и .envrc внутри проектов запрещены для чтения в любом месте диска (файловые инструменты отказываются их читать), но остаются доступными для записи: агент может создать или отредактировать их для вас — просто не может прочитать значения обратно.

Чувствительные пути внутри безопасного корня всё равно блокируются: направив HERMES_WRITE_SAFE_ROOT на $HOME, вы не разрешите запись в ~/.ssh/id_rsa.

~ в строках про учётные данные ОС означает любой домашний каталог, куда может попасть запись, а не только HOME процесса: настоящий домашний каталог пользователя ОС, домашний каталог профиля ({HERMES_HOME}/home при TERMINAL_HOME_MODE=profile, в контейнерах и порождённых воркерах, где HOME процесса закреплён) и именованные учётные записи (~root/.ssh/authorized_keys). Абсолютный путь к ~/.aws/credentials в настоящем домашнем каталоге запрещён, даже если процесс агента работает с HOME, указывающим в другое место.

Нарушение безопасного корня возвращает Write denied: '…' is outside HERMES_WRITE_SAFE_ROOT (…). Блокировка пути с учётными данными — Write denied: '…' is a protected system/credential file.

Исключение — ~/.ssh/config подтверждается, а не блокируется намертво. Клиентский конфиг SSH не содержит материала приватных ключей, а его правка (алиасы хостов, ProxyJump, цели VS Code Remote-SSH) — рутинная задача, поэтому write_file / patch направляют такую запись через тот же диалог approve-once/session/always, что terminal-инструмент уже использует для записей в ~/.ssh — вместо прежнего безусловного отказа. Файл всё ещё может содержать директивы ProxyCommand / Match exec, запускающие команды, поэтому запись никогда не проходит молча. Неинтерактивные вызовы (мост ACP к файловой системе, фоновые задачи без канала связи с человеком) отклоняются по умолчанию. Приватные ключи, authorized_keys и всё остальное внутри ~/.ssh/ остаются заблокированы намертво.

HERMES_WRITE_SAFE_ROOT (опциональная песочница)

Когда переменная задана, write_file и patch могут писать только внутрь перечисленных каталогов-префиксов. Всё за их пределами жёстко блокируется — через подтверждение опасных команд это не проходит.

  • Выставляется автоматически в официальном Docker-образе (HERMES_WRITE_SAFE_ROOT=/opt/data)
  • Поддерживает несколько корней через : в Unix и ; в Windows
  • Не добавляйте её в ~/.hermes/.env бездумно. Направив её на каталог проекта, вы лишаете агента возможности писать в ~/.hermes/cron/jobs.json, навыки профиля и прочее состояние Hermes за пределами этого префикса

Чтобы разрешить и рабочий каталог, и домашний каталог Hermes:

export HERMES_WRITE_SAFE_ROOT=/path/to/project:/home/you/.hermes

Уберите переменную, чтобы вернуть неограниченные записи (запретный список защищённых путей продолжает действовать). Полный справочник — HERMES_WRITE_SAFE_ROOT.

Cron и прочее состояние Hermes

Не просите агента напрямую делать patch над ~/.hermes/cron/jobs.json. Используйте инструмент cronjob_manage, hermes cron или /cron — они обновляют хранилище задач через поддерживаемый API. То же касается других управляющих файлов Hermes, когда защита записи блокирует правку напрямую.

заметка
Эшелонированная защита, а не жёсткая граница

Защита записи распространяется только на write_file и patch — с одним исключением: строка про пути NT/пространства устройств Windows действует и на чтение: read_file, search_files, контекстные ссылки @file:/@folder: и файловый мост ACP отклоняют такие пути по сырой строке, ещё до какого-либо разрешения. Инструмент terminal работает от того же пользователя ОС и по-прежнему способен прочитать или перезаписать запрещённые пути shell-командами. Запретный список снижает вероятность случайного ущерба и даёт модели внятный стоп-сигнал; враждебного или скомпрометированного агента он в песочницу не сажает.

Авторизация пользователей (шлюз)

При работе шлюза для мессенджеров Hermes контролирует, кто может взаимодействовать с ботом, через многоуровневую систему авторизации.

Порядок проверки авторизации

Метод _is_user_authorized() проверяет условия в следующем порядке:

  1. Флаг «разрешить всем» для конкретной платформы (например, DISCORD_ALLOW_ALL_USERS=true)
  2. Список подтверждённых через парный доступ пользователей (пользователи, подтверждённые кодами парного доступа)
  3. Allowlist для конкретной платформы (например, TELEGRAM_ALLOWED_USERS=12345,67890)
  4. Глобальный allowlist (GATEWAY_ALLOWED_USERS=12345,67890)
  5. Глобальное «разрешить всем» (GATEWAY_ALLOW_ALL_USERS=true)
  6. По умолчанию: отказ

Allowlist по платформам

Задайте разрешённые ID пользователей как значения через запятую в ~/.hermes/.env:

# Platform-specific allowlists
TELEGRAM_ALLOWED_USERS=123456789,987654321
DISCORD_ALLOWED_USERS=111222333444555666
WHATSAPP_ALLOWED_USERS=15551234567
SLACK_ALLOWED_USERS=U01ABC123

# Cross-platform allowlist (checked for all platforms)
GATEWAY_ALLOWED_USERS=123456789

# Per-platform allow-all (use with caution)
DISCORD_ALLOW_ALL_USERS=true

# Global allow-all (use with extreme caution)
GATEWAY_ALLOW_ALL_USERS=true

Глобальное «разрешить всем» можно задать и в config.yaml как gateway.allow_all_users: true (или allow_all_users: true на верхнем уровне); истинное значение прокидывается в GATEWAY_ALLOW_ALL_USERS при старте шлюза (и заново выводится при каждой загрузке конфига и перезапуске, так что возврат в false закрывает доступ), явная переменная окружения побеждает, а шлюз пишет в лог предупреждение с указанием config.yaml как источника разрешения. В многопрофильном шлюзе вторичный профиль задаёт GATEWAY_ALLOW_ALL_USERS в собственном .env (его config.yaml в окружение процесса никогда не прокидывается).

внимание

Если allowlist не настроены и GATEWAY_ALLOW_ALL_USERS не задан, всем пользователям отказывается в доступе. При запуске шлюз выводит предупреждение в лог:

No user allowlists configured. All unauthorized users will be denied.
Set GATEWAY_ALLOW_ALL_USERS=true in ~/.hermes/.env to allow open access,
or configure platform allowlists (e.g., TELEGRAM_ALLOWED_USERS=your_id).

Система парного доступа через DM

Для более гибкой авторизации в Hermes встроена система парного доступа по коду. Вместо того чтобы требовать ID пользователей заранее, незнакомые пользователи получают одноразовый код парного доступа, который владелец бота подтверждает через CLI.

Как это работает:

  1. Незнакомый пользователь отправляет боту личное сообщение
  2. Бот отвечает 8-символьным кодом парного доступа
  3. Владелец бота выполняет hermes pairing approve <platform> <code> в CLI
  4. Пользователь получает постоянное подтверждение для этой платформы

Управляйте тем, как обрабатываются неавторизованные личные сообщения, в ~/.hermes/config.yaml:

unauthorized_dm_behavior: pair

whatsapp:
  unauthorized_dm_behavior: ignore
  • pair — значение по умолчанию для платформ с личными сообщениями в стиле чата. Неавторизованные DM получают ответ с кодом парного доступа.
  • ignore молча отбрасывает неавторизованные DM.
  • decline отправляет один короткий вежливый отказ («I can only chat with my owner») вместо кода парного доступа, а затем игнорирует дальнейшие сообщения этого отправителя в течение 24 часов. Текст настраивается через unauthorized_dm_decline_message.
  • Для email по умолчанию установлено ignore, если явно не задано platforms.email.unauthorized_dm_behavior: pair, потому что во входящих может быть не связанная с этим непрочитанная почта.
  • Секции для конкретной платформы переопределяют глобальное значение по умолчанию, так что можно оставить парный доступ включённым в Telegram, отключив его в WhatsApp.

Функции безопасности (на основе рекомендаций OWASP и NIST SP 800-63-4):

ФункцияДетали
Формат кода8 символов из 32-символьного алфавита без неоднозначных символов (без 0/O/1/I)
СлучайностьКриптографическая (secrets.choice())
Срок действия кодаИстекает через 1 час
Ограничение частоты запросов1 запрос на пользователя за 10 минут
Лимит ожидающих кодовМаксимум 3 ожидающих кода на платформу
Блокировка5 неудачных попыток подтверждения → блокировка на 1 час
Безопасность файловchmod 0600 на все файлы данных парного доступа
ЛогированиеКоды никогда не пишутся в stdout

CLI-команды для парного доступа:

# List pending and approved users
hermes pairing list

# Approve a pairing code
hermes pairing approve telegram ABC12DEF

# Revoke a user's access
hermes pairing revoke telegram 123456789

# Clear all pending codes
hermes pairing clear-pending
совет
Docker: выполняйте команды парного доступа от пользователя

hermes Официальный Docker-образ запускает шлюз от непривилегированного пользователя hermes (uid 10000) через gosu, но docker exec по умолчанию использует root. Файлы подтверждения, созданные от root, записываются с правами 0600 root:root, и шлюз не может их прочитать — подтверждение молча игнорируется (#10270).

Всегда передавайте -u hermes:

docker exec -u hermes hermes-agent hermes pairing approve telegram ABC12DEF

Если вы уже выполнили команду от root и пользователь всё ещё не авторизован, перезапустите контейнер — точка входа исправит владельца файлов при следующем старте.

Хранение: данные парного доступа хранятся в ~/.hermes/pairing/ в отдельных JSON-файлах для каждой платформы:

  • {platform}-pending.json — ожидающие запросы парного доступа
  • {platform}-approved.json — подтверждённые пользователи
  • _rate_limits.json — отслеживание ограничения частоты запросов и блокировок

Изоляция контейнеров

При использовании терминального бэкенда docker Hermes применяет строгое усиление безопасности к каждому контейнеру.

Флаги безопасности Docker

Каждый контейнер запускается с этими флагами (определены в tools/environments/docker.py):

_BASE_SECURITY_ARGS = [
    "--cap-drop", "ALL",                          # Drop ALL Linux capabilities
    "--cap-add", "DAC_OVERRIDE",                  # Root can write to bind-mounted dirs
    "--cap-add", "CHOWN",                         # Package managers need file ownership
    "--cap-add", "FOWNER",                        # Package managers need file ownership
    "--security-opt", "no-new-privileges",         # Block privilege escalation
    "--pids-limit", "256",                         # Limit process count
    # no-tmp: ok — configures the sandbox's own tmpfs
    "--tmpfs", "/tmp:rw,nosuid,size=512m",         # Size-limited /tmp
    "--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",  # No-exec /var/tmp
]

SETUID/SETGID не входят в базовый список — они добавляются условно, когда контейнер запускается от root и init/entrypoint должен сбросить привилегии (путь сброса привилегий через s6). Они пропускаются, если контейнер уже работает от непривилегированного --user. Точка монтирования tmpfs для /run также вынесена из базового списка и монтируется отдельно для каждого образа (по умолчанию усилена флагом noexec, флаг exec используется только для образов на s6-overlay, которые выполняют файлы из /run).

Ограничения ресурсов

Ресурсы контейнера настраиваются в ~/.hermes/config.yaml:

terminal:
  backend: docker
  docker_image: "nousresearch/hermes-sandbox:desktop"
  docker_forward_env: []  # Explicit allowlist only; empty keeps secrets out of the container
  container_cpu: 1        # CPU cores
  container_memory: 5120  # MB (default 5GB)
  container_disk: 51200   # MB (default 50GB, requires overlay2 on XFS)
  container_persistent: true  # Persist filesystem across sessions

Устойчивость файловой системы

  • Постоянный режим (container_persistent: true): монтирует /workspace и /root из ~/.hermes/sandboxes/docker/<task_id>/
  • Эфемерный режим (container_persistent: false): использует tmpfs для рабочего пространства — всё теряется при очистке
совет

Для продакшен-развёртываний шлюза используйте бэкенд docker, modal, daytona или vercel_sandbox, чтобы изолировать команды агента от вашей хост-системы. Это полностью снимает необходимость в подтверждении опасных команд.

внимание

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

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

БэкендИзоляцияПроверка опасных командЛучше всего подходит для
localНет — выполняется на хосте✅ ДаРазработка, доверенные пользователи
sshУдалённая машина✅ ДаРабота на отдельном сервере
dockerКонтейнер❌ Пропускается (контейнер сам служит рубежом)Продакшен-шлюз
singularityКонтейнер❌ ПропускаетсяHPC-окружения
modalОблачная песочница❌ ПропускаетсяМасштабируемая облачная изоляция
daytonaОблачная песочница❌ ПропускаетсяПостоянные облачные рабочие пространства
vercel_sandboxОблачная микро-ВМ❌ ПропускаетсяОблачное выполнение с сохранением снимков

Проброс переменных окружения

И execute_code, и terminal убирают чувствительные переменные окружения из дочерних процессов, чтобы предотвратить утечку учётных данных через сгенерированный LLM код. Однако навыкам, которые декларируют required_environment_variables, легитимно нужен доступ к этим переменным.

Собственные учётные данные платформы — переменные BUZZ_*, которые использует мессенджер-платформа Buzz, — пробрасываются в дочерние процессы terminal (и на переднем плане, и при фоновом/PTY-запуске), но только когда сессия действительно работает как агент Buzz: процесс запущен как управляемый Buzz-ACP агент (BUZZ_MANAGED_AGENT устанавливает Buzz Desktop) либо платформа текущей сессии шлюза — buzz. Это позволяет агенту платформы Buzz вызывать из инструмента terminal свой обязательный CLI (например, buzz), а сессии Telegram/CLI/cron на том же хосте по-прежнему лишены этих переменных. Поскольку _sanitize_subprocess_env обслуживает ещё и рабочие процессы поиска (например, подпроцесс веб-поиска ddgs), бинарник драйвера computer-use и исполнители пользовательских скриптов (команды с !, быстрые команды, cron-скрипты, скрипты фильтрации webhook), эти дочерние процессы тоже получают переменные, если их запустила сессия Buzz. Исключение действует только для terminal: на execute_code, запуски из браузера/TUI-хоста (hermes_subprocess_env), дочерние процессы Docker/Modal и регистрацию через env_passthrough оно не распространяется — там фильтрация продолжает действовать в полном объёме.

Как это работает

Два механизма позволяют определённым переменным пройти через фильтры песочницы:

1. Проброс, привязанный к навыку (автоматически)

Когда навык загружается (через skill_view или команду /skill) и декларирует required_environment_variables, все переменные из этого списка, которые фактически заданы в окружении, автоматически регистрируются для проброса. Отсутствующие переменные (всё ещё в состоянии «требуется настройка») не регистрируются.

# In a skill's SKILL.md frontmatter
required_environment_variables:
  - name: TENOR_API_KEY
    prompt: Tenor API key
    help: Get a key from https://developers.google.com/tenor

После загрузки этого навыка TENOR_API_KEY пробрасывается в execute_code, terminal (локально) и в удалённые бэкенды (Docker, Modal) — без ручной настройки.

инфо
Docker и Modal

До версии v0.5.1 forward_env в Docker была отдельной системой от проброса на основе навыков. Теперь они объединены — переменные окружения, декларированные навыками, автоматически пробрасываются в контейнеры Docker и песочницы Modal без необходимости вручную добавлять их в docker_forward_env.

2. Проброс на основе конфигурации (вручную)

Для переменных окружения, не декларированных ни одним навыком, добавьте их в terminal.env_passthrough в config.yaml:

terminal:
  env_passthrough:
    - MY_CUSTOM_KEY
    - ANOTHER_TOKEN

Оба списка одинаково действуют на terminal, execute_code и cron-скрипты no_agent. Объявленная переменная пробрасывается со значением того профиля, для которого работает дочерний процесс: когда один процесс обслуживает несколько профилей (мультипрофильный шлюз, бэкенд десктопа/дашборда), объявленное значение каждого профиля берётся из его собственных .env / источников секретов, а не из окружения процесса, которое заполнил профиль запуска, и учётные данные из .env профиля запуска из дочерних процессов обслуживаемого профиля убираются.

Проброс файлов учётных данных (OAuth-токены и т. п.)

Некоторым навыкам в песочнице нужны файлы (а не только переменные окружения) — например, Google Workspace хранит OAuth-токены в виде google_token.json в директории HERMES_HOME активного профиля. Навыки декларируют это во frontmatter:

required_credential_files:
  - path: google_token.json
    description: Google OAuth2 token (created by setup script)
  - path: google_client_secret.json
    description: Google OAuth2 client credentials

При загрузке Hermes проверяет, существуют ли эти файлы в HERMES_HOME активного профиля, и регистрирует их для монтирования:

  • Docker: монтирование только для чтения (-v host:container:ro)
  • Modal: монтируются при создании песочницы + синхронизируются перед каждой командой (для случаев, когда OAuth настраивается посреди сессии)
  • Local: действий не требуется (файлы уже доступны)

Файлы учётных данных также можно перечислить вручную в config.yaml:

terminal:
  credential_files:
    - google_token.json
    - my_custom_oauth_token.json

Пути указываются относительно ~/.hermes/. Файлы монтируются в /root/.hermes/ внутри контейнера. Этот список читается модулем tools/credential_files.py (terminal.credential_files) — он находится в блоке terminal:, но загружается модулем файлов учётных данных, а не базовым терминальным бэкендом, поэтому не входит в встроенный снапшот DEFAULT_CONFIG.

Заимствованные логины CLI (Codex CLI, Claude Code)

Когда у Hermes нет собственного рабочего логина для openai-codex или anthropic, он может позаимствовать ~/.codex/auth.json из Codex CLI и ~/.claude/.credentials.json из Claude Code (или запись в Keychain) и обновлять их от вашего имени. Оба используют одноразовые ротируемые refresh-токены: как только одним семейством токенов владеют две программы, та, что обновится первой, делает копию второй недействительной — выглядит это как «я один раз залогинился в терминале, а Hermes постоянно падает» (или наоборот). Если вы пользуетесь этими CLI параллельно с Hermes, дайте Hermes собственный логин и выключите заимствование:

auth:
  adopt_external_logins: false   # по умолчанию: true

С выключенным переключателем Hermes эти файлы никогда не читает и не обновляет: строка claude_code в пуле учётных данных исчезает, hermes auth list печатает об этом одну строку, а в логе появляется по одной строке INFO на процесс. Затрагивается только автоматическое заимствование — hermes auth add openai-codex по-прежнему спрашивает, прежде чем импортировать существующий логин Codex CLI. Автоматическое восстановление тоже чинит только те учётные данные, которые у Hermes уже есть: логин Codex CLI/Desktop в другой воркспейс ChatGPT отклоняется с предупреждением (переаутентифицируйтесь через hermes auth add openai-codex), а логин, который вы завершили, пока шло восстановление, никогда не перезаписывается. Собственные логины добавляйте через hermes auth add anthropic / hermes auth add openai-codex.

Что фильтрует каждая песочница

ПесочницаФильтр по умолчаниюПереопределение проброса
execute_codeБлокирует переменные, содержащие в имени KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, PASSWD, AUTH; пропускает только переменные с безопасным префиксом✅ Переменные из проброса обходят обе проверки
terminal (локально)Блокирует явно перечисленные инфраструктурные переменные Hermes (ключи провайдеров, токены шлюза, API-ключи инструментов)✅ Переменные из проброса обходят блок-лист
terminal (Docker)По умолчанию ни одна переменная окружения хоста не передаётся✅ Переменные из проброса + docker_forward_env передаются через -e
terminal (SSH)По умолчанию ни одна переменная окружения хоста не передаётся✅ Переменные из проброса передаются через SendEnv; на удалённой стороне нужен соответствующий AcceptEnv в sshd_config (см. Бэкенд SSH)
terminal (Modal)По умолчанию ни переменные окружения, ни файлы хоста не передаются✅ Файлы учётных данных монтируются; проброс переменных окружения через синхронизацию
MCPБлокирует всё, кроме безопасных системных переменных и явно настроенного env❌ Проброс не действует (используйте настройку env в MCP вместо этого)

Соображения безопасности

  • Проброс затрагивает только те переменные, которые явно декларировали вы или ваши навыки, — стандартная политика безопасности для произвольного сгенерированного LLM кода не меняется
  • Файлы учётных данных монтируются в контейнеры Docker только для чтения
  • Skills Guard сканирует содержимое навыка на подозрительные шаблоны доступа к переменным окружения перед установкой
  • Отсутствующие/незаданные переменные никогда не регистрируются (нельзя допустить утечку того, чего не существует)
  • Инфраструктурные секреты Hermes (API-ключи провайдеров, токены шлюза) никогда не следует добавлять в env_passthrough — для них есть отдельные механизмы. Такое имя отклоняется при объявлении, а объявленное имя, которое позже заберёт адаптер платформы (плагин-адаптер, зарегистрировавшийся после загрузки навыка), с этого момента перестаёт пробрасываться

Обработка учётных данных MCP

Подпроцессы серверов MCP (Model Context Protocol) получают отфильтрованное окружение, чтобы предотвратить случайную утечку учётных данных.

Безопасные переменные окружения

От хоста в stdio-подпроцессы MCP пробрасываются только следующие переменные:

PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR

Плюс любые переменные XDG_*. Все остальные переменные окружения (API-ключи, токены, секреты) вырезаются.

Переменные, явно указанные в настройке env сервера MCP, пробрасываются:

mcp_servers:
  github:
    command: "npx"
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."  # Only this is passed

Редактирование учётных данных

Сообщения об ошибках от инструментов MCP очищаются перед возвратом LLM. Следующие шаблоны заменяются на [REDACTED]:

  • GitHub PAT (ghp_...)
  • Ключи в стиле OpenAI (sk-...)
  • Bearer-токены
  • Параметры token=, key=, API_KEY=, password=, secret=

Политика доступа к сайтам

Вы можете ограничить, к каким сайтам агент может обращаться через веб- и браузерные инструменты. Это полезно, чтобы не дать агенту доступ к внутренним сервисам, административным панелям или другим чувствительным URL.

# In ~/.hermes/config.yaml
security:
  website_blocklist:
    enabled: true
    domains:
      - "*.internal.company.com"
      - "admin.example.com"
    shared_files:
      - "/etc/hermes/blocked-sites.txt"

Когда запрашивается заблокированный URL, инструмент возвращает ошибку с объяснением, что домен заблокирован политикой. Блок-лист применяется к web_search, web_extract, browser_navigate и всем инструментам, работающим с URL.

Полные детали см. в разделе Website Blocklist руководства по конфигурации.

Защита от SSRF

Все инструменты, работающие с URL (веб-поиск, извлечение веб-контента, зрение, браузер), проверяют URL перед запросом, чтобы предотвратить атаки Server-Side Request Forgery (SSRF). Блокируемые адреса включают:

  • Частные сети (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Loopback: 127.0.0.0/8, ::1
  • Link-local: 169.254.0.0/16 (включая метаданные облака по адресу 169.254.169.254)
  • CGNAT / общее адресное пространство (RFC 6598): 100.64.0.0/10 (Tailscale, WireGuard VPN)
  • Хостнеймы метаданных облака: metadata.google.internal, metadata.goog
  • Зарезервированные, multicast и неопределённые адреса

Защита от SSRF всегда активна для использования в интернете, а сбои DNS трактуются как блокировка (fail-closed). Цепочки редиректов повторно проверяются на каждом шаге, чтобы предотвратить обход через редиректы.

Та же защита покрывает загрузки, URL которых приходит от удалённой стороны, а не от вас: URL изображений и видео, которые возвращает провайдер генерации, URL референсных изображений, которые модель передаёт для правок, спрайтшиты питомцев и манифест petdex, а также записи sitemap skills.sh. Если провайдер или индекс указывает такой URL на приватный адрес или адрес метаданных, запрос отклоняется ещё до открытия соединения. Собственный base_url провайдера, заданный оператором, это не затрагивает: загрузка напрямую с вашего настроенного base_url (эндпоинт видеоконтента OpenRouter) пропускает на этом первом шаге только проверку класса приватных адресов, нижняя граница для облачных метаданных всё равно действует, а любой выданный им редирект проверяется заново полностью. Провайдеру генерации изображений, размещённому в вашей LAN, нужен security.allow_private_urls: true (см. ниже), чтобы возвращаемые им URL результатов кэшировались локально.

Намеренное разрешение приватных URL

Некоторым конфигурациям легитимно нужен доступ к приватным/внутренним URL — домашние сети, где home.arpa резолвится в адреса RFC 1918, LAN-эндпоинты Ollama/llama.cpp, внутренние вики, отладка метаданных облака и тому подобное. Для таких случаев есть глобальная опция отключения защиты:

security:
  allow_private_urls: true   # default: false

Когда опция включена, веб-инструменты, браузер, запросы URL через зрение и загрузка медиа шлюзом больше не отклоняют адреса из RFC 1918 / loopback / link-local / CGNAT / метаданных облака. Это намеренный рубеж доверия — включайте только на машинах, где допустим риск того, что агент обратится по произвольным URL, полученным через промпт-инъекцию, к локальной сети. Публично доступные шлюзы должны оставлять эту опцию выключенной.

Защита от подстановки похожих доменов (которая блокирует юникодные трюки с похожими доменами даже при публичном IP) остаётся включённой независимо от этой настройки.

Диапазоны fake-ip локального прокси

TUN-прокси в режиме fake-ip (fake-ip в Mihomo/Clash, enhanced mode в Surge) отвечает на DNS-запросы адресом из собственного блока — по умолчанию 198.18.0.0/15 (RFC 2544, диапазон для бенчмарков) — для каждого имени вне своего фильтра. Такие ответы — маркер прокси, а не внутренний хост, но защита от приватных IP на подобном хосте иначе отклоняет каждый исходящий запрос: web_extract, загрузка вложений с платформ и браузерный relay падают с URL targets a private or internal network address, а запрос до сети даже не доходит. Объявите блок, чтобы пропускать этот маркер:

security:
  fake_ip_ranges:
    - 198.18.0.0/15

По умолчанию список пуст, и он уже, чем allow_private_urls: исключение получают только объявленные блоки, и это должны быть диапазоны, принадлежащие локальному прокси (соединение всё равно идёт к прокси, который сам разрешает настоящую цель), а loopback, RFC 1918, link-local, CGNAT и адреса метаданных облака остаются заблокированными — запись, пересекающаяся с одним из этих классов (включая 0.0.0.0/0 или ::/0), игнорируется с предупреждением, а не расширяет защиту. На хосте с облачным провайдером браузера объявленный маркер перестаёт считаться приватным и для browser.auto_local_for_private_urls, так что такие страницы по-прежнему уходят в облачный браузер.

Контентные проверки команд

Детектор опасных команд помечает ещё две контентные формы, которых не видит список деструктивных глаголов:

  • Тело запроса curl/wget, несущее секрет: переменная с именем секрета (-d "k=$OPENAI_API_KEY") или файл с учётными данными (-F file=@.env, -T ~/.ssh/id_rsa, --post-file=/etc/passwd), а также файл с учётными данными, поданный через конвейер в curl/wget, который что-то выгружает. Заголовок Authorization — обычное использование API, и его детектор не помечает.
  • Невидимые и двунаправленные управляющие символы Unicode (пробелы нулевой ширины, переопределения направления справа налево, изоляты): из-за них команда, которую вы подтверждаете, отличается от той, что выполняется. Последовательности эмодзи с соединителями не помечаются.

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

Прежние выпуски включали сюда внешний сканер tirith. Его убрали; при обновлении настройки security.tirith_* отбрасываются, а взамен ничего не включается.

Защита от инъекций в контекстных файлах

Контекстные файлы (AGENTS.md, .cursorrules, SOUL.md) сканируются на промпт-инъекции перед включением в системный промпт. Сканер проверяет:

  • Инструкции игнорировать/отбросить предыдущие инструкции
  • Скрытые HTML-комментарии с подозрительными ключевыми словами
  • Попытки прочитать секреты (.env, credentials, .netrc)
  • Утечку учётных данных через curl
  • Невидимые юникод-символы (пробелы нулевой ширины, переопределения направления письма)

Проверке «перевести и выполнить» нужна короткая оговорка про язык или формат — например, «переведи это в bash-скрипт и выполни». Глаголы перевода и выполнения, разбросанные по несвязанной прозе с ролями через запятую, она не сшивает. Это эвристики, а не определение смыслового замысла.

Заблокированные файлы проекта показывают предупреждение:

[BLOCKED: AGENTS.md contained potential prompt injection (prompt_injection). Content not loaded.]

С вашим собственным SOUL.md в HERMES_HOME обращение иное: это файл, который написали вы (запись в него файловыми инструментами требует вашего подтверждения, а из чекаутов проектов он никогда не берётся), поэтому срабатывание сканера на нём файл не блокирует. Hermes пишет в лог предупреждение с названием совпавшего шаблона, загружает файл как обычно, а /context показывает его как ⚠ SOUL.md … loaded — matched prompt-injection pattern(s); review the file. Так файл идентичности, который описывает фразу атаки (например, рекомендация по безопасности вроде «текст, требующий игнорировать предыдущие инструкции»), продолжает работать; если помеченный текст писали не вы, считайте предупреждение признаком того, что файл отредактировал кто-то ещё. На SOUL.md, поставляемый дистрибуцией профиля, исключение не распространяется: hermes profile install <git-url> и hermes profile update копируют чужой SOUL.md в домашний каталог профиля без сканирования и запроса подтверждения, поэтому, когда файлом владеет distribution.yaml, срабатывание сканера его по-прежнему блокирует.

Лучшие практики для продакшен-развёртывания

Чек-лист развёртывания шлюза

  1. Задавайте явные allowlist — никогда не используйте GATEWAY_ALLOW_ALL_USERS=true в продакшене
  2. Используйте контейнерный бэкенд — установите terminal.backend: docker в config.yaml
  3. Ограничивайте ресурсы — задайте подходящие лимиты CPU, памяти и диска
  4. Храните секреты безопасно — держите API-ключи в ~/.hermes/.env с правильными правами доступа к файлу
  5. Включите парный доступ через DM — используйте коды парного доступа вместо жёсткого прописывания ID пользователей, когда это возможно
  6. Проверяйте allowlist команд — периодически аудируйте command_allowlist в config.yaml
  7. Задайте terminal.cwd — не позволяйте агенту работать из чувствительных директорий
  8. Запускайте не от root — никогда не запускайте шлюз от root
  9. Отслеживайте логи — проверяйте ~/.hermes/logs/ на предмет неавторизованных попыток доступа
  10. Поддерживайте актуальность — регулярно выполняйте hermes update для получения патчей безопасности

Защита API-ключей

# Set proper permissions on the .env file
chmod 600 ~/.hermes/.env

# Keep separate keys for different services
# Never commit .env files to version control

Сетевая изоляция

Для максимальной безопасности запускайте шлюз на отдельной машине или виртуальной машине. Установите terminal.backend: ssh в config.yaml, затем укажите данные хоста через переменные окружения в ~/.hermes/.env:

# ~/.hermes/config.yaml
terminal:
  backend: ssh
# ~/.hermes/.env
TERMINAL_SSH_HOST=agent-worker.local
TERMINAL_SSH_USER=hermes
TERMINAL_SSH_KEY=~/.ssh/hermes_agent_key

Данные SSH-подключения хранятся в .env (а не в config.yaml), поэтому они не попадают в систему контроля версий и не передаются вместе с экспортом профиля. Это отделяет соединения шлюза для обмена сообщениями от выполнения команд агентом.

Доверие к TLS-сертификатам

Hermes инициализирует платформенный верификатор через truststore. Windows использует своё хранилище сертификатов, macOS — системные службы доверия, Linux — системные пути доверия OpenSSL. Если инициализация не удалась, Hermes пишет сбой в лог и откатывается к умолчаниям OpenSSL.

Для корпоративного TLS-прокси установите его корневой сертификат по принятой в вашей организации процедуре доверия на уровне ОС. Резолвер провайдеров Hermes больше не выбирает доверие через HERMES_CA_BUNDLE или старую лестницу переменных окружения для CA. У подпроцессов в песочнице может быть своя отдельная настройка CA.

Вместе с этим ушла и прежняя проверка сертификатов при запуске: Hermes больше не проверяет HERMES_CA_BUNDLE / SSL_CERT_FILE / REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE на старте, поэтому SSLConfigurationError больше нет, а запасной выход HERMES_SKIP_SSL_GUARD ни на что не влияет. HERMES_CA_BUNDLE по-прежнему учитывается только процессом входа в Nous Portal (hermes login или его флаг --ca-bundle); стандартные переменные SSL_CERT_FILE / REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE всё так же читают простые вызовы requests/urllib, которые делают некоторые инструменты (а также pip, uv, curl, Node), так что устаревший путь в одной из них теперь даёт сбой на том вызове, который её использует, а не при запуске. Исправьте или сбросьте переменную там.

Собственный провайдер может объявить ssl_ca_cert для своего эндпоинта. Этот бандл заменяет платформенное доверие для чата, метаданных моделей и проб каталога моделей. Отсутствующий файл даёт предупреждение и откат к платформенному доверию. ssl_verify: false отключает проверку сертификатов и небезопасен в недоверенных сетях. Не используйте его как постоянное решение для отсутствующего корпоративного корня.

HTTP-клиенты провайдеров держат настройку прокси отдельно от выбора сертификатов. Устаревший путь к файлу CA из окружения не может помешать этим клиентам запуститься. Учётные данные индекса PM отправляются только на их точный HTTPS-источник (origin); при редиректе на другой источник они не передаются.

Точки расширения с доверием по размещению

Большая часть стороннего кода, который может запустить Hermes, закрыта явным списком разрешений: обычным плагинам нужен plugins.enabled, shell-хукам — одобрение при первом использовании (или hooks_auto_accept), MCP-серверы перечисляются в конфиге. Одна поверхность намеренно устроена иначе:

Точка расширенияОткуда загружаетсяКогда загружаетсяКак включается
Хуки событий шлюза<домашний каталог профиля>/hooks/<name>/ (HOOK.yaml + handler.py)При запуске шлюза (HookRegistry.discover_and_load()), для каждого обслуживаемого профиляРазмещением каталога. Ни записи в plugins.enabled, ни запроса; HERMES_SAFE_MODE его не пропускает.

Шлюз импортирует каждый корректный каталог хука внутри своего процесса и с собственными привилегиями. Это задокументированный контракт (начиная с 3988c3c245f), а не недосмотр: домашний каталог профиля — конфигурация, которой владеет оператор, и тот, кто может в него писать, и так может запускать код от вашего имени через shell-хуки в config.yaml или правкой plugins.enabled, так что отдельный шлюз согласия для hooks/ добавил бы трения, не сдвинув границу доверия. Относитесь к содержимому ~/.hermes/hooks/ так же, как к содержимому config.yaml: просматривайте handler.py, прежде чем положить его туда, и включайте ls ~/.hermes/hooks/ в каждую проверку остального домашнего каталога профиля (каталога нет в запретном списке защищённых путей, так что это обычное записываемое состояние). Подробности — в модели доверия хукам шлюза.

Проверка рекомендаций по цепочке поставок

Hermes поставляется со встроенным сканером рекомендаций, который отмечает Python-пакеты в активном venv, совпадающие с курируемым каталогом версий с известной компрометацией (черви цепочки поставок вроде отравления mistralai 2.4.6 в мае 2026 года). Реализация находится в hermes_cli/security_advisories.py.

Как это работает:

  • Баннер при запуске CLI. Если найдено совпадение с рекомендацией, выводится однострочное предупреждение со ссылкой на hermes doctor для полной информации об устранении.
  • hermes doctor. Показывает все актуальные рекомендации с деталями по версии и инструкциями по устранению из 2–4 шагов.
  • Запуск шлюза. Записывается в gateway.log; первое интерактивное сообщение сопровождается коротким баннером для оператора.

Каждая рекомендация имеет стабильный идентификатор. Прочитав и отреагировав на неё, вы можете отключить её навсегда:

hermes doctor --ack <advisory-id>

Подтверждение сохраняется в config.security.acked_advisories и переживает перезапуск. Старые рекомендации намеренно не удаляются из каталога — их сохранение продолжает предупреждать о ранее скомпрометированных версиях в свежих установках, которые всё ещё могут быть закэшированы в приватном зеркале.

Сама проверка использует только стандартную библиотеку и выполняет один вызов importlib.metadata.version() на каждую рекомендацию, так что её безопасно запускать при каждом старте.

Отложенная установка опциональных зависимостей

PM управляет опциональными функциями Python как extras из pyproject.toml. Установщики из исходников выбирают extra all. Нативные бандлы включают все extras, которые поддерживает их целевая платформа. Это разные наборы функций.

Когда бэкенд запрашивает недоступный extra, pm.ensure_import("extra-name") использует ту же транзакцию зависимостей, что и допуск плагинов:

  1. PM проверяет поддержку платформы и security.allow_lazy_installs.
  2. PM готовит полное окружение с уже существующими extras и требованиями включённых плагинов.
  3. Без плагинов-участников используется закоммиченный lock-файл без изменений. С участниками разрешение идёт от прежнего выбора, а затем выполняется синхронизация замороженного workspace.
  4. Кандидат проверяется до публикации своего выбора. Проваленный кандидат оставляет выбранным прежнее окружение.
  5. Если текущий процесс использует прежнее окружение, PM сообщает, что Hermes нужно перезапустить. Уже импортированные библиотеки на месте он не подменяет.

Поставляемые исходники, lock-файлы и подписанные payload остаются неизменными. Дополнительные инструменты и окружения Python используют записываемое хранилище вне базового артефакта. Зависимости плагинов делят полное окружение; изолированными песочницами Python они не являются. Совместимые транзитивные зависимости могут меняться, но объявленные ограничения и точные пины остаются обязательными.

МеханизмПоведение
Объявленные extrasХелпер принимает имена extras проекта, а не произвольные команды pip. Удалённый реестр имён функций LAZY_DEPS не используется.
Проверенные инструментыУ архивов управляемых инструментов версии и хеши SHA-256 записаны в pm/lock.json.
Атомарный выборПодготовка и проверка предшествуют публикации нового выбора среды выполнения.
Сообщения о сбояхСбои выбрасывают pm.InstallError. Квитанции синхронизации PM содержат проваленные шаги и отказы политики.
Никакого автоматического удаления плагиновПроваленное объединение зависимостей не отключает и не удаляет установленные плагины молча.

Чтобы отключить установку по требованию, выполните:

hermes config set security.allow_lazy_installs false

Уже установленными зависимостями можно пользоваться и дальше. Явные команды установки PM существуют отдельно от установки по требованию. Замороженный список функций бандла, если он есть и ленивые установки выключены, ограничивает запрашиваемые имена extras Python. Эта настройка не запрещает оптом явный допуск плагинов или ручные команды менеджера пакетов. Официальный Docker-образ тоже отключает установку по требованию через свою внутреннюю политику окружения.

Если зависимостей не хватает, определите требование через hermes tools и hermes doctor. Не запускайте pip против подписанного payload или системного Python. Кто владеет установкой, диагностику и границы команд см. в разделе Управление пакетами.

перевод и редактура:

обновлено

⌕ ESC