Модель безпеки

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 не дає йому її читати (групи → перезавантаження без перезапуску)
Підробка журналу аудитуЛише дописується; запобігати цьому без окремої програми підпису - поза межами цієї моделі
Промпт-ін'єкція змушує Claude написати адресу, яка виносить дані назовніЗображення й медіатеги в тексті Claude показуються як посилання чи текст, ніколи як медіа, а попередній перегляд посилань вимкнено в повідомленнях звичайним текстом і HTML (мережеві з'єднання)
Зловмисник у мережіНемає вхідного порту, напряму недоступний
Локальне підвищення правЖодного sudo / setuid; aipager лишається в просторі користувача
Аудіо голосових витікає в хмаруРозшифрування локальне
Агент читає власні облікові дані Claude в aipagerОчікувано, не баг: див. Облікові дані Claude. Обмежуй самі облікові дані, а не файл, де вони лежать

Див. також#