Modelo de segurança

O aipager roda o Claude Code em seu nome e é controlado pelo Telegram. Daí vêm duas perguntas:

O original no GitHub ↗Esta página é uma tradução da página em inglês; se as duas não baterem, quem vale é a página em inglês. Inglês
  1. Quem pode controlar o bot?
  2. O que o bot pode fazer depois de controlado?

Limite de confiança#

Cada tratador que o bot expõe (mensagem, arquivo, áudio, botão) fica atrás do filtro de chats do python-telegram-bot, montado com os chats configurados em ~/.config/aipager/aipager.yaml, ou (para os comandos) atrás da mesma verificação na autorização do próprio aipager. A única exceção só anota quem escreve em grupos não configurados (abaixo). Só os chats configurados podem usar o bot. Nada que venha de outro chat é executado nem enviado para uma sessão. Um chat privado não configurado recebe uma única resposta “🚫 Este bot não está configurado para falar com você.”. Num grupo não configurado o bot nunca responde, mas anota quem escreveu para ele lá (o ID do Telegram, o nome de usuário e o nome da pessoa, e o ID, o título e o tipo do grupo) em ~/.claude/aipager-pending-users.json, para o aipager config conseguir encontrar um grupo novo e as pessoas dele enquanto o aipager roda. Qualquer um pode adicionar o bot a um grupo, então qualquer um pode acrescentar uma linha a esse arquivo; isso não dá nenhuma permissão. Só você pode ler o arquivo (permissões 0600), ele é substituído inteiro a cada escrita (nunca fica escrito pela metade) e guarda no máximo as 200 pessoas mais recentes, tirando quem não aparece há 30 dias. No uso em equipe, uma lista de pessoas permitidas com funções é acrescentada por cima, e num grupo só é atendida uma mensagem dirigida ao bot (um comando, uma resposta a ele, uma menção ou um toque no teclado dele), seja o bot admin do grupo ou não (veja uso em equipe).

Isso quer dizer que o que fica aberto para o mundo lá fora é:

  • O token do bot (um segredo).
  • O ID do chat (um número comprido).

Se os dois vazarem, alguém pode controlar o seu aipager. Se só o token vazar, alguém pode ler as mensagens que o seu chat manda para o bot, mas não pode enviar comandos que o aipager vá executar. Se só o ID do chat vazar, não serve para nada: o bot precisa do token para falar com o Telegram.

Segredos#

SegredoOndePermissões
Token do bot~/.config/aipager/aipager.yaml600 por padrão
IDs de chato mesmo arquivo600 por padrão

O token não vai para o log do aipager: as linhas de pedido HTTP que o carregam não são registradas, e as mensagens de erro, o aipager doctor e o assistente escondem qualquer coisa com cara de token de bot. Os IDs de chat aparecem no log do aipager (por exemplo nas linhas de limites de velocidade), então trate a saída do aipager logs como privada. Nenhum dos dois vai para o repo: a configuração fica no ~/.config do usuário, não no repo. O processo de publicação no PyPI com Trusted Publisher também não mexe em segredos; a autenticação é feita pelo OIDC.

Se você suspeitar que o token vazou, revogue-o no @BotFather (/revoke), gere um novo e rode aipager config de novo.

A credencial do Claude: o que a protege de verdade#

O aipager roda um agente autônomo com acesso ao Bash como o seu usuário do UNIX. Por isso, qualquer segredo que o aipager consegue ler, o agente que ele inicia também consegue. Vale dizer isso com clareza, porque é fácil buscar o tipo errado de controle.

~/.config/aipager/ está na lista de bloqueio do safety.py, que impede uma sessão controlada pelo Telegram de ler os arquivos que estão ali. Isso é um controle de caminho, e aqui não faz nada: a credencial do Claude (CLAUDE_CODE_OAUTH_TOKEN ou ANTHROPIC_API_KEY) não é alcançada por um caminho; ela é herdada no ambiente do processo de cada sessão iniciada. Dentro de uma sessão, echo $CLAUDE_CODE_OAUTH_TOKEN a imprime, com lista de bloqueio ou sem. Uma regra de caminho de arquivo é o tipo errado de controle para uma variável de ambiente.

Onde fica a credencial#

InstalaçãoDe onde vemPermissões
serviço de usuário do systemdLoadCredential=claude_oauth:%h/.config/aipager/daemon.env, lido de $CREDENTIALS_DIRECTORY/claude_oauth600 (daemon.env); o systemd mantém a credencial fora do ambiente do próprio serviço
macOS / Docker / sem systemd~/.config/aipager/daemon.env, lido diretamente600

Os dois caminhos são lidos pelo mesmo código (aipager/daemon_secrets.py) e passados à sessão pela tabela env= do subprocesso, nunca colocados dentro da string do comando bash -c. O /proc/PID/cmdline qualquer um pode ler (0444); o /proc/PID/environ só o dono (0400).

O que o LoadCredential= mais 0600 dão de verdade: outros usuários do UNIX no mesmo computador não conseguem ler a credencial, e ela nunca aparece no systemctl show, numa cópia de segurança do arquivo do serviço, nem num print da saída do journalctl que alguém cole num relatório de bug. Isso não cria um limite entre o aipager e o agente que o aipager inicia; esse limite não existe, e nenhuma quantidade de engenharia de permissões de arquivo pode criá-lo, porque todo o trabalho do agente é agir como o dono dentro desse mesmo ambiente.

O único controle que funciona de verdade#

Limite a própria credencial. Rode o aipager com uma conta separada da Anthropic, ou com uma chave de API que tenha um limite de gastos; não com o mesmo token que você usa para o seu próprio login interativo no claude. Revogar a credencial do aipager nunca deveria tirar você do seu login; se tirar, as duas são a mesma credencial e nenhuma está limitada. O aipager doctor --fix consegue encontrar um token existente para preencher o daemon.env, mas qual token entregar a ele é decisão sua, e é a única decisão deste documento que importa mais que as permissões dos arquivos em volta.

Diagnosticar o login sem adivinhar#

O aipager doctor (e o aviso de uma linha enviado uma vez a cada início do aipager) informam o login pelo próprio claude auth status do Claude Code, nunca por uma verificação de arquivos feita à mão. Três estados ficam separados no texto em todo lugar:

  • auth: <method> (<source>): login feito, confirmado.
  • auth: none (not logged in): sem login, confirmado.
  • auth: unknown (...): a própria verificação falhou (tempo esgotado, falta o executável, saída que não dá para entender) ou o executável é anterior à versão que adicionou a saída em JSON. Isso nunca é informado como “sem login”: o aipager não se recusa a iniciar uma sessão por uma verificação de login de cujo resultado não tem certeza. O login pelo Keychain do macOS, as credenciais que só têm um refresh token e os logins sem arquivo do plano Max são invisíveis para qualquer verificação de arquivos e, senão, pareceriam idênticos a “sem login”.

Quem decide se uma ferramenta roda#

Duas verificações separadas decidem se uma ferramenta roda, nesta ordem:

  1. A verificação de segurança do aipager. O Claude Code roda o hook PreToolUse do aipager antes de cada ferramenta. Para uma tarefa que começou pelo Telegram, ela prende a execução às regras da função de quem mandou a mensagem e ao piso de segurança (caminhos protegidos, padrões de comandos), e nega o que eles negam: aí o Claude Code recusa a ferramenta sem perguntar a ninguém, digam o que disserem as regras dele. Uma mensagem digitada no terminal não fica limitada por essa verificação. O que ela cobre está mais abaixo, em como as regras valem no uso em equipe.
  2. As regras do próprio Claude Code (~/.claude/settings.json, o modo de permissão da sessão). Se elas disserem para perguntar, o Claude Code dispara o PermissionRequest e o aipager mostra o pedido no Telegram (veja hooks). O seu toque volta ao Claude Code como a decisão do hook, ou é digitado na caixa de diálogo do próprio Claude Code se a espera de 20 segundos acabou. Se as regras do Claude Code negarem a ferramenta, ela nunca chega ao Telegram.

Então o aipager pode negar uma ferramenta que o Claude Code deixaria, e quando o Claude Code pergunta, o aipager nunca diz sim por conta própria: uma permissão sempre é o toque de uma pessoa. Três coisas deixam as ferramentas rodarem sem perguntar a ninguém:

  • Modo Acesso total, para admins (e para cada pessoa no próprio chat privado). Uma sessão no Modo Acesso total começa com --dangerously-skip-permissions: o Claude Code não pergunta nada, então toda execução que a verificação de segurança do aipager não nega roda sem toque. Só um admin (as funções owner e admin, ou uma função com can_manage) pode iniciar uma sessão no Modo Acesso total, mudar uma para ele (/mode full, depois de uma confirmação) ou retomar uma nele, com uma exceção: no próprio chat privado com o bot, uma pessoa cuja função deixa mandar mensagens e responder ao Claude (can_prompt e can_approve, então um user sim, mas read_only não) pode fazer o mesmo com as próprias sessões (/new !x1, ou Novas sessões no Modo Acesso total nesse chat, /mode full, /resume), no Telegram e no Mini App. Nunca num grupo nem no chat privado de outra pessoa. Os outros recebem o Modo Automático no lugar, e a resposta diz isso. O Modo Acesso total só pula as perguntas do próprio Claude Code: o hook PreToolUse do aipager continua aplicando as regras da função dessa pessoa (o piso de segurança, o limite de pastas, as regras de negação, os arquivos de credenciais) em cada execução, exatamente como no Modo Perguntar e no Modo Automático; o modo que o Claude Code informa nunca as afrouxa. A verificação de segurança do aipager continua rodando em cada chamada no Modo Acesso total, mas não nega nada nas tarefas do próprio dono nem numa mensagem digitada no terminal.
  • Modo Automático, o padrão. Uma sessão no Modo Automático começa com --permission-mode auto: a verificação de segurança do próprio Claude Code olha cada execução, roda as seguras e segura as arriscadas, que depois chegam ao Telegram como um pedido, igual no Modo Perguntar. Toda sessão começa no Modo Automático, a não ser que o chat tenha escolhido outra coisa em /settings → Novas sessões. Qualquer pessoa que pode agir sobre uma sessão pode mudá-la entre o Modo Perguntar e o Modo Automático (/mode); o aipager faz isso sem reiniciar, apertando Shift+Tab na sessão, nunca com uma opção que colocaria o Modo Acesso total no ciclo do Shift+Tab do Claude Code. Se algum dia o Claude Code informar o Modo Acesso total para uma sessão que o aipager não iniciou nesse modo, o aipager mostra o que foi informado, escreve um alerta no log e nunca inicia nem reinicia a sessão no Modo Acesso total por causa disso. Limite conhecido: o modo que o Claude Code informa chega pelo socket do hook, no qual qualquer processo local pode escrever (veja conexões de rede). Um informe falso pode mudar o modo com que uma sessão aparece e, enquanto uma troca entre o Modo Perguntar e o Modo Automático espera a confirmação, pode fazer o aipager apertar Shift+Tab mais uma vez, o que pode deixar a sessão de verdade no Modo Automático quando foi pedido o Modo Perguntar (o próximo hook de verdade mostra o modo real de novo). Ele nunca muda a opção que um reinício ou uma retomada passam, e nunca chega ao Modo Acesso total: só a opção de início define isso. O aipager não aperta Shift+Tab numa sessão que ele sabe que pode chegar ao Modo Acesso total pelo ciclo: uma que começou no Modo Acesso total (mesmo depois de ser mudada para o Modo Perguntar ou o Modo Automático no terminal), ou uma que informou o Modo Acesso total ao aipager em execução desde que começou. Ali toda troca de modo é um reinício, que começa com a opção do modo novo e tira a do Modo Acesso total, e um informe falso de Modo Acesso total só consegue transformar uma troca num reinício. O aipager não tem como saber isso de uma sessão iniciada num terminal com a opção que acrescenta o Modo Acesso total ao ciclo do Shift+Tab (--allow- na frente da opção do Modo Acesso total), ou com --dangerously-skip-permissions quando ela saiu do Modo Acesso total antes de o aipager saber dela (o aipager não estava rodando então, ou reiniciou depois). Ali, ir do Modo Perguntar para o Modo Automático pode parar no Modo Acesso total, e então o cartão diz que a sessão está no Modo Acesso total. Não use essas opções em sessões de terminal que pessoas que não são admins podem mudar pelo Telegram. Para o Modo Acesso total funcionar pelo Telegram, o aipager define skipDangerousModePermissionPrompt em ~/.claude/settings.json a cada início (senão o aviso do Claude Code antes de rodar nesse modo travaria a sessão); isso também desliga o aviso para as sessões que você inicia por conta própria. Do mesmo jeito, ele marca como confiável em ~/.claude.json a pasta onde cada sessão começa (a menos que essa pasta ou uma acima dela já seja confiável), para a pergunta “Do you trust this folder?” do Claude Code não travar uma sessão criada pelo Telegram: a primeira mensagem responderia com “No, exit”. As configurações .claude/, os hooks de .git/ e o .mcp.json de uma pasta confiável podem rodar comandos; por isso membros sem can_manage nunca podem escrever esses arquivos (a menos que o policy.yaml devolva um shell à função deles). O Claude Code estende a confiança de uma pasta às pastas abaixo dela que não estão dentro de um repositório git; um repositório continua recebendo a própria pergunta. A pasta pessoal é marcada quando uma sessão começa nela, já que o Claude Code guarda um sim dado na pasta pessoal só para aquela sessão. Depois disso, o Claude Code não pergunta nada numa pasta abaixo da sua pasta pessoal que não esteja dentro de um repositório git, inclusive nas sessões que você inicia por conta própria. / e as pastas acima da pasta pessoal nunca são marcadas: isso alcançaria todas essas pastas do computador, então uma sessão iniciada numa delas para na pergunta. Ao iniciar, o aipager não marca nada, só a pasta onde uma sessão está para começar. Uma sessão nova começa na pasta onde o aipager roda (a pasta pessoal com aipager service install no Linux e no macOS, e no contêiner) ou em AIPAGER_WORK_DIR quando ele está definido; o log do aipager avisa ao iniciar quando essa pasta é / ou uma pasta acima da pasta pessoal e não é confiável. As versões anteriores marcavam a pasta do aipager a cada início, inclusive / num serviço do macOS (que rodava em /); o aipager não consegue distinguir essa entrada de uma que você criou, então a deixa como está. O aipager não mexe em ~/.claude.json nem em ~/.claude/settings.json quando não consegue lê-los inteiros, ou quando um Claude em execução os mudou enquanto o aipager os atualizava.
  • Sempre permitir, que devolve ao Claude Code a regra de “don't ask again” que ele ofereceu junto com o pedido, exatamente como essa opção faria na caixa de diálogo dele.

Para bloquear uma ferramenta para todo mundo, negue-a em ~/.claude/settings.json; para bloquear para algumas pessoas, use as regras da função delas no policy.yaml (veja uso em equipe).

O que um pedido mostra é o que roda. Um pedido de permissão mostra o comando ou o caminho de verdade embaixo da descrição do Claude. Os controles de direção do Unicode (U+202A-U+202E, U+2066-U+2069, U+200E, U+200F, U+061C) mudam a ordem em que o Telegram, um navegador ou um celular desenham o texto que vem depois deles, enquanto o shell roda os caracteres em ordem, então eles poderiam fazer um comando parecer outro (o truque “Trojan Source”, CVE-2021-42574). Em cada pedido, pergunta, prévia de diff e registro de auditoria, eles aparecem como marcas visíveis (⟨U+202E⟩), e o pedido avisa que o comando pode rodar algo diferente do que parece. Os outros caracteres invisíveis ficam como estão.

Como as regras valem no uso em equipe#

No uso em equipe, o aipager acrescenta uma camada própria por baixo dos toques: cada mensagem que vem do Telegram leva uma nota de permissões (quem mandou e o que a função dessa pessoa permite) escrita em /tmp/claude-notes-<session>/, um arquivo por mensagem. Quando o Claude pega uma mensagem, o ajudante aipager-hook a casa com essas notas e instala as regras de quem mandou onde a verificação PreToolUse as lê. Estas propriedades são a base:

  • Ganha a regra mais rígida. Se o Claude pegar mensagens de mais de uma pessoa numa mesma tarefa (o aipager segura mensagens para evitar isso, mas se acontecer mesmo assim ele continua seguro), a tarefa roda com a combinação mais restritiva; a mensagem de um dono nunca pode emprestar as permissões dele à mensagem de outra pessoa.
  • Uma tarefa em andamento nunca se amplia. Uma mensagem de outra pessoa espera a tarefa em andamento terminar e depois roda como uma tarefa própria, com as regras de quem a mandou. Se mesmo assim uma entrar numa tarefa em andamento, a tarefa fica com as regras mais rígidas de todos que estão nela; uma mensagem do Telegram que entra numa tarefa que você digitou no terminal deixa o resto dessa tarefa com as regras de quem a mandou (a de um dono não muda nada).
  • Sua função é a do chat em que você age. Uma pessoa que está em vários chats (por exemplo user num grupo e owner do próprio chat privado) tem a função do grupo para tudo o que manda ou toca no grupo, e a do chat privado no chat privado, seja qual for a ordem dos chats no aipager.yaml.
  • Desconhecido quer dizer restrito. Uma mensagem que não dá para atribuir a ninguém roda com o piso próprio (sem isenções, com todas as regras de negação ativas), nunca como uma mensagem de terminal sem restrições. As mensagens que você digita direto no terminal continuam sem restrições; qualquer coisa com a marca do Telegram em qualquer linha fica sujeita às regras. Cada mensagem do Telegram leva essa marca, com o rótulo de quem mandou essa mensagem (nunca de quem controlou a sessão da última vez); alguém que o aipager não conhece, por exemplo alguém tirado do aipager.yaml enquanto a mensagem dele esperava, recebe um [via Telegram] sem nome e o piso.
  • Os comandos de barra precisam de um admin. Um comando de barra é digitado no Claude do jeito que é, sem marca, e um comando ou skill como /deliver roda uma tarefa inteira a partir do próprio texto. Então um membro cuja função não está isenta das regras de função (os user e read_only próprios, as funções que você define sem bypass_role_denies, e qualquer pessoa que o aipager não conheça) só pode mandar os comandos do teclado de Comandos do aipager (mais /compact) e trocas de modelo (/model <name>); qualquer outro comando de barra, digitado, mandado a uma sessão com /<name>, como legenda de um arquivo, como primeira mensagem do /new ou como template, é recusado com “Esse comando é só para admins.” e um 🤷. Donos e admins mandam qualquer um. Um comando de barra que chega ao Claude pelo Telegram continua sujeito às regras: o hook vê que a tarefa pegou uma mensagem do Telegram que mandou esse comando e o prende às regras de quem a mandou. Um comando de barra que você digita no terminal continua sem restrições, a não ser que o mesmo comando (com qualquer argumento) tenha sido mandado antes pelo Telegram nessa sessão: o aipager não consegue diferenciar a sua cópia do Claude Code entregando de novo a do Telegram, então a sua roda com as regras da última tarefa do Telegram. Isso acaba quando a sessão termina.
  • Uma mensagem sem remetente novo mantém as regras da tarefa. O Claude Code também informa mensagens que nenhuma mensagem do Telegram explica enquanto uma tarefa do Telegram está em andamento: uma mensagem digitada no terminal e na fila atrás da tarefa (informada no momento em que entra na fila), uma mensagem de outra sessão local do Claude, ou a mesma mensagem do Telegram entregue de novo depois de compactar. Uma mensagem assim não acrescenta nenhum remetente do Telegram, então a tarefa mantém as regras com que estava, mais rígidas por qualquer mensagem que ainda espera para ser pega e nunca mais frouxas. Uma mensagem com texto do Telegram que o aipager não consegue atribuir continua caindo no piso próprio, e uma mensagem de alguém com menos permissões continua baixando a tarefa para as regras dessa pessoa.

O que de fato contém um usuário restrito. A sessão do Claude roda com o mesmo usuário do sistema que é dono da configuração do aipager (o token do bot em ~/.config/aipager/aipager.yaml, a credencial do Claude em ~/.config/aipager/daemon.env) e do arquivo de regras de cada tarefa (/tmp/claude-policy-<session>.json). Um shell rodando com esse usuário consegue chegar aos dois por jeitos de escrever que nenhum padrão prevê (sed -i … /tmp/*-policy-*.json, um laço for sobre um glob, uma linha de Python) e reescrever as próprias regras como as do dono. Por isso os padrões de comandos são um filtro, não um limite. Para uma tarefa que roda com as funções restritas próprias (user, read_only), e para uma tarefa que o aipager não consegue atribuir a ninguém, o aipager aplica estas regras, e nenhuma delas depende de padrões de comandos (veja os limites conhecidos abaixo para o que elas não cobrem):

  • Sem shell. Elas não podem usar o Bash, nem nenhuma outra ferramenta com que o Claude Code roda código (PowerShell, Monitor, REPL, Workflow, os agendadores de mensagens CronCreate e RemoteTrigger, …; a lista é safety.CODE_EXECUTION_TOOLS). Uma mensagem agendada chegaria sem a marca do Telegram, ou seja, sem restrições, então essas também contam. Pelo mesmo motivo elas não podem usar SendMessage (ele pode passar uma mensagem para outra sessão local do Claude, que a rodaria como uma mensagem de terminal), nem EnterWorktree/ExitWorktree (eles mudam a pasta de trabalho da sessão, na qual a regra seguinte se apoia).
  • As escritas ficam no projeto. Write, Edit, MultiEdit e NotebookEdit só escrevem dentro da pasta da sessão (a pasta de trabalho que o Claude Code informa no conteúdo do hook, nunca um caminho da própria chamada da ferramenta) e do scratchpad do Claude Code da sessão (/tmp/claude-<uid>/<project>/<session id>/, ou /tmp/claude-<uid>/ quando não dá para descobrir). Todo o resto é negado: os arquivos de início do seu shell, ~/.ssh, outros projetos, os arquivos do aipager em /tmp. Dentro da pasta também são negados .claude/, .git/ e .mcp.json, porque cada um faz o Claude Code ou o git rodar um comando. Os links simbólicos são seguidos antes da decisão. Uma sessão cuja pasta é a sua pasta pessoal, / ou uma pasta acima da sua pasta pessoal não tem pasta de projeto: ali uma tarefa restrita só escreve no scratchpad dela, e uma escrita em qualquer outro lugar é negada com “esta sessão roda na pasta pessoal (ou numa pasta acima dela), e esta função só pode escrever dentro de uma pasta de projeto”. Para os membros restritos não ficarem presos ali, o /new e o Mini App não os deixam iniciar uma sessão nessas pastas (a pasta do próprio aipager, a padrão para uma sessão nova, é a sua pasta pessoal sob o serviço em segundo plano); eles podem criar uma pasta nova dentro da sua pasta pessoal e começar nela (uma nova: não uma pasta que você já tem ali, e não bin nem node_modules, de onde outros programas carregam código).
  • Os membros ficam na pasta raiz deles. Uma função sem can_manage (os user e read_only próprios) trabalha dentro da pasta raiz do chat, que você define no aipager config (padrão ~/aipager-users/<label>; ~/aipager-users e a pasta são criadas com permissões 0700). O /new, Continuar, Substituir, Reiniciar, a troca de modo e o Mini App só iniciam ou reiniciam a sessão de um membro assim dentro dela. O hook de segurança aplica isso por qualquer caminho que a mensagem chegue (Telegram, Mini App, a mensagem de outra sessão): a pasta raiz vai junto na cópia das regras da tarefa, nos recibos das mensagens entre sessões e nas permissões fixadas e agendadas, e quando membros com pastas raiz diferentes dividem uma tarefa, ganha a mais estreita (duas que não estão uma dentro da outra deixam só a pasta da própria sessão). As escritas só vão para a pasta da sessão quando ela está dentro da pasta raiz (uma sessão fora dela, como uma iniciada antes de existirem as pastas raiz, continua rodando, mas as tarefas dela só escrevem no scratchpad: “esta sessão roda fora da pasta desta função”), e Read, LSP, Grep e Glob não chegam a nada da sua pasta pessoal fora da pasta raiz (“na pasta pessoal, esta função só lê dentro da própria pasta”), fora o scratchpad da sessão e o próprio arquivo de resposta dela. A pasta raiz de qualquer outro chat é um caminho protegido para o membro, esteja onde estiver, e o mesmo vale para a pasta de arquivos enviados de qualquer outro chat em /tmp/aipager-files/ (Read, Grep, Glob e as ferramentas de escrita; para uma função com Bash, um comando que cite uma delas é recusado, numa regra de melhor esforço), junto com os arquivos que todos os chats dividiam ali antes das pastas por chat. Os arquivos enviados no próprio chat do membro continuam legíveis. As pastas fora da sua pasta pessoal (/usr, /etc, /srv) continuam legíveis. Uma função personalizada com bypass_role_denies ou bypass_safety mas sem can_manage escreve e procura em qualquer lugar dentro da pasta raiz dela, e as leituras dela na sua pasta pessoal também ficam dentro dela: só donos e admins não têm restrições. Com bypass_safety, numa sessão cuja pasta está fora da pasta raiz dela, ela só escreve e procura no scratchpad, como um user, e assim que uma das ferramentas dela é negada, o resto da tarefa para.
  • As buscas ficam no projeto. Um Grep ou Glob só é permitido quando tudo isto vale: a pasta em que ele procura (o path dele, ou a pasta da sessão quando ele não tem, seguindo links simbólicos) está dentro da pasta da sessão (não quando ela é a sua pasta pessoal, / ou uma acima da sua pasta pessoal, igual nas escritas) ou do scratchpad; essa pasta não é um caminho protegido e não contém nenhum (uma sessão iniciada na sua pasta pessoal contém ~/.config/aipager, então não pode procurar a partir da raiz dela); cada glob é um padrão relativo simples: sem /, ~, $ ou letra de unidade no começo, sem .., sem # (um comentário para o ripgrep) nem ! (uma negação), sem escape com \; e você não tem nenhuma regra de caminho protegido sem / nem ~ no começo (**/.env pode estar em qualquer lugar do projeto, então com uma assim toda busca restrita é negada). Todo o resto é negado. Isto é uma lista de permitidos de propósito: três rodadas de revisão acharam, cada uma, um jeito novo de escrever um glob que levava o ripgrep para fora da pasta que um modelo da análise dele esperava, e uma delas leu o token do bot.
  • As ferramentas de arquivo respeitam os caminhos protegidos. Read, LSP e as ferramentas de escrita conferem para onde um caminho leva de verdade, links simbólicos incluídos. O único arquivo de controle que uma tarefa pode ler é o /tmp/claude-reply-<session>.txt da própria sessão, que o aipager cita na mensagem quando você responde a uma mensagem anterior; os arquivos de resposta de outras sessões, e qualquer escrita nele, continuam negados. No macOS, /tmp é /private/tmp e o disco não diferencia maiúsculas, então as duas formas, e qualquer combinação de maiúsculas, são conferidas.
  • Uma verificação que falha nega. Se decidir sobre uma ferramenta der erro (um byte NUL num caminho, um bug), ou o hook ficar sem memória enquanto decide, a execução é negada, a não ser que as regras da sessão possam ser lidas e deem a isenção do dono. Antes ela passava.

Os seus próprios caminhos protegidos. A seção safety: do policy.yaml (deny_paths_no_access, deny_paths_no_write, deny_bash_patterns) se soma aos caminhos protegidos e aos padrões de comandos próprios. Ela faz parte do piso que toda tarefa de toda função recebe, menos owner, admins incluídos, e vale para as ferramentas de arquivo, as buscas e os comandos do Bash exatamente como a lista própria. O aipager escreve a lista combinada em /tmp/claude-policy-.floor-<uid>.json (que também é um caminho protegido) para os casos de reserva do hook de segurança; se esse arquivo não existir, não puder ser lido ou for de outro usuário do sistema, o hook usa a lista própria, nunca menos. Uma mudança feita com uma recarga da configuração vale para as mensagens enviadas depois da recarga, não para uma tarefa já em andamento nem para uma mensagem que já está esperando. Até a 0.7.20 a seção era lida e o aipager doctor --safety-check a mostrava, mas ela não era aplicada.

Tarefas que o aipager não consegue atribuir. Uma tarefa do Telegram sem um remetente a quem prendê-la roda com o piso, que corresponde à função user própria (não às restrições extras que você acrescentar a user no policy.yaml) mais a sua seção safety:: sem ferramentas de rodar código, escritas e buscas limitadas, os caminhos protegidos. Isso acontece com uma mensagem cuja nota de permissões venceu (em 24 horas) ou não pôde ser escrita, e com uma mensagem que tem texto do Telegram com o qual nenhuma nota casa. O botão Tentar de novo roda com a função de quem o toca quando essa pessoa também mandou a mensagem que está sendo repetida, então o Tentar de novo de um dono na própria mensagem mantém o Bash; um Tentar de novo na mensagem de outra pessoa, ou numa cujo remetente o aipager não conhece, roda com o piso, para o texto de ninguém pegar emprestadas as permissões de quem tocou.

As leituras fora dos caminhos protegidos são permitidas para uma função que não fica presa a uma pasta raiz (uma função personalizada com can_manage mas sem isenções): ela pode ler outros projetos do usuário do sistema. Os user e read_only próprios não leem nada da sua pasta pessoal fora da pasta raiz deles (acima). Os seus arquivos de credenciais são caminhos protegidos para toda função sem bypass_role_denies (os user e read_only próprios, as funções que você define, e o piso): ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.password-store, ~/.pgpass, ~/.npmrc e ~/.pypirc não podem ser lidos, escritos nem pesquisados. Eles são um padrão da função, então o deny_paths_no_access próprio de uma função no policy.yaml os substitui para essa função, menos quando outra mensagem ou uma mensagem digitada no terminal entra numa tarefa em andamento: a partir daí até a tarefa terminar, ela volta a ficar presa a eles (ganha a regra mais rígida). Um arquivo de credenciais em outro lugar (um .env num projeto, um token em outra pasta) não está coberto: acrescente-o ao deny_paths_no_access da função. Os donos não ficam presos a nada disso. As escritas dos admins não são limitadas. O Grep/Glob deles pode procurar em qualquer pasta que não chegue a um caminho protegido: uma busca é negada quando a pasta em que ela procura (o path dela, ou a pasta da sessão quando não tem; para um Glob, também a pasta que o padrão dele cita, como em /home/you/.config/aipager/*, ou a pasta acima de um padrão sem curinga, na qual o Claude Code procura esse nome em qualquer profundidade), com os links simbólicos resolvidos, é uma pasta protegida, está dentro de uma ou contém uma. Assim, Grep em ~, /, na pasta acima da sua pasta pessoal ou em /tmp é negado (elas contêm ~/.config/aipager, ~/.claude ou os arquivos /tmp/claude-* do aipager), enquanto Grep num projeto ou em /tmp/myproj não. Uma regra de caminho protegido sem / nem ~ no começo (como **/.env) pode casar em qualquer pasta, então com ela nas regras de um admin (ou na seção safety:, que toda função recebe menos o dono) toda busca do admin é negada, como para as funções restritas; o aipager doctor alerta sobre uma assim na seção safety:. Os admins têm Bash, então as regras deles continuam de melhor esforço (abaixo).

Limites conhecidos. Isto não está coberto:

  • Se o próprio hook falhar antes de começar a decidir (por exemplo não consegue ler o próprio arquivo de status, ou fica sem memória ao iniciar), o Claude Code trata isso como “permitir”. Um usuário restrito não consegue causar isso; qualquer erro enquanto decide nega.
  • As ferramentas que não são de arquivo mas conseguem mandar dados para fora não são conferidas por caminho: WebFetch (tudo o que a tarefa consegue ler ela consegue mandar para uma URL) e as outras ferramentas de entrega do Claude Code. Negue-as para uma função no policy.yaml se isso importar para você. O SendUserFile, que envia arquivos para o chat, é conferido (veja arquivos que o Claude envia).
  • Um agente em segundo plano que uma tarefa restrita iniciou continua rodando depois que a tarefa termina, e as ferramentas seguintes dele são julgadas pela mensagem mais recente da sessão: uma mensagem posterior do dono ou do terminal tira as restrições dele.
  • Uma função que você define com can_manage mas sem nenhuma isenção não fica presa a uma pasta raiz: ela ainda pode começar em qualquer pasta de projeto em que o chat dela já trabalha (uma pasta que uma sessão anterior usou), incluindo uma sua, e ler seus outros projetos.
  • Uma tarefa à qual nenhum membro pode ser preso (um remetente desconhecido, um membro tirado, uma mensagem de outra sessão cujo remetente não pode ser verificado) roda com o piso restrito, que não tem pasta raiz. Nas leituras esse piso é mais fraco que um user próprio: uma tarefa assim pode ler qualquer arquivo da sua pasta pessoal que não seja um caminho protegido (seus outros projetos, por exemplo), enquanto a tarefa de um user só lê dentro da pasta raiz dele. Uma tarefa assim também pode ler os arquivos enviados de todos os chats em /tmp/aipager-files/, que a tarefa de um user só lê no próprio chat. As escritas dela continuam limitadas à pasta da sessão e ao scratchpad. Uma mensagem verificada da sessão de um membro leva a pasta raiz desse membro.
  • A pasta raiz só limita as ferramentas de arquivo. Uma função personalizada a que você dá Bash pode ler fora dela pelo shell (os user e read_only próprios não têm Bash). Uma função personalizada com bypass_safety mas sem can_manage mantém a isenção dela para tudo menos os caminhos das ferramentas de arquivo: nenhuma ferramenta ou padrão de comando é negado a ela.
  • Um motivo de negação que cita a pasta raiz de outro membro mostra o caminho dessa pasta ao Claude e no chat.
  • A tarefa de um membro que já estava em andamento quando as pastas raiz chegam (uma atualização) não tem nenhuma até a próxima mensagem desse membro.
  • Dentro do projeto, um usuário restrito pode mudar o que você roda ou lê depois (o código, o Makefile, os scripts do package.json, o conftest.py, um CLAUDE.md que as suas próprias tarefas carregam).

Uma função que tem Bash (admin, que está isenta das regras de negação de função, ou qualquer função a que o policy.yaml o devolva: allow_tools citando Bash, ou um deny_tools substituído) só fica presa a regras de melhor esforço: os padrões de comandos abaixo, que um usuário determinado consegue contornar. O aipager doctor alerta uma vez para cada função assim que um membro tenha. Dê Bash só a alguém a quem você daria um shell no computador.

As regras de comando próprias. Entre os padrões do Bash a que fica presa toda tarefa do Telegram com Bash que não é do dono, um bloqueia a palavra claude: ele para um claude aninhado (com qualquer caminho, wrapper ou opção) e qualquer acesso a ~/.claude com uma única regra, e também pega os nomes simples dos arquivos de controle /tmp/claude-* do aipager, cujos nomes têm o mesmo prefixo (um padrão não é uma proteção de verdade para esses arquivos: uma forma com glob passa). A única exceção é a pasta scratchpad que o Claude Code cria, /tmp/claude-<uid>/…: essa pasta exata, como um componente inteiro do caminho, está isenta, para os comandos rodados a partir do scratchpad de uma sessão não pararem. Ela é do usuário do sistema, não de uma sessão, então uma tarefa com Bash pode ler ali os scratchpads de outras sessões e a saída das tarefas delas, como a ferramenta Read pode. Um glob ou algo parecido com ela (/tmp/claude-1*, claude-1000x), outra pasta temporária, e tudo o que se alcança a partir dela (…/../../home/you/.claude) continuam bloqueados. As outras ocorrências da palavra (git log --grep=claude, um arquivo chamado claude-notes.md, pip show claude-agent-sdk) também continuam bloqueadas, de propósito: a mesma palavra dá nome ao executável, aos SDKs dele e aos arquivos de controle do aipager, e um padrão não consegue diferenciá-los. As regras que você acrescenta no policy.yaml veem o comando exatamente como foi digitado, scratchpad incluído.

Buscas pelo Bash. Algumas versões do Claude Code não têm a ferramenta Grep nem Glob (a 2.1.289 é uma delas), e o Claude então procura com grep, rg ou find pelo Bash, onde a regra de Grep/Glob acima não vale. Para toda função menos o dono, os padrões de comandos também negam uma busca ou listagem recursiva da pasta pessoal inteira ou de /: grep -r (-R, --recursive, -d recurse, egrep, fgrep, rgrep), rg (e rg --files), ag, ack (e ack -f), find, fd, tree, du e ls -R cuja pasta seja ~, ~user, $HOME, ${HOME} ou "$HOME", /home, /home/<user> (ou /home/$USER), /Users, /Users/<user>, /root ou / (com ou sem /, /. ou /* no final), ou ~/.config ou ~/.local, que contêm as pastas do aipager. Isso vale depois de cd x &&, num pipe, dentro de bash -c "...", depois de xargs, env ou timeout e depois de configurações como LC_ALL=C. A resposta diz para procurar dentro de uma pasta de projeto. As buscas numa pasta de projeto (grep -rn x ~/proj, rg x src/, find . -name y), as buscas dentro de um projeto POR um caminho pessoal (grep -rn "$HOME" src; a primeira palavra depois das opções é o texto procurado, não uma pasta) e as buscas não recursivas (grep x ~/.bashrc, ls ~) não são afetadas. Isto é de melhor esforço, como todo padrão de comando: um padrão vê as palavras, não onde o shell as roda, então não enxerga uma busca sem pasta, ou com . ou .., rodada a partir da pasta pessoal (ou abaixo dela) (cd ~ && grep -r x .), uma pasta pessoal alcançada por outra variável ou um glob, nem uma escrita por inteiro fora de /home, /Users e /root, nem \grep, nem uma pasta logo depois de uma palavra que termina em -e ou -f (ele a toma pelo texto que -e/-f dão ao grep e ao rg, então ag x -f ~ passa). Ele também não sabe quais opções levam um valor separado, então rg -t py "$HOME" . ou grep -rn -A 3 "/home/you" src lê py ou 3 como o texto e o caminho pessoal como pasta, e é negado (escreva -A3, --include=*.py, ou ponha a opção depois do texto); também um caminho pessoal dado a -e dentro de um grupo de opções (grep -rne "/home" src; -r -n -e está ok); um comando citado dentro do texto de outro comando (echo "a; rg x ~") também é negado, e du -sh ~ também (só tamanhos).

Por que um bloqueio para a tarefa inteira. Depois que uma ferramenta é bloqueada, toda ferramenta seguinte da mesma tarefa também é negada, até a próxima mensagem. Sem isso, o agente simplesmente tenta a mesma coisa de novo com outras palavras (um glob, outro comando, um caminho codificado) até uma forma passar pelos padrões. Parar a tarefa deixa a primeira tentativa cara; não transforma um padrão num limite, porque a primeira forma tentada pode ser justamente a que passa. O custo de um falso positivo é uma tarefa parada, e é por isso que as regras acima são mantidas estreitas.

Veja uso em equipe → como as regras funcionam para as regras que essas notas carregam, do jeito que você as vê.

Arquivos que o Claude envia#

Toda sessão que o aipager inicia tem a ferramenta SendUserFile do Claude Code (CLAUDE_CODE_ATTACHMENTS_BY_PATH=1), que lê arquivos e os entrega no chat da sessão (comandos). O hook PreToolUse verifica cada arquivo da chamada antes de o Claude Code lê-lo:

  1. O piso de envio, para toda tarefa, a do owner e uma mensagem digitada no terminal incluídas: a configuração e o estado do aipager (~/.config/aipager/**, ~/.local/share/aipager/**, ~/.local/state/aipager/** e seus arquivos de controle em /tmp), todo o ~/.claude/** e ~/.claude.json* (as credenciais e a conta do Claude), os arquivos de credenciais de que toda função restrita é mantida longe (~/.ssh/**, ~/.gnupg/**, ~/.aws/**, ~/.git-credentials, ~/.netrc e o resto dessa lista), /proc/**, /sys/**, /dev/**, e nomes com cara de segredo, onde quer que estejam (.env, .env.*, *.pem, *.key, id_rsa*, id_ed25519* e os outros nomes de chaves, *.p12, *.pfx, *.kdbx, *.jks, *.keystore, .pgpass, credentials, *credentials*.json). Vale o caminho como está escrito e para onde ele leva de fato, então um link simbólico para um deles também é recusado. Alguns arquivos inofensivos têm esses nomes (um *.key do Keynote, um .env.example) e também são recusados.
  2. O próprio arquivo: um arquivo comum depois de resolvidos os links simbólicos, com um só hard link, não vazio, de no máximo 50 MB.
  3. As regras da própria tarefa: o que a tarefa não pode ler (Read), e para um membro (uma função sem can_manage, presa a uma pasta raiz) a regra mais rígida de que cada arquivo, como está escrito e para onde leva de fato, fique dentro da pasta raiz. Diferente das leituras de um membro, não há exceção para arquivos fora da pasta pessoal (/tmp, /etc) nem para o scratchpad da sessão.

Uma recusa é uma recusa branda: o Claude lê o motivo ("SendUserFile refused, nothing was sent: ... Tell the person which file could not be sent and why.") e a tarefa continua; a sessão não para como acontece com uma ferramenta bloqueada. Uma verificação que falha recusa o arquivo, para o owner também. Cada arquivo recusado tem um registro FileRefused no registro de auditoria.

O aipager não acredita no socket de controle só pela palavra quando se trata de um arquivo: qualquer processo local pode escrever nele, citando qualquer sessão, chamada ou conversa salva, e basta o hook de uma chamada de ferramenta para uma sessão parada passar a contar como ocupada. Por isso uma chamada só conta durante uma tarefa que o aipager viu começar com um prompt (um que ele digitou a partir do Telegram, ou um UserPromptSubmit) e durante o trabalho em segundo plano dessa tarefa, até a sessão ficar parada sem nenhum trabalho rodando; nesse começo, o aipager anota a hora e a conversa salva que acompanha para a sessão (um UserPromptSubmit posterior da mesma tarefa cita a conversa de novo). Um comando de barra que o aipager digita não começa nenhuma tarefa (um que roda uma tarefa manda o próprio UserPromptSubmit), e uma tarefa que nunca começa (a sessão ainda parada sem trabalho 15 segundos depois) é fechada de novo. O Claude Code começa algumas tarefas sem UserPromptSubmit: uma mensagem enviada enquanto uma tarefa roda e que ele pega como a próxima tarefa, e a tarefa que um hook de Stop mantém rodando (/goal). Uma tarefa assim conta quando o próprio aipager digitou essa mensagem, ou assim que a conversa salva que ele anotou mostra o começo dela depois do último Stop (uma linha de fila dequeue, ou a linha Stop hook feedback: que o Claude Code grava); até lá nada da chamada é lido, por até 20 segundos. Ele pega a lista de arquivos do PreToolUse que verificou, ou do PostToolUse quando aquele se perdeu (depois de um reinício); nunca do resultado do Claude Code. Ele copia cada chamada uma única vez: um id de chamada que já copiou nunca é copiado nem enviado de novo (ele guarda os últimos 256 por sessão, mesmo depois de um reinício). Ele descobre uma única vez o caminho real de cada arquivo e o verifica de novo ali (o piso, as verificações de arquivo e a pasta raiz da sessão), abre exatamente esse caminho sem seguir link, garante que o arquivo aberto é o que verificou e que continua nesse caminho, e o copia para ~/.local/state/aipager/outbox/<chat>/ (pasta 0700, arquivo 0600), um lugar de que toda tarefa restrita já é mantida longe. Antes de enviar, procura a chamada na conversa salva que anotou para a tarefa (para a chamada de um subagente, na conversa do próprio subagente, ao lado), nunca numa que o datagrama de uma chamada de ferramenta cite: uma linha do assistente com um tool_use de SendUserFile com esse id e exatamente esses arquivos, escrita a partir da mesma pasta e não antes do começo da tarefa, num arquivo .jsonl em ~/.claude/projects (ou $CLAUDE_CONFIG_DIR/projects, como está definido para o próprio aipager) cujo dono é o próprio usuário do sistema do aipager. O Claude Code grava a conversa salva com atraso, então o aipager espera a chamada por até 20 segundos, e enquanto o trabalho da sessão continua rodando (um agente em segundo plano, uma chamada de ferramenta) ele guarda os arquivos e procura de novo mais tarde. Uma chamada que ele ainda não encontra quando nada mais roda não é enviada e as cópias dela são apagadas; uma chamada que chegou quando nenhuma tarefa assim rodava, ou que chegou pela segunda vez, nem chega a ser copiada. Cada arquivo delas ganha um registro FileFailed no registro de auditoria com o motivo unverified. O chat recebe uma linha para eles (⚠️ Não foi possível enviar report.pdf: o aipager não conseguiu confirmar que o Claude o enviou nesta tarefa.) só quando a própria conversa salva do Claude mostra essa mesma chamada durante a tarefa, e a linha cita os arquivos que essa linha da conversa lista, então uma chamada fingida não consegue pôr nem uma linha nem um nome no chat; fora isso, nada vai para o chat. Só a cópia é enviada, só para o chat da sessão, depois da resposta da tarefa, depois dos arquivos anteriores da sessão e sem segurar a próxima tarefa; ela é apagada assim que é enviada, ou depois de um dia.

O que essas verificações não cobrem: um processo que roda como o próprio usuário do sistema do aipager pode escrever as conversas salvas do Claude tanto quanto o socket (ele já pode ler esses arquivos por conta própria). Um processo de outro usuário que alcança o socket também pode fingir um UserPromptSubmit, que para essas verificações começa uma tarefa e cita a conversa salva dela; depois pode fazer o aipager enviar para o chat dessa sessão um arquivo que uma chamada real de SendUserFile citou, se essa chamada for gravada nessa conversa a partir da mesma pasta depois desse momento (por exemplo, uma que outra sessão está fazendo bem nessa hora), uma vez por chamada. Para isso ele também precisa mandar o id dessa chamada em andamento (tool_use_id), que outro usuário não consegue ler: ele fica nas conversas salvas sob o ~/.claude do usuário do aipager, e o hook o recebe pela entrada, não pela linha de comando. Um Stop fingido encerra a tarefa para essas verificações, então os arquivos que essa tarefa enviar depois não são enviados. Uma sessão iniciada com um $CLAUDE_CONFIG_DIR diferente do do aipager, ou uma tarefa cujo UserPromptSubmit o aipager perdeu (digitada no terminal enquanto o aipager reiniciava), não envia arquivos. Uma tarefa que o Claude Code começa sem hook de outro jeito (os dois acima são os conhecidos) também não envia arquivos; um arquivo que a conversa salva do Claude mostra que ele pediu ali recebe a linha acima. O socket fica em $XDG_RUNTIME_DIR, que outros usuários não alcançam, quando ele está definido; sem ele, fica em /tmp.

Registro de auditoria#

Cada toque em Permitir ou Negar, mais cada resposta a um AskUserQuestion, acrescenta uma linha JSON a ~/.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
}

Campos (veja aipager/audit.py para a lista completa):

  • ts: data e hora ISO 8601 em UTC, com precisão de segundos.
  • session / label: o nome interno e o nome visível.
  • action: o que aconteceu (Allowed, Denied, Allowed always, uma resposta a AskUserQuestion, uma negação automática, …).
  • tool / summary: o nome da ferramenta e uma entrada cortada ou o texto da pergunta.
  • user_id / username: quem tocou, para saber quem foi no uso em equipe; scope_label / scope_chat_id quando há vários chats configurados.
  • denied: true para qualquer negação, por toque ou por regra.

A escrita é de melhor esforço. Se o disco encher ou ~/.claude/ deixar de aceitar escrita, o aipager escreve um WARNING no log e continua rodando: nem perda silenciosa, nem queda. Veja aipager/audit.py.

O registro de auditoria no disco só cresce. Use-o junto com a resposta de auditoria no chat (uma mensagem do Telegram por decisão, embaixo do cartão de atividade) para ter dois registros independentes.

Limite de permissões#

O aipager nunca pega permissões a mais. Nem sudo, nem setuid, nem doas. Cada arquivo que ele escreve fica na sua pasta pessoal, em /tmp (os arquivos de controle de cada sessão, os downloads e o log do reinício) ou na pasta do socket de controle dele ($XDG_RUNTIME_DIR); a lista completa está em arquitetura. Cada subprocesso (claude, dtach, os instaladores, claude update) roda com o usuário do aipager.

Instalar o recurso de voz pelo Telegram ([📦 Instalar recurso de voz]) passa pelo instalador que é dono do aipager (uv tool install --reinstall aipager[voice], pipx install --force aipager[voice]), e senão instala os pacotes do recurso com o próprio Python do aipager (<python> -m pip install): sempre no ambiente do próprio aipager, nunca no Python do sistema.

O botão [🔄 Reiniciar o aipager], sob o serviço do systemd, agenda systemctl --user restart aipager.service num serviço de usuário temporário à parte; no macOS roda launchctl kickstart -k; um aipager que você iniciou por conta própria lança uma cópia separada de si mesmo que espera o antigo terminar. Depois de uma atualização, só o serviço do systemd se reinicia sozinho (veja comandos → atualizar). Todos rodam com o mesmo usuário; nenhuma permissão a mais é pega.

Conexões de rede#

O aipager não escuta em nenhuma porta TCP que não seja loopback. O servidor do Mini App só abre em 127.0.0.1 (fixo no código; de propósito não há opção de host) e de fora só se chega a ele pelo túnel gerenciado descrito abaixo. Conexões de saída:

  • Long-poll HTTPS para api.telegram.org (receber as atualizações do bot).
  • HTTPS para telegram.org para o script do SDK do Mini App (telegram-web-app.js), baixado uma vez quando o servidor do Mini App inicia e renovado no máximo uma vez por dia; guardado em ~/.local/share/aipager/webapp-sdk/. O aipager serve esses bytes à página pelo próprio endereço e nunca os executa. O aipager não redistribui o script: nada vem dentro do pacote, e quando o aipager não tem cópia, a página o carrega direto do telegram.org, como antes.
  • O túnel do Mini App para o Cloudflare, enquanto ligado (o padrão). O executável cloudflared que ele usa é baixado uma vez das versões publicadas em github.com/cloudflare/cloudflared, conferido com um SHA-256 fixo e guardado em ~/.local/share/aipager/cloudflared/; um cloudflared que esteja no seu PATH nunca é usado. Nada disso acontece com a sua própria URL para o Mini App (veja abaixo).
  • A verificação de versões, só quando alguém toca em Verificar atualizações ou Atualizar (/update, as Atualizações do Mini App), ou quando alguém que pode atualizar abre a aba Configurações do Mini App (no máximo uma vez a cada dez minutos por app aberto): pypi.org para o aipager, e downloads.claude.ai ou registry.npmjs.org para o Claude Code, dependendo de como foi instalado. Depois uma atualização roda o instalador (que fala com o PyPI ou o Homebrew) e claude update. O aipager update na linha de comando só roda o instalador.
  • A instalação do recurso de voz, quando você toca no botão dele: o instalador baixa os pacotes do PyPI, e o modelo de voz é baixado do Hugging Face (huggingface.co) no primeiro uso.
  • HTTPS para raw.githubusercontent.com (o arquivo da chave dos relatórios) e para o servidor de recebimento do Sentry, só quando você confirma o envio de um relatório de problemas: o relatório que você viu na prévia e um resumo curto feito a partir dele, nada mais. Veja Relatórios de problemas.

Conexões de entrada:

  • Socket datagram do Unix em $XDG_RUNTIME_DIR/aipager.sock (ou /tmp/aipager.sock quando $XDG_RUNTIME_DIR não está definido). O aipager o abre e define as permissões dele ao iniciar (aipager/dtach/hook_receiver.py). Permissões 0o666 para qualquer processo local poder mandar eventos de hook para ele: a mesma confiança que o ~/.claude/settings.json, que já decide o que roda os hooks.

Isso quer dizer que o aipager não pode ser atacado a distância. Um atacante na rede não consegue chegar a ele sem já ter um pé no computador.

O texto do Claude e os downloads que o próprio Telegram faz. Os servidores do Telegram baixam sozinhos alguns endereços de uma mensagem: um bloco de imagem, vídeo ou áudio numa mensagem com formatação rica, e a página por trás de uma prévia de link. As respostas do Claude, as notas dele no cartão de atividade, as descrições dos agentes e os argumentos das ferramentas chegam ao Telegram como o Claude os escreveu, então uma injeção de prompt poderia fazer o Claude escrever um endereço que leva um segredo para fora, sem nenhuma chamada de ferramenta e sem nenhum pedido de permissão. O aipager fecha o primeiro caminho, e o segundo onde o Telegram tem uma opção para isso:

  • Em toda mensagem com formatação rica, uma imagem escrita como ![alt](url) é enviada como um link simples com o texto alternativo dela, e qualquer outra forma de escrever uma imagem, e as tags <img>, <video>, <audio>, <source>, <picture>, <figure>, <iframe>, <embed>, <object>, <tg-document>, <tg-collage>, <tg-slideshow>, <tg-button> e <tg-button-row> escritas direto, aparecem como texto (aipager/bot/media_guard.py). Dentro de um bloco de código, como um trecho de HTML, isso mostra um \ ou um &lt; a mais.
  • As prévias de link ficam desligadas em toda mensagem de texto simples e HTML que o aipager envia ou edita (incluindo as versões em texto simples das respostas e dos cartões), também nas mensagens do próprio aipager. Os links dele para o Mini App são botões, que o Telegram não baixa.

O Telegram não documenta nenhuma opção de prévia para as mensagens com formatação rica; se ele monta uma prévia de link para uma delas não foi conferido no serviço de verdade.

O túnel do Mini App#

O Mini App vem ligado por padrão, então numa instalação normal o aipager abre este túnel sem ninguém pedir. É uma escolha consciente (uma opção que ninguém descobre é como se não existisse), mas quer dizer que uma instalação nova se conecta à internet. Desligue com aipager miniapp disable, ou ponha enabled: false embaixo de miniapp: no aipager.yaml; um false explícito é sempre respeitado. Note que um bloco miniapp: ausente ou quebrado agora volta para LIGADO, não para DESLIGADO.

O Mini App (a não ser que você defina outro endereço com --url) inicia um túnel rápido do Cloudflare gerenciado que aponta para o servidor loopback do próprio aipager, e publica o endereço público https://*.trycloudflare.com que receber como botão de menu do Telegram e resposta do /app.

Esse endereço não é secreto, nem pretende ser: cada pedido que o Mini App atende é conferido com a assinatura initData do Telegram (veja Limite de confiança acima), a mesma verificação que o protege não importa como a URL foi obtida. Saber o nome do host sozinho não dá a um atacante nada que ele não conseguiria adivinhando ao acaso um subdomínio de trycloudflare.com; a porta de verdade é a verificação da assinatura. Trate-o como qualquer outra URL que não está publicada mas também não é secreta: pode ficar rodando, mas não a cole sem motivo num canal público.

O nome do host só fica na memória; ele nunca é escrito no aipager.yaml nem em nenhum lugar dentro de ~/.config/aipager/. Um reinício planejado (o de uma atualização, Reiniciar agora, o reinício no lugar sem serviço) mantém o túnel rodando, então o endereço, o botão do menu e um Mini App aberto continuam funcionando: o próximo aipager só o assume depois de conferir que é o mesmo boot, a mesma porta, que o processo é aquele cloudflared (a linha de comando dele), que ele ainda informa o mesmo endereço (~/.claude/aipager-tunnel.json guarda um hash dele, nunca o endereço) e que o endereço responde. Qualquer outra coisa o para e inicia um novo, e um túnel mantido que nenhum aipager assume em 3 minutos é parado por um pequeno processo guardião. Qualquer outra parada (Ctrl+C, docker stop, systemctl stop) para o túnel, e reiniciar o computador ou o contêiner dá um endereço novo. MINIAPP_KEEP_TUNNEL=0 desliga essa manutenção do túnel. Se o aipager for morto na hora (sem memória, kill -9), o túnel continua rodando até o aipager iniciar de novo com o túnel gerenciado ligado (o Mini App ativado, sem MINIAPP_PUBLIC_URL) e assumi-lo ou pará-lo, ou até o computador reiniciar; aipager uninstall também o para. Se o túnel cair no meio do funcionamento, o aipager o inicia de novo com esperas crescentes e publica o endereço novo; se ele não voltar depois de várias tentativas, o botão é tirado em vez de ficar apontando para uma porta morta.

Definir MINIAPP_PUBLIC_URL (ou aipager miniapp enable --url https://…) desliga de vez o túnel gerenciado (nenhum cloudflared é baixado nem iniciado) e a URL dada é usada como está, com a mesma verificação de initData por baixo. A detecção automática do Tailscale (tailscale status --json) continua disponível como opção mais simples para quem já usa o Tailscale e prefere não depender do Cloudflare.

Transformar áudio em texto#

O faster-whisper roda dentro do próprio processo. O áudio é baixado como .ogg na pasta do próprio chat em /tmp/aipager-files/ (onde ficam os arquivos enviados, 0700, o arquivo 0600), vira texto neste computador usando a CPU, e o arquivo fica sob o seu controle. Nenhum áudio sai do computador. Nenhuma API de terceiros. Não precisa de nenhuma chave além do token do bot para falar com o Telegram.

Se você apagar o .ogg depois de transformá-lo em texto, o único registro em texto da mensagem é o texto que é digitado na sessão do claude (que segue as regras de privacidade do próprio Claude Code).

Isolamento entre sessões#

Cada sessão do Claude Code roda no próprio dtach. O socket de controle em /tmp/claude-dtach-<name>.sock é do usuário do aipager; o dtach não deixa outro usuário se conectar. Cada sessão começa na própria pasta: a escolhida com o /new ou o Mini App, ou a pasta em que você rodou aipager session.

O aipager não isola as sessões entre si no nível do sistema de arquivos: uma sessão ligada a ~/projects/foo em princípio pode ler ~/projects/bar se as permissões do Claude Code deixarem. Para um isolamento mais forte, use configurações ~/.claude/settings.json por projeto ou um contêiner (imagem do Docker).

Mensagens entre sessões do Claude#

O Claude Code deixa uma sessão mandar uma mensagem para outra (a ferramenta SendMessage dele). Na sessão que recebe, essa mensagem é uma mensagem como qualquer outra, então o aipager decide as permissões dela do mesmo jeito que decide as de uma mensagem do Telegram: por quem a mandou de verdade, nunca pelo que ela diz.

  • A mensagem leva as permissões de quem a manda, nunca mais. Quando o aipager deixa uma sessão mandar uma mensagem, ele escreve um recibo de uso único: uma impressão digital do texto exato, o processo do Claude que manda (o número do processo e, no Linux, a hora de início) e as permissões que essa tarefa tinha. O Claude Code registra a mensagem recebida com o número de processo do remetente, que o kernel verificou, sem nunca confiar no nome que o remetente dá a si mesmo. Antes de a tarefa que recebe rodar qualquer ferramenta, o aipager casa os dois e dá à tarefa exatamente as permissões da tarefa que mandou: a mensagem de um dono roda com as permissões do dono, a de um membro com as desse membro, e uma mensagem digitada no terminal da sessão que manda, sem restrições.
  • Sem recibo, permissões restritas. Uma mensagem que o aipager não consegue casar (mandada por uma sessão ou um processo do Claude que o aipager não roda, um recibo que venceu depois de uma hora, um remetente em outro computador) roda com as regras mais rígidas mais nenhuma mudança de arquivos e nenhum acesso à web (só ler e pensar). As permissões anteriores da própria sessão que recebe nunca valem para ela.
  • O mesmo para outras entradas de fora da sessão. Tudo o que o Claude Code registra como não vindo da pessoa desta sessão recebe essas mesmas permissões restritas: um aviso de um canal MCP, uma chamada remota, uma mensagem que um plugin coloca na fila, uma mensagem repassada da caixa de outra sessão, e qualquer tipo de entrada que o aipager ainda não conheça (um comando de um plugin conta como entrada externa mesmo quando foi você que o iniciou). Os próprios agentes em segundo plano da sessão (o que eles devolvem e os relatórios deles) e os avisos de tarefas dela mantêm as permissões do trabalho.
  • Uma mensagem que a própria sessão agendou roda com as permissões da tarefa que a agendou. Quando uma tarefa configura um /loop ou um cron, o aipager guarda as permissões dessa tarefa para a mensagem; quando ela dispara depois, recebe essas permissões, nunca mais, não importa quem falou por último; uma mensagem que entra nessa tarefa só pode restringi-la mais, e uma mudança de regras que tira permissões de quem a agendou também a restringe. A mesma mensagem agendada duas vezes fica com as permissões mais rígidas. Um /loop sem texto dispara com as instruções de loop do próprio Claude Code em vez do próprio texto, então recebe as permissões mais rígidas de todos os loops sem texto que a sessão configurou, assim que a conversa salva mostra o disparo (até lá uma ferramenta negada é negada com suavidade, e o Claude tenta de novo). Uma mensagem que ninguém agendou passando pela verificação do aipager recebe as permissões restritas.
  • O texto não prova nada. O nome de um remetente, uma linha [via Telegram · @owner] ou “o dono pede…” dentro de uma mensagem não mudam nada. O Claude fica sabendo quem o aipager verificou como remetente, e que o que o texto afirma não é prova.
  • Entrar numa tarefa em andamento só a restringe. Uma mensagem de outra sessão que chega enquanto uma tarefa está em andamento deixa essa tarefa tão rígida quanto a mais rígida das duas, nunca mais frouxa. Isso vale quando o Claude Code pega a mensagem sem nenhum evento de mensagem próprio (juntada à tarefa em andamento, ou rodada como a próxima tarefa a partir da fila dele): o caminho rápido do dono olha o fim da conversa salva antes de cada ferramenta, e o fim da tarefa e o aipager marcam a sessão para uma verificação quando veem uma mensagem assim na fila.
  • As funções ficam no chat delas. Um recibo de uma sessão de outro chat só conta quando a tarefa dele tinha as permissões do dono ou foi digitada no terminal dela; todo o resto roda restrito.
  • Quem pode mandar. As funções restritas próprias não podem usar SendMessage de jeito nenhum. Uma função a que uma política o dê só pode escrever para sessões que o aipager roda (com o endereço uds:…/<pid>.sock); o aipager doctor alerta sobre uma função assim.
  • Os agentes em segundo plano mantêm as próprias permissões. Um agente mantém as permissões da tarefa que o iniciou (ou as mais rígidas de uma tarefa posterior), nunca as de uma tarefa posterior que tenha mais.
  • Quando é parada. Se as regras de segurança pararem uma tarefa que outra sessão começou, o cartão de atividade já mostra quem a mandou, o aviso de parada diz isso, e quem passou a tarefa fica sabendo, para ela nunca sumir em silêncio: o chat da sessão que mandou quando as duas sessões estão no mesmo chat; senão, só o dono, no privado, e só quando o dono (ou o terminal) controlava a tarefa que mandou.

Limites conhecidos:

  • Uma tarefa com shell (a do dono ou a de um admin) roda com o mesmo usuário do sistema que o aipager e poderia falsificar um recibo, assim como já podia reescrever os outros arquivos de controle do aipager. Esse é o modelo de confiança que já existe: dê um shell só a pessoas a quem você confiaria o computador.
  • O aipager não governa de jeito nenhum as sessões do Claude que ele não roda, escreva quem escrever para elas.
  • Fora do Linux, o remetente é casado só pelo número do processo (o Claude Code não registra a hora de início lá). No Linux, um registro de recebimento sem hora de início não casa com nada.
  • O Claude Code pode escrever a mensagem na conversa dele um momento depois da primeira ferramenta da tarefa. Até ele escrever, uma execução que as permissões restritas negariam é negada com suavidade (o Claude é avisado para tentar de novo; a tarefa não para). Depois de 8 execuções sem ela, a tarefa roda restrita.
  • O caminho rápido do dono lê o último 1 MiB da conversa salva, nunca mais. Uma mensagem de outra sessão mais comprida que isso termina numa linha que o caminho rápido toma por uma (qualquer linha de mais de 256 KiB cortada pela janela é). Uma que fica inteira mais para trás (mais de 1 MiB de conversa escrito depois dela antes da próxima ferramenta) é pega pelo aipager enquanto a tarefa roda, ou pelo fim da tarefa enquanto ela ainda está na fila, não pelo caminho rápido.
  • Enquanto uma sessão tem um /loop sem texto configurado, qualquer tarefa que nenhuma mensagem do Telegram inicia (incluindo uma mensagem digitada no terminal) pode ter as primeiras ferramentas negadas com suavidade, até o Claude Code escrever a mensagem dessa tarefa e o aipager conseguir saber que não é o loop disparando; aí o Claude tenta de novo.
  • O aipager lê a tarefa de uma mensagem agendada a partir do que a conversa salva ganhou desde que ela começou; acima de 8 MiB, a partir dos primeiros e dos últimos 4 MiB dela (o que fica no meio foi conferido quando era a parte mais recente).
  • As permissões guardadas de uma mensagem agendada ficam até a sessão ser encerrada (/kill): uma sessão encerrada ou retomada as mantém, assim como o Claude Code mantém os crons dele e um despertar pendente de /loop. São guardadas no máximo 256 por sessão, e agendar mais uma é recusado para todos nessa sessão, então qualquer pessoa que pode agendar pode esgotá-las (aí inicie uma sessão com outro nome, ou dê /kill nesta). Uma mensagem que dispara sem registro recebe as permissões restritas ou, numa sessão com um /loop sem texto, as desse loop, assim que o Claude Code a escreve na conversa salva; até lá o aipager lê a tarefa pela anterior. Isso acontece com uma mensagem configurada antes desta versão; depois de reiniciar o computador (os registros ficam na pasta temporária do sistema, que um reinício apaga, enquanto o Claude Code ainda pode trazer de volta o despertar de uma sessão retomada dentro da hora); e com um cron duradouro, que o Claude Code roda em outra sessão da mesma pasta quando a própria sessão dele já terminou. Uma sessão com menos permissões que agende o mesmo texto pode baixar as permissões de uma mensagem (nunca aumentá-las).
  • Uma sessão nova criada com o nome de uma anterior (Substituir) mantém os registros dessa sessão, incluindo as permissões do /loop sem texto dela, até o /kill.
  • Uma mensagem agendada que o Claude Code roda logo depois de outra tarefa, sem um evento de mensagem próprio (a partir da fila dele), recebe as mais rígidas entre as próprias permissões e as da tarefa que ela segue (depois de uma tarefa digitada no terminal, exatamente as dela).
  • As primeiras ferramentas de uma tarefa digitada no terminal podem ser negadas até o Claude Code escrever a mensagem dela na conversa salva (até lá o aipager lê a tarefa anterior).
  • Um agente em segundo plano iniciado com permissões mais estreitas pode devolver texto para uma tarefa posterior do mesmo trabalho, que o lê com as permissões do trabalho, como os outros avisos de tarefas do Claude Code.
  • O Claude Code pode segurar uma mensagem para aprovação no terminal que recebe (a configuração crossSessionInbound dele, ou quando as duas sessões rodam em classes de permissão diferentes, por exemplo o Modo Acesso total e o Modo Automático). O aipager não consegue liberar uma mensagem assim pelo Telegram; o recibo espera até uma hora.

Resumo das ameaças#

AmeaçaComo se mitiga
Um estranho manda um comando ao botO filtro de ID de chat recusa
Roubam o token do bot/revoke no @BotFather e configurar de novo
Uma execução de ferramenta comprometida no claudeA verificação de segurança do aipager nega o que as regras de uma tarefa do Telegram negam; no Modo Perguntar, as próprias regras do Claude Code no settings.json decidem sobre o que ele pergunta e a única permissão é o toque de uma pessoa; no Modo Automático (o padrão de toda sessão nova) a verificação de segurança do próprio Claude Code roda sem perguntar as chamadas que considera seguras e segura as arriscadas para uma pessoa; uma sessão no Modo Acesso total (admins, e cada pessoa no próprio chat privado) não pergunta nada, então só a verificação de segurança do aipager fica no caminho, e para as tarefas do próprio dono e as mensagens do terminal, nada (quem decide)
Um usuário restrito do Telegram se passa pelo donoSem shell, sessões só dentro da pasta raiz do próprio membro, escritas e buscas limitadas ao projeto dentro dela (nunca a pasta pessoal), nenhuma leitura na pasta pessoal fora da pasta raiz, pastas raiz de outros membros e arquivos enviados em outros chats inalcançáveis, arquivos de credenciais ilegíveis, caminhos protegidos para as ferramentas de arquivo, verificações que falham negam (como as regras valem no uso em equipe); só de melhor esforço para uma função com Bash, e sem cobertura para os casos de “Limites conhecidos”
A mensagem de uma sessão diz ter a autoridade de um donoA mensagem roda com as permissões comprovadas da tarefa que a manda, nunca com as anteriores da sessão que a recebe; sem prova, só ler e pensar (mensagens entre sessões)
Um membro de um grupo pega emprestadas as permissões de outroCada mensagem roda com a função de quem a manda no chat de onde ela vem; a mensagem de outra pessoa espera a tarefa em andamento; se mesmo assim uma entrar, ganham as regras mais rígidas; os comandos de barra das funções restritas ficam limitados aos do teclado (como as regras valem no uso em equipe)
A conversa do grupo chega a uma sessãoNum grupo só são atendidos os comandos, as respostas ao bot, as menções a ele e os toques marcados do teclado (uso em equipe)
Um membro tirado continua com acessoO aipager config recarrega a configuração do aipager sem reiniciar; as mensagens guardadas dele são descartadas e uma tarefa dele ainda em andamento perde as permissões anteriores; tirá-lo do grupo do Telegram o impede de continuar lendo (uso em equipe → recarregar a configuração)
Adulteração do registro de auditoriaSó cresce; impedir isso sem um programa de assinatura à parte fica fora deste desenho
Uma injeção de prompt faz o Claude escrever um endereço que leva dados para foraAs imagens e as tags de mídia no texto do Claude aparecem como links ou texto, nunca como mídia, e as prévias de link ficam desligadas nas mensagens de texto simples e HTML (conexões de rede)
Atacante na redeNenhuma porta de entrada, não dá para chegar direto
Escalada de privilégios localNem sudo nem setuid; o aipager fica no espaço do usuário
O áudio vaza para a nuvemVira texto neste computador
O agente lê a credencial do Claude do próprio aipagerEsperado, não é bug; veja A credencial do Claude. Limite a própria credencial, não o arquivo em que ela fica

Veja também#