Модель безопасности

aipager запускает Claude Code вместо тебя, а управляется он из Telegram. Отсюда два вопроса:

Исходник на GitHub ↗Эта страница переведена с английской; если они расходятся, верна английская версия. English
  1. Кто может управлять ботом?
  2. Что может сделать бот, когда им управляют?

Граница доверия#

Каждый обработчик, который открывает бот (сообщения, файлы, голосовые, кнопки), проходит через фильтр чатов python-telegram-bot, построенный из чатов, настроенных в ~/.config/aipager/aipager.yaml, или (для команд) через ту же проверку в собственной авторизации aipager. Единственное исключение только записывает отправителей в группах, которые не настроены (ниже). С ботом могут взаимодействовать только настроенные чаты. Ничто из другого чата не обрабатывается и не попадает в сессию. Личный чат, который не настроен, получает один ответ «Этот бот не настроен общаться с тобой». В группе, которая не настроена, бот никогда не отвечает, но записывает, кто ему там написал (Telegram ID, имя пользователя и имя человека, а также ID, название и тип группы), в ~/.claude/aipager-pending-users.json, чтобы aipager config мог найти новую группу и её людей, пока aipager работает. Добавить бота в группу может кто угодно, значит, и добавить строку в этот файл может кто угодно; никаких прав это не даёт. Файл может читать только ты (права 0600), он перезаписывается целиком при каждой записи (никогда не остаётся записанным наполовину) и хранит не больше 200 последних людей, убирая тех, кого не было видно 30 дней. В командном режиме поверх этого действует список разрешённых людей с ролями, а в группе обрабатывается только сообщение, адресованное боту (команда, ответ на его сообщение, упоминание или нажатие на его клавиатуру), независимо от того, админ ли бот в группе (см. группы).

Значит, то, что открыто внешнему миру, это:

  • Токен бота (секрет).
  • ID чата (длинное целое число).

Если утекут оба, злоумышленник может управлять твоим aipager. Если утечёт только токен, злоумышленник может читать сообщения, которые отправляются боту из твоего чата, но не может отправлять команды, которые aipager выполнит. Если утечёт только ID чата, ничего полезного: боту нужен токен, чтобы вообще говорить с Telegram.

Секреты#

СекретГдеПрава
Токен бота~/.config/aipager/aipager.yamlпо умолчанию 600
ID чата (чатов)тот же файлпо умолчанию 600

Токен не попадает в лог aipager: строки HTTP-запросов, которые его несут, не логируются, а сообщения об ошибках, aipager doctor и мастер скрывают всё, что похоже на токен бота. ID чатов в логе aipager встречаются (например, в строках про лимиты), поэтому считай вывод aipager logs личным. Ни то ни другое не попадает в репозиторий: конфигурация лежит в ~/.config пользователя, а не в репозитории. Выпуск версий на PyPI через Trusted Publisher тоже никогда не касается секретов; аутентификацией занимается OIDC.

Если подозреваешь, что токен скомпрометирован, отзови его через @BotFather (/revoke), создай новый и снова запусти aipager config.

Учётные данные Claude: что их на самом деле защищает#

aipager запускает автономного агента с доступом к Bash от имени твоего пользователя UNIX. Поэтому любой секрет, который может прочитать aipager, может прочитать и агент, которого он запускает. Об этом стоит сказать прямо, потому что легко схватиться не за тот вид защиты.

~/.config/aipager/ есть в списке запретов safety.py, который не даёт сессии, управляемой из Telegram, читать файлы в нём. Это защита по пути, и здесь она ничего не даёт: учётные данные Claude (CLAUDE_CODE_OAUTH_TOKEN или ANTHROPIC_API_KEY) достаются не по пути, а наследуются в окружение процесса каждой запущенной сессии. Внутри сессии echo $CLAUDE_CODE_OAUTH_TOKEN их выведет, есть список запретов или нет. Правило по пути к файлу это не тот вид защиты для переменной окружения.

Где лежат учётные данные#

УстановкаИсточникПрава
Пользовательская служба systemdLoadCredential=claude_oauth:%h/.config/aipager/daemon.env, читается из $CREDENTIALS_DIRECTORY/claude_oauth600 (daemon.env); systemd держит учётные данные вне собственного окружения службы
macOS / Docker / без systemd~/.config/aipager/daemon.env, читается напрямую600

Оба пути разбирает один и тот же код (aipager/daemon_secrets.py) и передаёт запущенной сессии через таблицу env= подпроцесса, никогда не подставляя их в строку команды bash -c. /proc/PID/cmdline читается всеми (0444); /proc/PID/environ это 0400 (только владелец).

Что на самом деле даёт LoadCredential= + 0600: другие пользователи UNIX на том же компьютере не могут прочитать учётные данные, и они никогда не появляются в systemctl show, в резервной копии файла службы или на снимке вывода journalctl, который ты вставишь в баг-репорт. Это не создаёт границы между aipager и агентом, которого aipager запускает: такой границы нет, и никакая настройка прав на файлы не может её создать, потому что вся работа агента в том, чтобы действовать от твоего имени в этом же окружении.

Единственная защита, которая действительно работает#

Ограничивай сами учётные данные. Запускай aipager на отдельном аккаунте Anthropic или с ключом API, у которого есть лимит расходов, а не с тем же токеном, что для твоего собственного интерактивного входа в claude. Отзыв учётных данных aipager никогда не должен разлогинивать человека; если разлогинивает, это одни и те же учётные данные, и ни те ни другие не ограничены. aipager doctor --fix может найти существующий токен, чтобы заполнить daemon.env, но какой токен ему дать, решаешь ты, и это единственное решение в этом документе, которое важнее прав на файлы вокруг.

Проверка входа без догадок#

aipager doctor (и однострочное уведомление, которое приходит один раз за запуск aipager) сообщают о входе через собственный claude auth status Claude Code, никогда через самодельную проверку файлов. Три состояния везде, где они появляются, остаются разными по тексту:

  • auth: <method> (<source>): вход подтверждён.
  • auth: none (not logged in): подтверждено, что входа нет.
  • auth: unknown (...): сама проба не удалась (тайм-аут, нет программы, вывод не разбирается) или программа старше версии, где появился вывод JSON. Это никогда не показывается как «не выполнен вход»: aipager не отказывается запускать сессию из-за проверки входа, в которой он не уверен. Вход через Связку ключей macOS, учётные данные только с токеном обновления и вход в тарифе Max без файла невидимы для любой проверки файлов и иначе выглядели бы ровно как «входа нет».

Кто решает, выполнится ли вызов инструмента#

Две отдельные проверки решают, выполнится ли вызов инструмента, и идут они в таком порядке:

  1. Проверка безопасности aipager. Перед каждым вызовом инструмента Claude Code запускает hook PreToolUse от aipager. Для задачи, начатой из Telegram, он проверяет вызов по правилам роли того, кто её отправил, и по нижнему порогу безопасности (защищённые пути, шаблоны команд) и отклоняет то, что они запрещают: тогда Claude Code отклоняет инструмент, никого не спрашивая, что бы ни говорили его собственные правила. Сообщение, набранное в терминале, этой проверкой не ограничивается. Что она покрывает, описано ниже, в разделе как правила действуют в команде.
  2. Собственные правила Claude Code (~/.claude/settings.json, режим разрешений сессии). Если они говорят спросить, Claude Code вызывает PermissionRequest, и aipager показывает запрос в Telegram (см. hooks). Твоё нажатие возвращается в Claude Code как решение hook или вводится в собственный диалог Claude Code, если 20 секунд ожидания вышли. Если правила Claude Code отклоняют вызов, до Telegram он не доходит.

Значит, aipager может отклонить вызов, который Claude Code разрешил бы, а когда Claude Code спрашивает, aipager никогда не отвечает «да» сам: разрешение это всегда нажатие человека. Три вещи позволяют вызовам выполняться без вопроса человеку:

  • Режим «Полный доступ», для админов (и для человека в его собственном личном чате). Сессия в режиме «Полный доступ» запускается с --dangerously-skip-permissions: Claude Code ничего не спрашивает, поэтому каждый вызов, который не отклонила проверка безопасности aipager, выполняется без нажатия. Только админ (роли owner и admin или роль с can_manage) может запустить сессию в этом режиме, переключить на него сессию (/mode full, после подтверждения) или продолжить в нём сессию, с одним исключением: в своём собственном личном чате с ботом человек, чья роль может отправлять сообщения и отвечать Claude (can_prompt и can_approve, то есть user, но не read_only), может делать то же самое для своих сессий (/new !x1 или режим «Полный доступ» в настройке «Новые сессии» этого чата, /mode full, /resume), в Telegram и в Mini App. Никогда в группе и никогда в личном чате другого человека. Все остальные получают вместо этого режим Авто, и ответ это говорит. Режим «Полный доступ» пропускает только собственные запросы Claude Code: hook PreToolUse от aipager по-прежнему применяет к каждому вызову правила роли этого человека (нижний порог безопасности, ограничение папкой, правила запрета, файлы учётных данных), ровно как в режимах «Спрашивать» и Авто; режим, о котором сообщает Claude Code, никогда их не ослабляет. Проверка безопасности aipager в режиме «Полный доступ» по-прежнему идёт на каждом вызове, но ничего не отклоняет в собственных задачах владельца и в сообщении, набранном в терминале.
  • Режим Авто, по умолчанию. Сессия в режиме Авто запускается с --permission-mode auto: собственная проверка безопасности Claude Code смотрит на каждый вызов, пропускает безопасные и останавливает рискованные, которые тогда приходят в Telegram запросом, как в режиме «Спрашивать». Каждая сессия запускается в режиме Авто, если чат не выбрал другое в /settings → Новые сессии. Любой, кто может действовать с сессией, может переключать её между режимами «Спрашивать» и Авто (/mode); aipager делает это без перезапуска, нажимая Shift+Tab в сессии, и никогда не использует флаг, который добавил бы режим «Полный доступ» в цикл Shift+Tab у Claude Code. Если Claude Code когда-нибудь сообщит о режиме «Полный доступ» для сессии, которую aipager в этом режиме не запускал, aipager покажет, что пришло, запишет предупреждение в лог и никогда не запустит и не перезапустит из-за этого сессию в этом режиме. Известное ограничение: режим, о котором сообщает Claude Code, приходит через сокет hooks, в который может писать любой локальный процесс (см. сетевые соединения). Поддельное сообщение может изменить режим, в котором показана сессия, а пока переключение между режимами «Спрашивать» и Авто ждёт подтверждения, оно может заставить aipager нажать Shift+Tab ещё раз, и работающая сессия останется в режиме Авто, хотя просили режим «Спрашивать» (следующий настоящий hook снова покажет настоящий режим). Оно никогда не меняет флаг, который передаёт перезапуск или продолжение, и никогда не может дотянуться до режима «Полный доступ»: этот режим ставит только флаг запуска. aipager не нажимает Shift+Tab в сессии, про которую знает, что она может по кругу дойти до режима «Полный доступ»: в той, что запущена в этом режиме (даже после того, как её в терминале переключили на режим «Спрашивать» или Авто), или в той, что с момента своего запуска сообщала работающему aipager об этом режиме. Там каждая смена режима это перезапуск, который передаёт флаг нового режима и убирает флаг режима «Полный доступ», а поддельное сообщение об этом режиме может только превратить смену в перезапуск. aipager не может этого знать про сессию, запущенную в терминале с флагом, который добавляет режим «Полный доступ» в цикл Shift+Tab (--allow- перед флагом этого режима), или с --dangerously-skip-permissions, если она вышла из этого режима раньше, чем aipager о ней узнал (aipager тогда не работал или с тех пор перезапускался). Там переход от режима «Спрашивать» к Авто может остановиться на режиме «Полный доступ», и карточка тогда скажет, что сессия в режиме «Полный доступ». Не используй эти флаги для терминальных сессий, режим которых могут переключать из Telegram люди, не являющиеся админами. Чтобы режим «Полный доступ» работал из Telegram, aipager при каждом запуске ставит skipDangerousModePermissionPrompt в ~/.claude/settings.json (иначе предупреждение Claude Code перед работой в этом режиме остановило бы сессию); это выключает предупреждение и для сессий, которые ты запускаешь вручную. Так же он отмечает в ~/.claude.json папку, в которой запускается каждая сессия, как доверенную (если только она сама или папка выше уже не доверенная), чтобы вопрос Claude Code «Do you trust this folder?» не останавливал сессию, запущенную из Telegram: первое сообщение ответило бы на него «No, exit». Собственные настройки .claude/, hooks в .git/ и .mcp.json доверенной папки могут запускать команды, поэтому участники без can_manage никогда не могут записывать эти файлы (если только policy.yaml не вернёт их роли shell). Claude Code распространяет доверие к папке на вложенные папки, которые не лежат внутри git-репозитория; репозиторий всё равно получает свой вопрос. Домашняя папка отмечается, когда в ней запускается сессия: Claude Code запоминает «да», данное в домашней папке, только для этой одной сессии. После этого Claude Code ничего не спрашивает ни в одной папке внутри твоей домашней папки, которая не лежит в git-репозитории, в том числе в сессиях, которые ты запускаешь вручную. / и папки выше домашней никогда не отмечаются: это распространилось бы на все такие папки на компьютере, поэтому сессия, запущенная в одной из них, останавливается на этом вопросе. При запуске aipager ничего не отмечает, только папку, в которой вот-вот запустится сессия. Новая сессия запускается в папке, в которой работает aipager (домашняя папка при aipager service install на Linux и macOS и в контейнере), или в AIPAGER_WORK_DIR, если он задан; лог aipager предупреждает при запуске, если эта папка / или папка выше домашней и она не доверенная. Версии до этой отмечали папку aipager при каждом запуске, включая / для службы macOS (которая работала в /); aipager не может отличить такую запись от созданной тобой, поэтому оставляет её. aipager не трогает ~/.claude.json и ~/.claude/settings.json, если не может прочитать их целиком или если работающий Claude изменил их, пока aipager их обновлял.
  • Разрешать всегда, которое возвращает Claude Code правило «больше не спрашивать», предложенное вместе с запросом, ровно как это сделал бы этот вариант в его собственном диалоге.

Чтобы закрыть инструмент для всех, запрети его в ~/.claude/settings.json; чтобы закрыть для некоторых людей, используй правила их роли в policy.yaml (см. группы).

Что показывает запрос, то и выполняется. Запрос разрешения показывает настоящую команду или путь под описанием, которое даёт Claude. Управляющие символы направления Unicode (U+202A-U+202E, U+2066-U+2069, U+200E, U+200F, U+061C) меняют порядок, в котором Telegram, браузер или телефон рисуют текст после них, а shell выполняет символы по порядку, поэтому из-за них одна команда может читаться как другая (трюк «Trojan Source», CVE-2021-42574). В каждом запросе, вопросе, предпросмотре изменений и записи аудита они показываются видимыми метками (⟨U+202E⟩), а запрос предупреждает, что команда может выполнить не то, на что похожа. Другие невидимые символы остаются как есть.

Как правила действуют в команде#

В командном режиме aipager добавляет свой слой под нажатиями: каждое сообщение из Telegram несёт заметку о разрешениях (кто его отправил и что разрешает его роль), записанную в /tmp/claude-notes-<session>/, по файлу на сообщение. Когда Claude забирает сообщение, вспомогательная программа aipager-hook сверяет его с этими заметками и кладёт правила отправителя туда, где их читает проверка PreToolUse. На этих свойствах держится всё остальное:

  • Побеждает самое строгое. Если Claude забирает сообщения больше чем от одного человека одной задачей (aipager придерживает сообщения, чтобы этого не было, но остаётся безопасным, если это всё же случится), задача идёт с самым строгим сочетанием: сообщение владельца никогда не может одолжить свои права кому-то другому.
  • Идущая задача никогда не расширяется. Сообщение от другого человека ждёт, пока идущая задача закончится, а потом выполняется как своя задача с правилами своего отправителя. Если сообщение всё же присоединится к идущей задаче, задача сохраняет самые строгие правила всех, кто в ней участвует; сообщение из Telegram, которое присоединилось к задаче, набранной в терминале, держит остаток этой задачи в правилах своего отправителя (сообщение владельца ничего не меняет).
  • Твоя роль это роль в том чате, где ты действуешь. У человека, который есть в нескольких чатах (скажем, user в группе и owner своего личного чата), для всего, что он отправляет или нажимает в группе, роль из группы, а в личном чате роль из личного чата, в каком бы порядке чаты ни стояли в aipager.yaml.
  • Неизвестный значит ограниченный. Сообщение, которое нельзя ни к кому отнести, выполняется со встроенным порогом (никаких исключений, все правила запрета действуют), но никогда как неограниченное сообщение из терминала. Сообщения, которые ты набираешь прямо в терминале, остаются неограниченными; всё, что несёт метку Telegram в любой строке, проверяется. Каждое сообщение из Telegram несёт эту метку с именем того, кто отправил именно это сообщение (никогда того, кто управлял сессией последним); отправитель, которого aipager не знает, например убранный из aipager.yaml, пока его сообщение ждало, получает голое [via Telegram] и порог.
  • Slash-команды требуют админа. Slash-команда вводится в Claude как есть, без метки, а команда или навык вроде /deliver проводит целую задачу по своему собственному тексту. Поэтому участник, чья роль не обходит правила ролей (встроенные user и read_only, роли, которые ты задаёшь без bypass_role_denies, и любой, кого aipager не знает), может отправлять только команды клавиатуры «Команды» aipager (плюс /compact) и смену модели (/model <name>); любая другая slash-команда, набранная, отправленная сессии через /<name>, подписью к файлу, первым сообщением /new или шаблоном, отклоняется с «Эта команда только для админов.» и 🤷. Владельцы и админы отправляют любые. Slash-команда, которая всё же дошла до Claude из Telegram, по-прежнему проверяется: hook видит, что задача забрала сообщение из Telegram, которое отправило эту команду, и подчиняет её правилам этого отправителя. Slash-команда, которую ты набираешь в терминале, остаётся неограниченной, если только та же команда (с любыми аргументами) не приходила из Telegram раньше в этой сессии: aipager не может отличить твою копию от той, что пришла из Telegram и которую Claude Code доставил снова, поэтому твоя тогда выполняется по правилам последней задачи из Telegram. Это заканчивается вместе с сессией.
  • Сообщение без нового отправителя сохраняет правила задачи. Пока идёт задача из Telegram, Claude Code сообщает и о сообщениях, которые не объясняет ни одно сообщение из Telegram: сообщение, набранное в терминале и поставленное в очередь за задачей (о нём сообщается в момент постановки в очередь), сообщение от другой локальной сессии Claude или то же сообщение из Telegram, доставленное снова после сжатия переписки. Такое сообщение не добавляет отправителя из Telegram, поэтому задача сохраняет правила, с которыми шла, ужесточённые любым сообщением, которое ещё ждёт, и никогда не ослабленные. Сообщение с текстом из Telegram, который aipager не может ни к кому отнести, по-прежнему падает на встроенный порог, а сообщение от отправителя с меньшими правами по-прежнему опускает задачу до правил этого отправителя.

Что на самом деле сдерживает ограниченного человека. Сессия Claude работает от того же пользователя системы, которому принадлежат конфигурация aipager (токен бота в ~/.config/aipager/aipager.yaml, учётные данные Claude в ~/.config/aipager/daemon.env) и его файл правил на задачу (/tmp/claude-policy-<session>.json). Shell от этого пользователя может добраться до обоих способами, которых не предусмотрит ни один шаблон (sed -i … /tmp/*-policy-*.json, цикл for по glob, однострочник на Python), и переписать свои правила на правила владельца. Поэтому шаблоны команд это фильтр, а не граница. Для задачи со встроенными ограниченными ролями (user, read_only) и для задачи, которую aipager не может ни к кому отнести, aipager применяет такие правила, и ни одно из них не зависит от шаблонов команд (что они не покрывают, см. известные ограничения ниже):

  • Никакого shell. Им нельзя использовать Bash и любой другой инструмент, которым Claude Code выполняет код (PowerShell, Monitor, REPL, Workflow, планировщики сообщений CronCreate и RemoteTrigger, …; список это safety.CODE_EXECUTION_TOOLS). Запланированное сообщение пришло бы без метки Telegram, то есть без ограничений, поэтому они тоже считаются. По той же причине им нельзя SendMessage (он может передать сообщение другой локальной сессии Claude, которая выполнит его как сообщение из терминала) и EnterWorktree/ExitWorktree (они двигают рабочую папку сессии, на которую опирается следующее правило).
  • Запись остаётся в проекте. Write, Edit, MultiEdit и NotebookEdit пишут только в папку сессии (рабочую папку, которую Claude Code сообщает в содержимом hook, никогда не путь из вызова инструмента) и в scratchpad Claude Code этой сессии (/tmp/claude-<uid>/<project>/<session id>/ или /tmp/claude-<uid>/, если это нельзя определить). Всё остальное отклоняется: файлы запуска твоего shell, ~/.ssh, другие проекты, файлы aipager в /tmp. Внутри папки отклоняются и .claude/, .git/ и .mcp.json, потому что каждый из них заставляет Claude Code или git выполнить команду. Символические ссылки раскрываются до решения. У сессии, чья папка это твоя домашняя папка, / или папка выше домашней, нет папки проекта: там ограниченная задача пишет только в свой scratchpad, а запись куда-то ещё отклоняется с «эта сессия работает в домашней папке (или в папке выше), а эта роль может писать только внутри папки проекта». Чтобы ограниченные участники там не застревали, /new и Mini App не дают им запустить сессию в этих папках (собственная папка aipager, папка новой сессии по умолчанию, под фоновой службой это твоя домашняя папка); они могут создать новую папку внутри твоей домашней папки и запуститься в ней (именно новую: не ту, что у тебя там уже есть, и не bin или node_modules, откуда другие программы загружают код).
  • Участники остаются в своей корневой папке. Роль без can_manage (встроенные user и read_only) работает в корневой папке чата, которую ты задаёшь в aipager config (по умолчанию ~/aipager-users/<label>; ~/aipager-users и сама папка создаются с правами 0700). /new, Продолжить, Заменить, Перезапустить, смена режима и Mini App запускают или перезапускают сессию такого участника только внутри неё. Hook безопасности следит за этим, каким бы путём ни пришло сообщение (Telegram, Mini App, сообщение другой сессии): корневая папка едет в снимке правил задачи, в квитанциях сообщений между сессиями и в закреплённых и запланированных правах, а когда задачу делят участники с разными корневыми папками, побеждает более узкая (две, которые не вложены друг в друга, оставляют только собственную папку сессии). Запись идёт в папку сессии только если она внутри корневой папки (сессия вне её, например запущенная до появления корневых папок, продолжает работать, но её задачи пишут только в scratchpad: «эта сессия работает вне папки этой роли»), а Read, LSP, Grep и Glob не достают ничего в твоей домашней папке вне корневой папки («в домашней папке эта роль читает только внутри своей папки»), кроме scratchpad сессии и её собственного файла ответа. Корневая папка любого другого чата для участника защищённый путь, где бы она ни лежала, как и папка загрузок любого другого чата в /tmp/aipager-files/ (для Read, Grep, Glob и инструментов записи; для роли, которой дан Bash, команда, где названа такая папка, отклоняется, в меру возможного), вместе с файлами, которые лежали там общими для всех чатов, пока у каждого чата не было своей папки. Загрузки из собственного чата участника остаются доступными для чтения. Папки вне твоей домашней папки (/usr, /etc, /srv) остаются доступными для чтения. Своя роль с bypass_role_denies или bypass_safety, но без can_manage пишет и ищет где угодно внутри своей корневой папки, а её чтение в твоей домашней папке тоже остаётся внутри неё: без ограничений только владельцы и админы. С bypass_safety в сессии, чья папка вне её корневой папки, она пишет и ищет только в scratchpad, как user, и как только один из её вызовов инструментов отклонён, остаток задачи останавливается.
  • Поиск остаётся в проекте. Grep или Glob разрешён, только если выполнено всё это: папка, в которой идёт поиск (его path или папка сессии, если его нет, с раскрытыми символическими ссылками), находится внутри папки сессии (но не когда это твоя домашняя папка, / или папка выше домашней, как для записи) или в scratchpad; эта папка не защищённый путь и не содержит его (сессия, запущенная в твоей домашней папке, содержит ~/.config/aipager, поэтому не может искать с самого верха); каждый glob это простой относительный шаблон (без /, ~, $ или буквы диска в начале, без .., без # (комментарий для ripgrep) или ! (отрицание), без экранирования \); и у тебя нет правила защищённого пути без / или ~ в начале (**/.env может оказаться где угодно в проекте, поэтому с таким правилом каждый ограниченный поиск отклоняется). Всё остальное отклоняется. Это намеренно список разрешённого: три круга проверки каждый находили новое написание glob, которое выводило ripgrep за пределы папки, ожидаемой моделью его разбора, и одно из них прочитало токен бота.
  • Файловые инструменты соблюдают защищённые пути. Read, LSP и инструменты записи проверяют, куда на самом деле ведёт путь, включая символические ссылки. Единственный управляющий файл, который задача может прочитать, это /tmp/claude-reply-<session>.txt её собственной сессии, который aipager называет в сообщении, когда ты отвечаешь на более старое сообщение; файлы ответа других сессий и любая запись в этот остаются запрещёнными. На macOS /tmp это /private/tmp, а диск не различает регистр, поэтому распознаются оба написания и любой регистр.
  • Проверка, которая сломалась, отклоняет. Если решение по вызову инструмента вызывает ошибку (нулевой байт в пути, баг) или hook исчерпывает память во время решения, вызов отклоняется, если только правила сессии не удалось прочитать и они не дают исключение владельца. Раньше такой вызов пропускался.

Твои собственные защищённые пути. Раздел safety: в policy.yaml (deny_paths_no_access, deny_paths_no_write, deny_bash_patterns) добавляется к встроенным защищённым путям и шаблонам команд. Он входит в порог, который получает каждая задача каждой роли, кроме owner, включая админов, и применяется к файловым инструментам, поиску и командам Bash ровно как встроенный список. aipager записывает объединённый список в /tmp/claude-policy-.floor-<uid>.json (сам по себе защищённый путь) для запасных путей hook безопасности; если этого файла нет, он не читается или принадлежит другому пользователю системы, hook использует встроенный список, никогда не меньше. Изменение через перезагрузку без перезапуска применяется к сообщениям, отправленным после перезагрузки, а не к уже идущей задаче или уже ждущему сообщению. До 0.7.20 раздел читался и показывался в aipager doctor --safety-check, но не применялся.

Задачи, которые aipager не может ни к кому отнести. Задача из Telegram без отправителя, к которому её можно привязать, выполняется с порогом, который соответствует встроенной роли user (а не дополнительным ограничениям, добавленным к user в policy.yaml) плюс твой раздел safety:: никаких инструментов, выполняющих код, запись и поиск ограничены, защищённые пути. Так бывает с сообщением, чья заметка о разрешениях истекла (через 24 часа) или не записалась, и с сообщением с текстом из Telegram, который не совпадает ни с одной заметкой. Кнопка Повторить выполняется с правами того, кто её нажал, если этот же человек отправил повторяемое сообщение, так что Повторить владельца на своём сообщении сохраняет Bash; Повторить на чужом сообщении или на сообщении, отправителя которого aipager не знает, идёт по порогу, чтобы ничей текст не занимал права того, кто нажимает.

Чтение вне защищённых путей разрешено роли, которую не держит корневая папка (своя роль с can_manage, но без исключений): она может читать другие проекты пользователя системы. Встроенные user и read_only не читают ничего в твоей домашней папке вне своей корневой папки (выше). Твои файлы учётных данных это защищённые пути для каждой роли без bypass_role_denies (встроенные user и read_only, роли, которые ты задаёшь, и порог): ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.password-store, ~/.pgpass, ~/.npmrc и ~/.pypirc нельзя ни читать, ни писать, ни искать в них. Это значение роли по умолчанию, поэтому собственный deny_paths_no_access роли в policy.yaml заменяет их для этой роли, кроме случая, когда к идущей задаче присоединяется другое сообщение или сообщение, набранное в терминале: с этого момента до конца задачи она снова держится их (побеждают самые строгие правила). Файл учётных данных в другом месте (.env в проекте, токен в другой папке) не покрыт: добавь его в deny_paths_no_access роли. Владельцев всё это не касается. Запись админов не ограничена. Их Grep/Glob может искать в любой папке, которая не задевает защищённый путь: поиск отклоняется, когда папка, в которой он ищет (его path или папка сессии, если его нет; для Glob ещё и папка, которую называет его шаблон, как в /home/you/.config/aipager/*, или папка над шаблоном без подстановочных знаков, где Claude Code ищет это имя на любой глубине), после раскрытия символических ссылок является защищённой папкой, лежит внутри неё или содержит её. Поэтому Grep по ~, /, папке над твоей домашней папкой или /tmp отклоняется (в них есть ~/.config/aipager, ~/.claude или файлы aipager /tmp/claude-*), а Grep по проекту или /tmp/myproj нет. Правило защищённого пути без / или ~ в начале (вроде **/.env) может совпасть в любой папке, поэтому с ним в правилах админа (или в разделе safety:, который получает каждая роль, кроме owner) каждый поиск админа отклоняется, как у ограниченных ролей; aipager doctor предупреждает о таком правиле в разделе safety:. У админов есть Bash, поэтому их правила остаются действующими в меру возможного (ниже).

Известные ограничения. Это не покрыто:

  • Если сам hook ломается до того, как начинает решать (например, не может прочитать свой файл состояния или исчерпывает память при запуске), Claude Code считает это «разрешить». Ограниченный человек не может этого вызвать; любая ошибка во время решения отклоняет.
  • Инструменты, которые не файловые, но могут отправить данные наружу, по пути не проверяются: WebFetch (всё, что задача может прочитать, она может отправить на URL) и другие инструменты доставки Claude Code. Запрети их для роли в policy.yaml, если это для тебя важно. SendUserFile, который отправляет файлы в чат, проверяется (см. файлы, которые присылает Claude).
  • Фоновый агент, которого запустила ограниченная задача, продолжает работать после её конца, и его следующие вызовы инструментов оцениваются по самому новому сообщению в сессии: более позднее сообщение владельца или из терминала снимает его ограничения.
  • Роль, которую ты задаёшь с can_manage, но без обоих исключений, не держится корневой папки: она по-прежнему может запуститься в любой папке проекта, где её чат уже работает (папке, которую использовала прежняя сессия), включая твою, и читать твои другие проекты.
  • Задача, которую нельзя привязать ни к одному участнику (неизвестный отправитель, убранный участник, сообщение от другой сессии, чьего отправителя нельзя проверить), выполняется с ограниченным порогом, у которого нет корневой папки. По чтению этот порог слабее встроенного user: такая задача может прочитать любой файл в твоей домашней папке, который не защищённый путь (например, твои другие проекты), тогда как задача user читает только внутри своей корневой папки. Такая задача может ещё и читать загрузки всех чатов в /tmp/aipager-files/, а задача user читает только загрузки своего чата. Её запись остаётся ограничена папкой сессии и scratchpad. Проверенное сообщение из сессии участника несёт корневую папку этого участника.
  • Корневая папка ограничивает только файловые инструменты. Своя роль, которой ты дашь Bash, может читать вне её через shell (у встроенных user и read_only нет Bash). Своя роль с bypass_safety, но без can_manage сохраняет своё исключение для всего, кроме путей файловых инструментов: ей не запрещён ни один инструмент и ни один шаблон команд.
  • Причина отказа, которая называет корневую папку другого участника, показывает путь этой папки Claude и в чате.
  • У задачи участника, которая уже шла, когда появились корневые папки (обновление), её нет до следующего сообщения этого участника.
  • Внутри проекта ограниченный человек может изменить то, что ты потом запускаешь или читаешь (исходники, Makefile, скрипты package.json, conftest.py, CLAUDE.md, который загружают твои задачи).

Роль с Bash (admin, которая обходит правила запрета роли, или любая роль, которой policy.yaml его возвращает: allow_tools с Bash или заменённый deny_tools) держится только правил, работающих в меру возможного: шаблонов команд ниже, которые решительный человек может обойти. aipager doctor один раз предупреждает о каждой такой роли у участника. Давай Bash только тому, кому можно доверить shell на этом компьютере.

Встроенные правила команд. Среди шаблонов Bash, которых держится каждая задача из Telegram с Bash, не принадлежащая владельцу, один блокирует слово claude: он останавливает вложенный claude (с любым путём, обёрткой или флагом) и любой доступ к ~/.claude одним правилом, а ещё ловит простые имена управляющих файлов aipager /tmp/claude-*, у которых тот же префикс (для этих файлов шаблон не настоящая защита: написание через glob его обходит). Единственное исключение это папка scratchpad, которую создаёт Claude Code, /tmp/claude-<uid>/…: именно эта папка, как целая часть пути, исключена, поэтому команды из scratchpad сессии не останавливаются. Она принадлежит пользователю системы, а не одной сессии, поэтому задача с Bash может читать там scratchpad и вывод задач других сессий, как и инструмент Read. Glob или двойник этой папки (/tmp/claude-1*, claude-1000x), другая временная папка и всё, что достижимо из неё (…/../../home/you/.claude), по-прежнему блокируются. Другие упоминания слова (git log --grep=claude, файл с именем claude-notes.md, pip show claude-agent-sdk) тоже блокируются, намеренно: одно и то же слово называет программу, её SDK и управляющие файлы aipager, а шаблон не может их различить. Правила, которые ты добавляешь в policy.yaml, видят команду ровно так, как её набрали, включая scratchpad.

Поиск через Bash. В некоторых версиях Claude Code нет инструмента Grep или Glob (2.1.289 одна из них), и тогда Claude ищет через Bash с помощью grep, rg или find, где правило Grep/Glob выше не действует. Для каждой роли, кроме owner, шаблоны команд ещё и запрещают рекурсивный поиск или перечисление всей домашней папки или /: grep -r (-R, --recursive, -d recurse, egrep, fgrep, rgrep), rg (и rg --files), ag, ack (и ack -f), find, fd, tree, du и ls -R, если папка это ~, ~user, $HOME, ${HOME} или "$HOME", /home, /home/<user> (или /home/$USER), /Users, /Users/<user>, /root или / (с завершающим /, /. или /* или без), или ~/.config или ~/.local, где лежат папки aipager. Это действует и после cd x &&, в pipe, внутри bash -c "...", после xargs, env или timeout и после настроек вроде LC_ALL=C. Ответ советует искать внутри папки проекта. Поиск в папке проекта (grep -rn x ~/proj, rg x src/, find . -name y), поиск самого домашнего пути внутри проекта (grep -rn "$HOME" src: первое слово после опций это искомый текст, а не папка) и нерекурсивный поиск (grep x ~/.bashrc, ls ~) не затронуты. Это работает в меру возможного, как любой шаблон команд: шаблон видит слова, а не то, где shell их выполняет, поэтому он не видит поиск без папки, или с . или .., запущенный из домашней папки (или ниже) (cd ~ && grep -r x .), домашнюю папку, до которой дошли через другую переменную или glob, домашнюю папку, полностью записанную вне /home, /Users и /root, \grep и папку сразу после слова, которое кончается на -e или -f (её принимают за текст, который -e/-f даёт grep и rg, так что ag x -f ~ проходит). Он также не знает, какие опции берут отдельное значение, поэтому rg -t py "$HOME" . или grep -rn -A 3 "/home/you" src читает py или 3 как текст, а домашний путь как папку, и отклоняется (пиши -A3, --include=*.py или ставь опцию после текста), как и домашний путь, переданный -e внутри группы опций (grep -rne "/home" src; -r -n -e подходит); команда, процитированная внутри текста другой команды (echo "a; rg x ~"), тоже отклоняется, как и du -sh ~ (только размеры).

Почему одна блокировка останавливает всю задачу. После того как вызов инструмента заблокирован, каждый следующий вызов в той же задаче тоже отклоняется до следующего сообщения. Без этого агент просто повторяет то же действие другими словами (glob, другая команда, закодированный путь), пока одно из написаний не проскочит шаблоны. Остановка задачи делает первую попытку дорогой; она не делает шаблон границей, потому что первое же попробованное написание может оказаться тем, что проскочит. Цена ложной тревоги это одна остановленная задача, поэтому правила выше сделаны узкими.

См. группы → как работают правила о правилах, которые несут эти заметки, в том виде, в каком их видишь ты.

Файлы, которые присылает Claude#

У каждой сессии, которую запускает aipager, есть инструмент SendUserFile из Claude Code (CLAUDE_CODE_ATTACHMENTS_BY_PATH=1): он читает файлы и доставляет их в чат сессии (команды). Хук PreToolUse проверяет каждый файл вызова до того, как Claude Code его прочитает:

  1. Нижняя граница отправки, для каждой задачи, включая задачи owner и сообщение, набранное в терминале: конфигурация и состояние aipager (~/.config/aipager/**, ~/.local/share/aipager/**, ~/.local/state/aipager/** и его служебные файлы в /tmp), весь ~/.claude/** и ~/.claude.json* (учётные данные и аккаунт Claude), файлы с учётными данными, к которым не пускают ни одну ограниченную роль (~/.ssh/**, ~/.gnupg/**, ~/.aws/**, ~/.git-credentials, ~/.netrc и остальной этот список), /proc/**, /sys/**, /dev/** и имена, похожие на секреты, где бы они ни были (.env, .env.*, *.pem, *.key, id_rsa*, id_ed25519* и другие имена ключей, *.p12, *.pfx, *.kdbx, *.jks, *.keystore, .pgpass, credentials, *credentials*.json). Проверяется путь так, как он написан, и то, куда он ведёт на самом деле, поэтому символическая ссылка на такой файл тоже отклоняется. У некоторых безобидных файлов такие же имена (*.key из Keynote, .env.example), и они тоже отклоняются.
  2. Сам файл: обычный файл после раскрытия символических ссылок, с одной жёсткой ссылкой, не пустой, не больше 50 МБ.
  3. Правила самой задачи: то, что задаче нельзя читать (Read), а для участника (роль без can_manage, привязанная к корневой папке) более строгое правило: каждый файл, как написан и куда ведёт на самом деле, должен лежать в корневой папке. В отличие от чтения у участника, здесь нет исключения ни для файлов вне домашней папки (/tmp, /etc), ни для scratchpad сессии.

Отказ мягкий: Claude читает причину («SendUserFile refused, nothing was sent: ... Tell the person which file could not be sent and why.»), и задача продолжается; сессия не останавливается, как при заблокированном инструменте. Если проверка не сработала, файл отклоняется, и для owner тоже. У каждого отклонённого файла есть запись FileRefused в журнале аудита.

aipager не верит управляющему сокету на слово, когда речь о файле: писать в него может любой локальный процесс, называя любую сессию, любой вызов и любую историю переписки, а одного хука вызова инструмента уже хватает, чтобы простаивающая сессия считалась занятой. Поэтому вызов засчитывается только во время задачи, начало которой aipager видел по промпту (набранному им из Telegram или по UserPromptSubmit), и во время фоновой работы этой задачи, пока сессия не простаивает без единой идущей работы; в это начало aipager запоминает время и историю переписки, которую он ведёт для сессии (более поздний UserPromptSubmit той же задачи называет историю заново). Slash-команда, которую набирает aipager, не начинает задачу (команда, которая запускает задачу, сама отправляет свой UserPromptSubmit), а задача, которая так и не началась (сессия через 15 секунд всё ещё простаивает без работы), снова закрывается. Некоторые задачи Claude Code начинает без UserPromptSubmit: сообщение, отправленное во время идущей задачи, которое он берёт следующей задачей, и задачу, которую продолжает Stop-хук (/goal). Такая задача засчитывается, если это сообщение набрал сам aipager, или как только записанная история переписки показывает её начало после последнего Stop (строку очереди dequeue или строку Stop hook feedback:, которую пишет Claude Code); до этого из вызова ничего не читается, не дольше 20 секунд. Список файлов он берёт из проверенного им PreToolUse или из PostToolUse, когда первый пропущен (после перезапуска); никогда из результата Claude Code. Каждый вызов он копирует один раз: id вызова, который он уже копировал, больше никогда не копируется и не отправляется (он хранит последние 256 на сессию, в том числе после перезапуска). Он один раз находит настоящий путь каждого файла и снова проверяет файл по нему (нижнюю границу, проверки файла и корневую папку сессии), открывает именно этот путь, не переходя по ссылкам, убеждается, что открытый файл и есть проверенный и всё ещё лежит по этому пути, и копирует его в ~/.local/state/aipager/outbox/<chat>/ (папка 0700, файл 0600), куда ни одна ограниченная задача и так не допускается. Перед отправкой он ищет вызов в той истории переписки, которую запомнил для задачи (для вызова субагента в собственной истории этого субагента рядом с ней), и никогда в той, которую называет датаграмма вызова инструмента: строку ассистента с tool_use SendUserFile с этим id и ровно этими файлами, записанную из той же папки и не раньше начала задачи, в файле .jsonl внутри ~/.claude/projects (или $CLAUDE_CONFIG_DIR/projects, как эта переменная задана для самого aipager), владелец которого сам пользователь системы, под которым работает aipager. Claude Code пишет историю переписки с запозданием, поэтому aipager ждёт вызов до 20 секунд, а пока работа сессии ещё идёт (фоновый агент, вызов инструмента), хранит файлы и проверяет снова позже. Вызов, который он так и не нашёл, когда уже ничего не идёт, не отправляется, а его копии удаляются; вызов, пришедший, когда такой задачи не было, или пришедший второй раз, даже не копируется. Каждый их файл получает в журнале аудита запись FileFailed с причиной unverified. Строку о них (⚠️ Не удалось отправить report.pdf: aipager не смог подтвердить, что Claude отправил этот файл в рамках текущей задачи.) чат получает, только если собственная история переписки Claude показывает именно этот вызов во время задачи, и строка называет файлы, которые перечислены в этой строке истории, так что поддельный вызов не может поместить в чат ни строку, ни имя; иначе в чат ничего не уходит. Отправляется только копия, только в чат самой сессии, после ответа задачи, после более ранних файлов сессии и не задерживая следующую задачу; она удаляется сразу после отправки или через сутки.

Чего эти проверки не покрывают: процесс, работающий под тем же пользователем системы, что и aipager, может писать истории переписки Claude так же, как и в сокет (эти файлы он и так может прочитать сам). Процесс другого пользователя, который дотягивается до сокета, может подделать и UserPromptSubmit, который для этих проверок начинает задачу и называет её историю переписки; после этого он может заставить aipager отправить в чат этой сессии файл, который назвал настоящий вызов SendUserFile, если этот вызов записан в эту историю из той же папки после этого момента (например, вызов, который как раз в это время делает другая сессия), по разу на вызов. Для этого он должен отправить и собственный id этого идущего вызова (tool_use_id), который другой пользователь прочитать не может: он лежит в историях переписки в ~/.claude пользователя aipager, а хук получает его на вход, а не в командной строке. Поддельный Stop завершает задачу для этих проверок, так что файлы, которые эта задача отправит после него, не отправляются. Сессия, запущенная с другим $CLAUDE_CONFIG_DIR, чем у aipager, или задача, чей UserPromptSubmit aipager пропустил (набранная в терминале, пока aipager перезапускался), файлов не отправляет. Не отправляет файлов и задача, которую Claude Code начинает без хука как-то иначе (известны только два случая выше); файл, который, судя по истории переписки Claude, он там запросил, получает строку, указанную выше. Сокет лежит в $XDG_RUNTIME_DIR, куда другие пользователи не дотягиваются, если эта переменная задана; иначе он лежит в /tmp.

Журнал аудита#

Каждое нажатие Разрешить / Отклонить и каждый ответ на AskUserQuestion добавляют одну строку JSON в ~/.claude/aipager-audit.jsonl:

json
{
  "ts": "2026-05-18T15:42:11+00:00",
  "session": "claude-jim",
  "label": "jim",
  "action": "Allowed",
  "tool": "Bash",
  "summary": "ls -la /tmp",
  "user_id": 12345,
  "username": "alice",
  "denied": false
}

Поля (полный набор см. в aipager/audit.py):

  • ts: отметка времени UTC в ISO 8601, с точностью до секунды.
  • session / label: внутреннее имя и понятное имя.
  • action: что произошло (Allowed, Denied, Allowed always, ответ на AskUserQuestion, автоматический отказ, …).
  • tool / summary: имя инструмента и сокращённый ввод или текст вопроса.
  • user_id / username: кто нажал, чтобы в командном режиме было видно, кто что сделал; scope_label / scope_chat_id, где настроено несколько чатов.
  • denied: true для любого отказа, по нажатию или по правилу.

Запись идёт в меру возможного. Если диск заполнится или ~/.claude/ станет недоступной для записи, aipager пишет в лог WARNING и продолжает работать: никакой тихой потери, никакого падения. См. aipager/audit.py.

Журнал аудита на диске только дописывается. Вместе с ответом аудита в чате (одно сообщение Telegram на решение, под рабочей карточкой) он даёт две независимые записи.

Граница прав#

aipager никогда не повышает свои права. Никакого sudo, setuid, doas. Каждый файл, который он пишет, лежит в твоей домашней папке, в /tmp (управляющие файлы каждой сессии aipager, загрузки и лог перезапуска) или в папке его управляющего сокета ($XDG_RUNTIME_DIR); полный список в архитектуре. Каждый подпроцесс (claude, dtach, установщики, claude update) работает от пользователя aipager.

Установка поддержки голосовых из Telegram ([📦 Установить голосовые]) идёт через установщик, которому принадлежит aipager (uv tool install --reinstall aipager[voice], pipx install --force aipager[voice]), а иначе ставит пакеты этой поддержки собственным Python aipager (<python> -m pip install): всегда в собственное окружение aipager, никогда в системный Python.

Кнопка [🔄 Перезапустить aipager] под службой systemd планирует systemctl --user restart aipager.service в отдельной временной пользовательской службе; на macOS она запускает launchctl kickstart -k; aipager, запущенный вручную, запускает отсоединённую копию себя, которая ждёт, пока старый закончит работу. После обновления сам перезапускается только служба systemd (см. команды → обновление). Всё это работает от того же пользователя; никакого повышения прав.

Сетевые соединения#

aipager не слушает ни одного TCP-порта, кроме loopback. Сервер Mini App привязывается только к 127.0.0.1 (зашито в код: опции хоста намеренно нет) и доступен снаружи только через управляемый туннель, описанный ниже. Исходящие соединения:

  • Длинные опросы по HTTPS к api.telegram.org (опрос бота Telegram).
  • HTTPS к telegram.org ради скрипта SDK Mini App (telegram-web-app.js), который скачивается один раз при запуске сервера Mini App и обновляется не чаще раза в день; кешируется в ~/.local/share/aipager/webapp-sdk/. aipager отдаёт эти байты странице со своего источника и никогда их не выполняет. aipager не распространяет этот скрипт: в пакете его нет, а когда у aipager нет копии, страница грузит его прямо с telegram.org, как раньше.
  • Туннель Mini App к Cloudflare, пока он включён (по умолчанию так). Программа cloudflared, которую он запускает, скачивается один раз из релизов github.com/cloudflare/cloudflared, сверяется с зафиксированным SHA-256 и кешируется в ~/.local/share/aipager/cloudflared/; cloudflared из твоего PATH никогда не используется. С твоим собственным URL для Mini App ничего из этого не происходит (см. ниже).
  • Проверка версий, только когда кто-то нажимает Проверить обновления или Обновить (/update, «Обновления» в Mini App), или когда кто-то, кому можно обновлять, открывает вкладку Настройки в Mini App (не чаще раза в десять минут на открытое приложение): pypi.org для aipager и downloads.claude.ai или registry.npmjs.org для Claude Code, в зависимости от того, как он установлен. Обновление потом запускает установщик (который говорит с PyPI или Homebrew) и claude update. aipager update в командной строке запускает только установщик.
  • Установка поддержки голосовых, когда ты нажимаешь её кнопку: установщик скачивает пакеты с PyPI, а голосовая модель скачивается с Hugging Face (huggingface.co) при первом использовании.
  • HTTPS к raw.githubusercontent.com (файл с ключом отчётов) и к узлу приёма Sentry, только когда ты подтверждаешь отправку отчёта о проблеме: отчёт, который был в предпросмотре, и короткая сводка из него, больше ничего. См. Отчёты о проблемах.

Входящие:

  • Unix datagram-сокет в $XDG_RUNTIME_DIR/aipager.sock (с запасным /tmp/aipager.sock, когда $XDG_RUNTIME_DIR не задан). Привязывается и получает права через chmod при запуске aipager (aipager/dtach/hook_receiver.py). Права 0o666, чтобы любой локальный процесс мог отправлять ему события hooks: то же доверие, что и к ~/.claude/settings.json, который и так решает, что запускает hooks.

Это значит, что aipager не поверхность для удалённой атаки. Злоумышленник в сети не может до него добраться без опоры на самом компьютере.

Текст Claude и то, что Telegram скачивает сам. Серверы Telegram сами скачивают некоторые адреса из сообщения: блок с картинкой, видео или аудио в rich-сообщении и страницу, которую показывает предпросмотр ссылки. Ответы Claude, его заметки на рабочей карточке, описания агентов и аргументы инструментов попадают в Telegram такими, какими их написал Claude, поэтому prompt injection может заставить Claude написать адрес, который уносит секрет наружу, без вызова инструмента и без запроса разрешения. aipager закрывает первый путь, а второй там, где у Telegram есть для этого переключатель:

  • В каждом rich-сообщении картинка, записанная как ![alt](url), отправляется обычной ссылкой со своим alt-текстом, а любая другая запись картинки и сырые теги <img>, <video>, <audio>, <source>, <picture>, <figure>, <iframe>, <embed>, <object>, <tg-document>, <tg-collage>, <tg-slideshow>, <tg-button> и <tg-button-row> показываются как текст (aipager/bot/media_guard.py). Внутри блока кода, например фрагмента HTML, из-за этого виден лишний \ или &lt;.
  • Предпросмотр ссылок выключен в каждом обычном текстовом и HTML-сообщении, которое aipager отправляет или правит (включая запасные версии ответов и карточек простым текстом), в том числе в его собственных. Ссылки на его Mini App это кнопки, а их Telegram не скачивает.

Telegram не описывает настройку предпросмотра для rich-сообщений; строит ли он предпросмотр ссылки для такого сообщения, на живом сервисе не проверялось.

Туннель Mini App#

Mini App включена по умолчанию, поэтому на стандартной установке aipager открывает этот туннель, никого не спрашивая. Это сознательный выбор (опция «включить», которую никто не находил, была функцией, которой не существовало), но он значит, что свежая установка выходит в интернет. Выключи её через aipager miniapp disable или поставь enabled: false под miniapp: в aipager.yaml; явный false всегда соблюдается. Обрати внимание: отсутствующий или повреждённый блок miniapp: теперь откатывается на ВКЛ, а не на ВЫКЛ.

Mini App (если её не заменяет --url) запускает управляемый быстрый туннель Cloudflare, который смотрит на собственный loopback-сервер aipager, и публикует выданный ему публичный адрес https://*.trycloudflare.com как кнопку меню Telegram и ответ на /app.

Этот адрес не секрет и не должен им быть: каждый запрос, который обслуживает Mini App, проверяется по подписи initData от Telegram (см. Граница доверия выше), и эта же проверка защищает её, как бы ни был получен URL. Знание одного имени хоста не даёт злоумышленнику ничего, что он не получил бы, наугад угадав поддомен trycloudflare.com: настоящие ворота это проверка подписи. Относись к нему как к любому другому URL, который не опубликован, но и не секретен: держать его включённым нормально, а вставлять в публичный канал без причины не стоит.

Имя хоста хранится только в памяти: оно никогда не пишется в aipager.yaml или куда-либо в ~/.config/aipager/. Плановый перезапуск (перезапуск после обновления, Перезапустить сейчас, перезапуск на месте без службы) оставляет туннель работать, так что адрес, кнопка меню и открытая Mini App продолжают работать: следующий aipager подхватывает его, только проверив, что это та же загрузка системы, тот же порт, что процесс именно этот cloudflared (по его командной строке), что он всё ещё сообщает тот же адрес (~/.claude/aipager-tunnel.json хранит его хеш, никогда не сам адрес) и что адрес отвечает. Во всех остальных случаях он останавливает его и запускает новый, а оставленный туннель, который ни один aipager не подхватил за 3 минуты, останавливает маленький процесс-сторож. Любая другая остановка (Ctrl+C, docker stop, systemctl stop) останавливает туннель, а перезагрузка компьютера или перезапуск контейнера даёт новый адрес. MINIAPP_KEEP_TUNNEL=0 выключает сохранение туннеля. Если aipager остановлен жёстко (нехватка памяти, kill -9), туннель работает, пока aipager снова не запустится с управляемым туннелем (Mini App включена, нет MINIAPP_PUBLIC_URL) и не подхватит или не остановит его, или пока компьютер не перезагрузится; aipager uninstall тоже его останавливает. Если туннель падает посреди работы, aipager перезапускает его с растущими паузами и публикует новый адрес; если после нескольких попыток он не поднимается, кнопка убирается, а не остаётся указывать на мёртвый порт.

Если задать MINIAPP_PUBLIC_URL (или aipager miniapp enable --url https://…), управляемый туннель полностью выключается (программа cloudflared никогда не скачивается и не запускается), а заданный URL используется как есть, с той же проверкой initData под ним. Автоопределение Tailscale (tailscale status --json) остаётся более простой альтернативой для тех, у кого уже работает Tailscale и кто предпочитает вовсе не зависеть от Cloudflare.

Превращение голосовых в текст#

faster-whisper работает внутри процесса aipager. Аудио скачивается как .ogg в собственную папку чата в /tmp/aipager-files/ (туда же попадают присланные файлы; папка 0700, файл 0600), расшифровывается локально на процессоре, и файл остаётся под твоим контролем. Ни одно аудио не покидает компьютер. Никаких сторонних API. Никаких ключей, кроме токена бота, который и так нужен, чтобы говорить с Telegram.

Если удалить .ogg после расшифровки, единственной записью сообщения обычным текстом будет расшифровка, которая вводится в сессию claude (где она подчиняется правилам конфиденциальности самого Claude Code).

Изоляция между сессиями#

Каждая сессия Claude Code работает в своём dtach. Управляющий сокет в /tmp/claude-dtach-<name>.sock принадлежит пользователю aipager; dtach не даёт подключаться другим пользователям. Каждая сессия запускается в своей папке, выбранной через /new или Mini App, или в папке, где был запущен aipager session.

aipager не изолирует сессии друг от друга на уровне файловой системы: сессия, привязанная к ~/projects/foo, в принципе может читать ~/projects/bar, если разрешения Claude Code это позволяют. Для более строгой изоляции используй ~/.claude/settings.json для каждого проекта или контейнер (образ Docker).

Сообщения между сессиями Claude#

Claude Code позволяет одной сессии отправить сообщение другой (его инструмент SendMessage). В сессии, которая его получает, это сообщение такое же, как любое другое, поэтому aipager решает о его правах так же, как о правах сообщения из Telegram: по тому, кто на самом деле его отправил, а не по тому, что в нём написано.

  • Сообщение несёт права отправителя, никогда не больше. Когда aipager разрешает сессии отправить сообщение, он пишет одноразовую квитанцию: отпечаток точного текста, процесс Claude, который отправляет (его номер процесса и, на Linux, время его запуска), и права, которые были у этой задачи. Claude Code записывает полученное сообщение с номером процесса отправителя, который проверило ядро, и никогда не доверяет имени, которым отправитель называет себя сам. Прежде чем задача, получившая сообщение, запустит хоть один инструмент, aipager сопоставляет одно с другим и даёт задаче ровно права задачи, которая его отправила: сообщение владельца идёт с правами владельца, сообщение участника с правами этого участника, сообщение, набранное в терминале отправляющей сессии, без ограничений.
  • Нет квитанции, ограниченные права. Сообщение, которое aipager не может сопоставить (отправленное сессией или процессом Claude, который aipager не запускал, квитанция, истёкшая через час, отправитель на другом компьютере), идёт с самыми строгими правилами, без изменений файлов и без доступа в интернет (только читать и думать). Прежние права получающей сессии к нему никогда не применяются.
  • Другой ввод извне сессии так же. Всё, что Claude Code записывает как пришедшее не от человека этой сессии, тоже получает эти ограниченные права: уведомление из канала MCP, удалённый вызывающий, сообщение, которое ставит в очередь плагин, сообщение, переданное из входящих другой сессии, и любой вид ввода, которого aipager ещё не знает (команда плагина считается вводом извне, даже когда её запускаешь ты). Собственные фоновые агенты сессии (их возвраты и отчёты) и её уведомления о задачах сохраняют права работы.
  • Сообщение, которое сессия запланировала, идёт с правами задачи, которая его запланировала. Когда задача ставит /loop или cron, aipager записывает для этого сообщения права этой задачи; когда оно срабатывает позже, оно получает эти права, никогда не больше, кто бы ни говорил последним; сообщение, присоединившееся к этой задаче, может их только сузить, а изменение правил, которое отнимает права у того, кто запланировал, тоже их сужает. Одно и то же сообщение, запланированное дважды, сохраняет более строгие права. Голый /loop срабатывает с инструкциями цикла Claude Code вместо своего текста, поэтому получает самые строгие права из всех голых циклов, которые поставила сессия, как только история переписки покажет срабатывание (до этого отклонённый вызов инструмента отклоняется мягко, и Claude пробует снова). Сообщение, которое никто не планировал через проверку aipager, получает ограниченные права.
  • Текст ничего не доказывает. Имя отправителя, строка [via Telegram · @owner] или «владелец просит…» внутри сообщения ничего не меняют. Claude узнаёт, кого aipager проверил как отправителя, и что утверждения в тексте не доказательство.
  • Присоединение к идущей задаче её только сужает. Сообщение от другой сессии, которое приходит, пока идёт задача, делает эту задачу такой же строгой, как более строгая из двух, никогда свободнее. Это так и тогда, когда Claude Code забирает сообщение без собственного события сообщения (оно поглощается идущей задачей или запускается из очереди как следующая задача): быстрый путь владельца смотрит в конец истории переписки перед каждым вызовом инструмента, а конец задачи и aipager отмечают сессию для проверки, когда видят такое сообщение в очереди.
  • Роли остаются в своём чате. Квитанция от сессии другого чата считается, только если у её задачи были права владельца или она была набрана в её терминале; всё остальное идёт ограниченно.
  • Кто может отправлять. Встроенные ограниченные роли вообще не могут использовать SendMessage. Роль, которой правило его даёт, может писать только сессиям, которые запускает aipager (адресуются как uds:…/<pid>.sock); aipager doctor предупреждает о такой роли.
  • Фоновые агенты сохраняют свои права. Агент сохраняет права задачи, которая его запустила (или более строгие права более поздней задачи), никогда не права более поздней задачи, у которой их больше.
  • Когда её останавливают. Если задачу, которую начала другая сессия, останавливают правила безопасности, рабочая карточка уже показывает, кто её отправил, уведомление об остановке это говорит, а тот, кто передал задачу, узнаёт об этом, чтобы она никогда не пропадала молча: чат отправляющей сессии, когда обе сессии в одном чате; иначе только владелец, в личном чате, и только когда отправляющей задачей управлял владелец (или терминал).

Известные ограничения:

  • Задача с shell (у владельца или админа) работает от того же пользователя системы, что и aipager, и могла бы подделать квитанцию, так же как она уже могла переписать другие управляющие файлы aipager. Это существующая модель доверия: давай shell только тем, кому можно доверить этот компьютер.
  • Сессии Claude, которые не запускает aipager, aipager вообще не управляет, кто бы им ни писал.
  • Не на Linux отправитель узнаётся только по номеру процесса (Claude Code не записывает там время запуска). На Linux запись о получении без времени запуска ни с чем не совпадает.
  • Claude Code может записать сообщение в историю переписки на мгновение позже первого вызова инструмента задачи. Пока он этого не сделал, вызов, который запретили бы ограниченные права, отклоняется мягко (Claude говорят попробовать снова; задача не останавливается). После 8 вызовов без него задача идёт ограниченно.
  • Быстрый путь владельца читает последний 1 МиБ истории переписки, никогда не больше. Сообщение от другой сессии длиннее этого заканчивается строкой, которую быстрый путь принимает за такое сообщение (так считается любая строка длиннее 256 КиБ, обрезанная окном). Сообщение, которое лежит целиком дальше (больше 1 МиБ истории записано после него до следующего вызова инструмента), ловит aipager, пока идёт задача, или конец задачи, пока оно ещё в очереди, а не быстрый путь.
  • Пока у сессии стоит голый /loop, у любой задачи, которую начинает не сообщение из Telegram (включая сообщение, набранное в терминале), первые вызовы инструментов могут мягко отклоняться, пока Claude Code не запишет сообщение этой задачи и aipager не увидит, что это не срабатывание цикла; тогда Claude пробует снова.
  • aipager читает задачу запланированного сообщения из того, что история переписки получила с её начала; свыше 8 МиБ из её первых и последних 4 МиБ (то, что между ними, проверялось, когда было самой новой частью).
  • Записанные права запланированного сообщения остаются, пока сессию не завершат (/kill): завершённая или продолженная сессия их сохраняет, так же как Claude Code сохраняет свои cron и ожидающее пробуждение /loop. На сессию хранится не больше 256, а запланировать ещё одно сверх этого не сможет никто в этой сессии, так что любой, кто может планировать, может их исчерпать (тогда запусти сессию под другим именем или сделай /kill этой). Сообщение, которое срабатывает без записи, получает ограниченные права, а в сессии с голым /loop права этого цикла, как только Claude Code запишет его в историю переписки; до этого aipager читает задачу по предыдущей. Так бывает с сообщением, поставленным до этой версии; после перезагрузки (записи живут во временной папке системы, которую перезагрузка очищает, а Claude Code ещё может вернуть пробуждение продолженной сессии в течение часа); и с долговременным cron, который Claude Code запускает в другой сессии той же папки, когда его собственная сессия завершилась. Сессия с меньшими правами, которая планирует тот же текст, может понизить права сообщения (но никогда не повысить).
  • Сессия, запущенная заново под именем прежней (Заменить), сохраняет записи той сессии, включая права её голого /loop, до /kill.
  • Запланированное сообщение, которое Claude Code выполняет сразу после другой задачи без собственного события сообщения (из своей очереди), получает более строгие из своих прав и прав задачи, за которой оно идёт (после задачи, набранной в терминале, ровно свои).
  • Первые вызовы инструментов задачи, набранной в терминале, могут отклоняться, пока Claude Code не запишет её сообщение в историю переписки (до этого aipager читает задачу перед ней).
  • Фоновый агент, запущенный с более узкими правами, может вернуть текст в более позднюю задачу той же работы, которая читает его с правами работы, как и другие уведомления о задачах Claude Code.
  • Claude Code может придержать сообщение до одобрения в получающем терминале (его настройка crossSessionInbound или когда две сессии работают в разных классах разрешений, например режимы «Полный доступ» и Авто). aipager не может выпустить такое сообщение из Telegram; квитанция ждёт до часа.

Сводка угроз#

УгрозаЗащита
Незнакомец отправляет боту командуФильтр по ID чата отклоняет
Украден токен бота/revoke в @BotFather, потом настроить заново
Скомпрометированный вызов инструмента claudeПроверка безопасности aipager отклоняет то, что запрещают правила задачи из Telegram; в режиме «Спрашивать» собственные правила Claude Code в settings.json решают, о чём он спрашивает, а нажатие человека единственное разрешение; в режиме Авто (по умолчанию для каждой новой сессии) собственная проверка безопасности Claude Code пропускает без вопроса вызовы, которые считает безопасными, и останавливает рискованные для человека; сессия в режиме «Полный доступ» (админы и человек в своём собственном личном чате) ничего не спрашивает, поэтому на пути стоит только проверка безопасности aipager, а для собственных задач владельца и сообщений из терминала ничего (кто решает)
Ограниченный человек в Telegram получает права владельцаНикакого shell, сессии запускаются только в собственной корневой папке участника, запись и поиск ограничены проектом внутри неё (никогда не домашняя папка), никакого чтения в домашней папке вне корневой папки, корневые папки других участников и загрузки других чатов недоступны, файлы учётных данных не читаются, защищённые пути для файловых инструментов, сломавшиеся проверки отклоняют (как правила действуют в команде); только в меру возможного для роли с Bash и не покрыто для случаев из «Известных ограничений»
Сообщение сессии утверждает, что говорит владелецСообщение идёт с доказанными правами отправившей задачи, никогда не с прежними правами получающей сессии; без доказательства только читать и думать (сообщения между сессиями)
Участник группы занимает чужие праваКаждое сообщение идёт с ролью своего отправителя в том чате, откуда пришло; сообщение другого отправителя ждёт идущую задачу; если оно всё же присоединится, побеждают самые строгие правила; slash-команды ограниченных ролей сведены к командам клавиатуры (как правила действуют в команде)
Разговоры в группе попадают в сессиюВ группе обрабатываются только команды, ответы боту, упоминания бота и нажатия на помеченную клавиатуру (группы)
Убранный участник сохраняет доступaipager config перезагружает aipager без перезапуска; его придержанные сообщения выбрасываются, а его ещё идущая задача теряет прежние права; удаление из группы Telegram не даёт ему её читать (группы → перезагрузка без перезапуска)
Подделка журнала аудитаТолько дописывается; предотвращать это без отдельной программы подписи вне рамок
Prompt injection заставляет Claude написать адрес, который уносит данные наружуКартинки и медиатеги в тексте Claude показываются ссылками или текстом, никогда как медиа, а предпросмотр ссылок выключен в обычных текстовых и HTML-сообщениях (сетевые соединения)
Злоумышленник в сетиНет входящего порта, напрямую недоступен
Локальное повышение правНикакого sudo / setuid; aipager остаётся в пространстве пользователя
Аудио голосовых утекает в облакоРасшифровка локальная
Агент читает собственные учётные данные Claude у aipagerОжидаемо, не баг: см. Учётные данные Claude. Ограничивай сами учётные данные, а не файл, где они лежат

См. также#