Modelo de seguridad
aipager ejecuta Claude Code en tu nombre y se maneja desde Telegram. De ahí salen dos preguntas:
- ¿Quién puede manejar el bot?
- ¿Qué puede hacer el bot una vez que lo manejan?
Límite de confianza#
Cada manejador que expone el bot (mensaje, archivo, audio, botón) está detrás del filtro de chats de python-telegram-bot, construido con los chats configurados en ~/.config/aipager/aipager.yaml, o (para los comandos) detrás de la misma comprobación en la autorización propia de aipager. La única excepción solo anota a quienes escriben en grupos no configurados (abajo). Solo los chats configurados pueden usar el bot. Nada que venga de otro chat se ejecuta ni se envía a una sesión. Un chat privado no configurado recibe una sola respuesta «🚫 Este bot no está configurado para hablar contigo.». En un grupo no configurado el bot nunca responde, pero sí anota quién le escribió ahí (su ID de Telegram, su nombre de usuario y su nombre, y el ID, el título y el tipo del grupo) en ~/.claude/aipager-pending-users.json, para que aipager config pueda encontrar un grupo nuevo y a su gente mientras aipager está en marcha. Cualquiera puede añadir el bot a un grupo, así que cualquiera puede añadir una línea a ese archivo; no da ningún permiso. El archivo solo lo puedes leer tú (permisos 0600), se reemplaza entero en cada escritura (nunca queda a medio escribir) y guarda como mucho las 200 personas más recientes, quitando a quien lleve 30 días sin aparecer. En el uso en equipo se añade encima una lista de permitidos por persona con roles, y en un grupo solo se atiende un mensaje dirigido al bot (un comando, una respuesta a él, una mención o un toque en su teclado), sea o no el bot admin del grupo (mira uso en equipo).
Esto significa que lo expuesto «al mundo» es:
- El token del bot (un secreto).
- El ID del chat (un número largo).
Si se filtran los dos, alguien puede manejar tu aipager. Si solo se filtra el token, alguien puede leer los mensajes que tu chat le envía al bot, pero no puede enviar comandos que aipager vaya a ejecutar. Si solo se filtra el ID del chat, no sirve de nada: el bot necesita el token para hablar con Telegram.
Secretos#
| Secreto | Dónde | Permisos |
|---|---|---|
| Token del bot | ~/.config/aipager/aipager.yaml | 600 por defecto |
| IDs de chat | el mismo archivo | 600 por defecto |
El token no llega al log de aipager: no se registran las líneas de petición HTTP que lo llevan, y los mensajes de error, aipager doctor y el asistente ocultan cualquier cosa con forma de token de bot. Los IDs de chat sí aparecen en el log de aipager (por ejemplo en las líneas de límites de velocidad), así que trata la salida de aipager logs como privada. Ninguno se sube al repo: la configuración vive en el ~/.config del usuario, no en el repo. El proceso de publicación en PyPI con Trusted Publisher tampoco toca secretos; la autenticación la hace OIDC.
Si sospechas que el token se filtró, revócalo en @BotFather (/revoke), genera uno nuevo y vuelve a ejecutar aipager config.
La credencial de Claude: qué la protege de verdad#
aipager ejecuta un agente autónomo con acceso a Bash como tu usuario de UNIX. Por lo tanto, cualquier secreto que aipager pueda leer, el agente que arranca también puede leerlo. Vale la pena decirlo claro, porque es fácil buscar el tipo de control equivocado.
~/.config/aipager/ está en la lista de bloqueo de safety.py, que impide que una sesión manejada desde Telegram lea los archivos de ahí. Eso es un control de ruta, y aquí no hace nada: la credencial de Claude (CLAUDE_CODE_OAUTH_TOKEN o ANTHROPIC_API_KEY) no se alcanza por una ruta; se hereda en el entorno del proceso de cada sesión que se arranca. Dentro de una sesión, echo $CLAUDE_CODE_OAUTH_TOKEN la imprime, haya lista de bloqueo o no. Una regla de ruta de archivo es la forma equivocada de controlar una variable de entorno.
Dónde está la credencial#
| Instalación | De dónde sale | Permisos |
|---|---|---|
| servicio de usuario de systemd | LoadCredential=claude_oauth:%h/.config/aipager/daemon.env, leído de $CREDENTIALS_DIRECTORY/claude_oauth | 600 (daemon.env); systemd mantiene la credencial fuera del entorno del propio servicio |
| macOS / Docker / sin systemd | ~/.config/aipager/daemon.env, leído directamente | 600 |
Las dos vías se leen con el mismo código (aipager/daemon_secrets.py) y se pasan a la sesión con la tabla env= del subproceso, nunca metidas en la cadena del comando bash -c. /proc/PID/cmdline lo puede leer cualquiera (0444); /proc/PID/environ solo su dueño (0400).
Lo que de verdad dan LoadCredential= y 0600: los demás usuarios de UNIX de la misma computadora no pueden leer la credencial, y nunca aparece en systemctl show, en una copia de seguridad del archivo del servicio, ni en una captura de la salida de journalctl que alguien pegue en un informe de error. No crea un límite entre aipager y el agente que aipager arranca; ese límite no existe, y ninguna cantidad de ingeniería de permisos de archivos puede crearlo, porque todo el trabajo del agente es actuar como su dueño dentro de ese mismo entorno.
El único control que funciona de verdad#
Limita la propia credencial. Ejecuta aipager con una cuenta de Anthropic aparte, o con una clave de API que tenga un límite de gasto; no con el mismo token con el que usas tú claude de forma interactiva. Revocar la credencial de aipager nunca debería desconectarte a ti de claude; si lo hace, son la misma credencial y ninguna está limitada. aipager doctor --fix puede encontrar un token existente para rellenar daemon.env, pero cuál token darle es decisión tuya, y es la única decisión de este documento que importa más que los permisos de los archivos de alrededor.
Diagnosticar la autenticación sin adivinar#
aipager doctor (y el aviso de una línea que se envía una vez en cada arranque de aipager) informan de la autenticación con el propio claude auth status de Claude Code, nunca con una comprobación de archivos hecha a mano. Tres estados se mantienen distintos en el texto en todas partes:
auth: <method> (<source>): autenticado, confirmado.auth: none (not logged in): sin autenticar, confirmado.auth: unknown (...): la propia comprobación falló (tiempo agotado, falta el ejecutable, salida que no se entiende) o el ejecutable es anterior a la versión que añadió la salida JSON. Esto nunca se informa como «sin autenticar»: aipager no se niega a arrancar una sesión por una comprobación de autenticación de la que no está seguro. La autenticación con el Llavero de macOS, las credenciales que solo tienen un refresh token y la autenticación sin archivo del plan Max no se ven en ninguna comprobación de archivos, que las haría parecer idénticas a «sin autenticar».
Quién decide si se ejecuta una herramienta#
Dos comprobaciones distintas deciden si una herramienta se ejecuta, en este orden:
- La comprobación de seguridad de aipager. Claude Code ejecuta el hook
PreToolUsede aipager antes de cada herramienta. Para una tarea que empezó en Telegram, la somete a las reglas del rol de quien envió el mensaje y al mínimo de seguridad (rutas protegidas, patrones de comandos), y rechaza lo que ellos rechazan: Claude Code entonces rechaza la herramienta sin preguntar a nadie, digan lo que digan sus propias reglas. Un mensaje escrito en la terminal no queda limitado por esta comprobación. Lo que cubre está más abajo, en cómo se aplican las reglas en equipo. - Las propias reglas de Claude Code (
~/.claude/settings.json, el modo de permisos de la sesión). Si dicen que pregunte, Claude Code lanzaPermissionRequesty aipager muestra la solicitud en Telegram (mira hooks). Tu toque vuelve a Claude Code como la decisión del hook, o se escribe en el propio diálogo de Claude Code si se acabó la espera de 20 segundos. Si las reglas de Claude Code rechazan la herramienta, nunca llega a Telegram.
Así, aipager puede rechazar una herramienta que Claude Code permitiría, y cuando Claude Code pregunta, aipager nunca dice que sí por su cuenta: un permiso siempre es el toque de una persona. Tres cosas dejan que las herramientas se ejecuten sin preguntar a nadie:
- Modo Acceso total, para admins (y para cada persona en su propio chat privado). Una sesión en Modo Acceso total arranca con
--dangerously-skip-permissions: Claude Code no pregunta nada, así que cualquier ejecución que la comprobación de seguridad de aipager no rechace se ejecuta sin toque. Solo un admin (los rolesowneryadmin, o un rol concan_manage) puede arrancar una sesión en Acceso total, pasar una a ese modo (/mode full, tras una confirmación) o reanudar una en él, con una excepción: en su propio chat privado con el bot, una persona cuyo rol le deja enviar mensajes y responder a Claude (can_promptycan_approve, así que unusersí peroread_onlyno) puede hacer lo mismo con sus propias sesiones (/new !x1, o Sesiones nuevas en Acceso total en ese chat,/mode full,/resume), en Telegram y en la Mini App. Nunca en un grupo ni en el chat privado de otra persona. Los demás reciben el Modo Automático en su lugar, y la respuesta lo dice. El Acceso total solo se salta las preguntas propias de Claude Code: el hook PreToolUse de aipager sigue aplicando las reglas del rol de esa persona (el mínimo de seguridad, el límite de carpetas, las reglas de rechazo, los archivos de credenciales) a cada ejecución, exactamente igual que en Modo Preguntar y Modo Automático; el modo que informa Claude Code nunca las relaja. La comprobación de seguridad de aipager sigue ejecutándose en cada llamada en Acceso total, pero no rechaza nada en las tareas propias de un propietario ni en un mensaje escrito en la terminal. - Modo Automático, el de por defecto. Una sesión en Automático arranca con
--permission-mode auto: la comprobación de seguridad propia de Claude Code revisa cada ejecución, ejecuta las seguras y detiene las riesgosas, que luego llegan a Telegram como una solicitud igual que en Modo Preguntar. Toda sesión arranca en Automático salvo que el chat eligiera otra cosa en/settings→ Sesiones nuevas. Cualquiera que pueda actuar sobre una sesión puede pasarla entre Modo Preguntar y Modo Automático (/mode); aipager lo hace sin reiniciar pulsando Shift+Tab en la sesión, nunca con una opción que meta el Acceso total en el ciclo de Shift+Tab de Claude Code. Si alguna vez Claude Code informa Acceso total para una sesión que aipager no arrancó en ese modo, aipager muestra lo que se informó, escribe una advertencia en el log y nunca arranca ni reinicia la sesión en Acceso total por eso. Límite conocido: el modo que informa Claude Code llega por el socket del hook, en el que puede escribir cualquier proceso local (mira conexiones de red). Un informe falso puede cambiar el modo con el que se muestra una sesión y, mientras un cambio entre Modo Preguntar y Modo Automático espera su confirmación, puede hacer que aipager pulse Shift+Tab una vez más, lo que puede dejar la sesión real en Automático cuando se pidió Modo Preguntar (el siguiente hook real vuelve a mostrar el modo real). Nunca cambia la opción que pasa un reinicio o una reanudación, y nunca puede llegar a Acceso total: eso solo lo pone la opción de arranque. aipager no pulsa Shift+Tab en una sesión que sabe que puede llegar a Acceso total por el ciclo: una que arrancó en Acceso total (también después de pasarla a Modo Preguntar o Automático en la terminal), o una que informó Acceso total al aipager en marcha desde que arrancó. Ahí todo cambio de modo es un reinicio, que arranca con la opción del modo nuevo y quita la de Acceso total, y un informe falso de Acceso total solo puede convertir un cambio en un reinicio. aipager no puede saber esto de una sesión arrancada en una terminal con la opción que añade el Acceso total al ciclo de Shift+Tab (--allow-delante de la opción de Acceso total), o con--dangerously-skip-permissionscuando salió del Acceso total antes de que aipager supiera de ella (aipager no estaba en marcha entonces, o se reinició después). Ahí, pasar de Modo Preguntar a Modo Automático puede terminar en Acceso total, y entonces la tarjeta dice que la sesión está en Acceso total. No uses esas opciones en sesiones de terminal que personas que no son admins puedan cambiar desde Telegram. Para que el Acceso total funcione desde Telegram, aipager poneskipDangerousModePermissionPrompten~/.claude/settings.jsonen cada arranque (si no, el aviso de Claude Code antes de ejecutarse en ese modo frenaría la sesión); eso también apaga el aviso para las sesiones que arrancas tú. Del mismo modo, marca como de confianza en~/.claude.jsonla carpeta donde empieza cada sesión (salvo que esa carpeta o una superior ya lo sea), para que la pregunta «Do you trust this folder?» de Claude Code no frene una sesión creada desde Telegram: el primer mensaje la respondería con «No, exit». Los ajustes.claude/, los hooks de.git/y el.mcp.jsonpropios de una carpeta de confianza pueden ejecutar comandos; por eso los miembros sincan_managenunca pueden escribir esos archivos (salvo quepolicy.yamlle devuelva un shell a su rol). Claude Code aplica la confianza de una carpeta a las carpetas de debajo que no están dentro de un repositorio git; un repositorio sigue recibiendo su propia pregunta. La carpeta personal se marca cuando una sesión empieza allí, porque Claude Code solo guarda un sí dado en la carpeta personal para esa sesión. Desde entonces, Claude Code no pregunta nada en una carpeta bajo tu carpeta personal que no esté dentro de un repositorio git, tampoco en las sesiones que arrancas tú./y las carpetas por encima de la personal nunca se marcan: eso alcanzaría a todas esas carpetas de la computadora, así que una sesión que empiece en una de ellas se queda parada en la pregunta. Al arrancar, aipager no marca nada, solo la carpeta en la que está por empezar una sesión. Una sesión nueva empieza en la carpeta donde se ejecuta aipager (la carpeta personal conaipager service installen Linux y macOS, y en el contenedor) o enAIPAGER_WORK_DIRsi está definido; el log de aipager avisa al arrancar cuando esa carpeta es/o una carpeta por encima de la personal y no es de confianza. Las versiones anteriores a esta marcaban la carpeta de aipager en cada arranque,/incluida en un servicio de macOS (que se ejecutaba en/); aipager no puede distinguir esa entrada de una que hayas creado tú, así que la deja. aipager no toca~/.claude.jsonni~/.claude/settings.jsoncuando no puede leerlos enteros o cuando un Claude en marcha los cambió mientras aipager los actualizaba. - Permitir siempre, que le devuelve a Claude Code la regla de «no volver a preguntar» que ofreció con la solicitud, exactamente como lo haría esa opción en su propio diálogo.
Para bloquear una herramienta para todo el mundo, recházala en ~/.claude/settings.json; para bloquearla para algunas personas, usa las reglas de su rol en policy.yaml (mira uso en equipo).
Lo que muestra una solicitud es lo que se ejecuta. Una solicitud de permiso muestra el comando o la ruta reales debajo de la descripción de Claude. Los controles de dirección de Unicode (U+202A-U+202E, U+2066-U+2069, U+200E, U+200F, U+061C) cambian el orden en que Telegram, un navegador o un teléfono dibujan el texto que les sigue, mientras que el shell ejecuta los caracteres en orden, así que podrían hacer que un comando se lea como otro distinto (el truco «Trojan Source», CVE-2021-42574). En cada solicitud, pregunta, vista previa de diff y registro de auditoría se muestran como marcas visibles (⟨U+202E⟩), y la solicitud avisa de que el comando puede ejecutar algo distinto de lo que parece. Los demás caracteres invisibles se dejan como están.
Cómo se aplican las reglas en equipo#
En el uso en equipo, aipager añade su propia capa debajo de los toques: cada mensaje que viene de Telegram lleva una nota de permisos (quién lo envió y qué permite su rol) que se escribe en /tmp/claude-notes-<session>/, un archivo por mensaje. Cuando Claude toma un mensaje, el ayudante aipager-hook lo empareja con esas notas e instala las reglas de quien lo envió donde las lee la comprobación PreToolUse. Estas propiedades son la base:
- Gana la regla más estricta. Si Claude toma mensajes de más de una persona en una sola tarea (aipager retiene mensajes para evitarlo, pero si pasa igual se mantiene seguro), la tarea se ejecuta con la combinación más restrictiva; el mensaje de un propietario nunca puede prestarle sus permisos al de otra persona.
- Una tarea en curso nunca se amplía. Un mensaje de otra persona espera a que termine la tarea en curso y luego se ejecuta como su propia tarea con las reglas de quien lo envió. Si aun así uno se suma a una tarea en curso, la tarea se queda con las reglas más estrictas de todos los que están en ella; un mensaje de Telegram que se suma a una tarea que escribiste en la terminal deja el resto de esa tarea con las reglas de quien lo envió (el de un propietario no cambia nada).
- Tu rol es el del chat en el que actúas. Una persona que está en varios chats (por ejemplo
useren un grupo yownerde su propio chat privado) tiene el rol del grupo para todo lo que envía o toca en el grupo, y el del chat privado en el chat privado, sea cual sea el orden de los chats enaipager.yaml. - Desconocido significa restringido. Un mensaje que no se puede atribuir a nadie se ejecuta con el mínimo de seguridad incorporado (sin exenciones, con todas las reglas de rechazo activas), nunca como un mensaje de terminal sin restricciones. Los mensajes que escribes directamente en la terminal siguen sin restricciones; cualquier cosa con la marca de Telegram en cualquier línea queda sujeta a las reglas. Cada mensaje de Telegram lleva esa marca, con la etiqueta de quien envió ese mensaje (nunca de quien manejó la sesión la última vez); alguien que aipager no conoce, por ejemplo alguien a quien quitaron de
aipager.yamlmientras su mensaje esperaba, recibe un[via Telegram]sin nombre y el mínimo de seguridad. - Los comandos de barra necesitan un admin. Un comando de barra se escribe en Claude tal cual, sin marca, y un comando o skill como
/deliverejecuta una tarea completa a partir de su propio texto. Así que un miembro cuyo rol no está exento de las reglas de rol (losuseryread_onlyincorporados, los roles que defines sinbypass_role_denies, y cualquiera que aipager no conozca) solo puede enviar los comandos del teclado de Comandos de aipager (más/compact) y cambios de modelo (/model <name>); cualquier otro comando de barra, escrito, enviado a una sesión con/<name>, como pie de un archivo, como primer mensaje de/newo como plantilla, se rechaza con «Ese comando es solo para admins.» y un 🤷. Los propietarios y los admins envían cualquiera. Un comando de barra que sí llega a Claude desde Telegram igualmente queda sujeto a las reglas: el hook ve que la tarea tomó un mensaje de Telegram que envió este comando y lo somete a las reglas de quien lo envió. Un comando de barra que escribes en la terminal sigue sin restricciones, salvo que el mismo comando (con cualquier argumento) se haya enviado antes desde Telegram en esa sesión: aipager no puede distinguir tu copia de Claude Code entregando otra vez la de Telegram, así que la tuya se ejecuta con las reglas de la última tarea de Telegram. Esto termina cuando termina la sesión. - Un mensaje sin remitente nuevo conserva las reglas de la tarea. Claude Code también informa de mensajes que ningún mensaje de Telegram explica mientras hay una tarea de Telegram en curso: un mensaje escrito en la terminal y en cola detrás de la tarea (se informa en cuanto entra en la cola), un mensaje de otra sesión local de Claude, o el mismo mensaje de Telegram entregado otra vez después de compactar. Un mensaje así no añade ningún remitente de Telegram, así que la tarea conserva las reglas con las que iba, más estrictas por cualquier mensaje que todavía espere a ser tomado y nunca más relajadas. Un mensaje con texto de Telegram que aipager no puede atribuir sigue cayendo en el mínimo de seguridad incorporado, y un mensaje de alguien con menos permisos sigue bajando la tarea a las reglas de esa persona.
Lo que de verdad contiene a un usuario restringido. La sesión de Claude se ejecuta con el mismo usuario del sistema que es dueño de la configuración de aipager (el token del bot en ~/.config/aipager/aipager.yaml, la credencial de Claude en ~/.config/aipager/daemon.env) y de su archivo de reglas por tarea (/tmp/claude-policy-<session>.json). Un shell que se ejecuta con ese usuario puede llegar a los dos con formas de escribirlo que ningún patrón prevé (sed -i … /tmp/*-policy-*.json, un bucle for sobre un glob, una línea de Python) y reescribir sus propias reglas como las del propietario. Por eso los patrones de comandos son un filtro, no un límite. Para una tarea que se ejecuta con los roles restringidos incorporados (user, read_only), y para una tarea que aipager no puede atribuir a nadie, aipager aplica estas reglas, ninguna de las cuales depende de patrones de comandos (mira los límites conocidos más abajo para lo que no cubren):
- Sin shell. No pueden usar
Bash, ni ninguna otra herramienta con la que Claude Code ejecute código (PowerShell,Monitor,REPL,Workflow, las herramientas que programan mensajes,CronCreateyRemoteTrigger, …; la lista essafety.CODE_EXECUTION_TOOLS). Un mensaje programado llegaría sin la marca de Telegram, es decir, sin restricciones, así que esos también cuentan. Por la misma razón no pueden usarSendMessage(puede pasarle un mensaje a otra sesión local de Claude, que lo ejecutaría como un mensaje de terminal), niEnterWorktree/ExitWorktree(mueven la carpeta de trabajo de la sesión, en la que se apoya la siguiente regla). - Las escrituras se quedan en el proyecto.
Write,Edit,MultiEdityNotebookEditsolo escriben dentro de la carpeta de la sesión (la carpeta de trabajo que Claude Code informa en el contenido del hook, nunca una ruta de la propia llamada a la herramienta) y del scratchpad de Claude Code de la sesión (/tmp/claude-<uid>/<project>/<session id>/, o/tmp/claude-<uid>/cuando no se puede averiguar). Todo lo demás se rechaza: los archivos de inicio de tu shell,~/.ssh, otros proyectos, los archivos de aipager en/tmp. Dentro de la carpeta también se rechazan.claude/,.git/y.mcp.json, porque cada uno hace que Claude Code o git ejecuten un comando. Los enlaces simbólicos se siguen antes de decidir. Una sesión cuya carpeta es tu carpeta personal,/o una carpeta por encima de tu carpeta personal no tiene carpeta de proyecto: ahí una tarea restringida solo escribe en su scratchpad, y una escritura en cualquier otro sitio se rechaza con «esta sesión funciona en la carpeta personal (o en una superior), y este rol solo puede escribir dentro de una carpeta de proyecto». Para que los miembros restringidos no se queden atascados ahí,/newy la Mini App no les dejan arrancar una sesión en esas carpetas (la carpeta propia de aipager, la de por defecto para una sesión nueva, es tu carpeta personal bajo el servicio en segundo plano); pueden crear una carpeta nueva dentro de tu carpeta personal y arrancar en ella (una nueva: no una carpeta que ya tengas ahí, y nobinninode_modules, de donde otros programas cargan código). - Los miembros se quedan en su carpeta raíz. Un rol sin
can_manage(losuseryread_onlyincorporados) trabaja dentro de la carpeta raíz del chat, que pones enaipager config(por defecto~/aipager-users/<label>;~/aipager-usersy la carpeta se crean con permisos 0700)./new, Continuar, Reemplazar, Reiniciar, el cambio de modo y la Mini App solo arrancan o vuelven a arrancar la sesión de un miembro así dentro de ella. El hook de seguridad lo aplica llegue como llegue el mensaje (Telegram, la Mini App, el mensaje de otra sesión): la carpeta raíz viaja en la copia de las reglas de la tarea, en los recibos de los mensajes entre sesiones y en los permisos fijados y programados, y cuando miembros con carpetas raíz distintas comparten una tarea gana la más estrecha (dos que no están una dentro de otra dejan solo la carpeta de la propia sesión). Las escrituras solo van a la carpeta de la sesión cuando está dentro de la carpeta raíz (una sesión fuera de ella, como una que se arrancó antes de que existieran las carpetas raíz, sigue en marcha, pero sus tareas solo escriben en el scratchpad: «esta sesión funciona fuera de la carpeta de este rol»), yRead,LSP,GrepyGlobno llegan a nada de tu carpeta personal fuera de la carpeta raíz («en la carpeta personal, este rol solo lee dentro de su propia carpeta»), salvo el scratchpad de la sesión y su propio archivo de respuesta. La carpeta raíz de cualquier otro chat es una ruta protegida para el miembro, esté donde esté, y también lo es la carpeta de archivos subidos de cualquier otro chat bajo/tmp/aipager-files/(Read,Grep,Globy las herramientas de escritura; para un rol al que le des Bash, un comando que nombre una de esas carpetas se rechaza, de mejor esfuerzo), junto con los archivos que todos los chats compartían ahí antes de que hubiera una carpeta por chat. Los archivos subidos en el propio chat del miembro se siguen pudiendo leer. Las carpetas fuera de tu carpeta personal (/usr,/etc,/srv) siguen siendo legibles. Un rol personalizado conbypass_role_deniesobypass_safetypero sincan_manageescribe y busca en cualquier sitio dentro de su carpeta raíz, y sus lecturas en tu carpeta personal también se quedan dentro de ella: solo los propietarios y los admins no tienen restricciones. Conbypass_safety, en una sesión cuya carpeta está fuera de su carpeta raíz, solo escribe y busca en el scratchpad, como unuser, y en cuanto se rechaza una de sus herramientas, se detiene el resto de la tarea. - Las búsquedas se quedan en el proyecto. Un
GrepoGlobsolo se permite cuando se cumple todo esto: la carpeta en la que busca (supath, o la carpeta de la sesión si no tiene, siguiendo enlaces simbólicos) está dentro de la carpeta de la sesión (no cuando esa es tu carpeta personal,/o una superior a tu carpeta personal, igual que con las escrituras) o del scratchpad; esa carpeta no es una ruta protegida ni contiene ninguna (una sesión arrancada en tu carpeta personal contiene~/.config/aipager, así que no puede buscar desde su raíz); cada glob es un patrón relativo sencillo: sin/,~,$ni letra de unidad al principio, sin.., sin#(un comentario para ripgrep) ni!(una negación), sin escapes con\; y no tienes ninguna regla de ruta protegida sin/ni~al principio (**/.envpuede estar en cualquier sitio del proyecto, así que con una así cada búsqueda restringida se rechaza). Todo lo demás se rechaza. Es una lista de permitidos a propósito: tres rondas de revisión encontraron cada una una forma nueva de escribir un glob que sacaba a ripgrep de la carpeta que un modelo de su análisis esperaba, y una de ellas leyó el token del bot. - Las herramientas de archivos respetan las rutas protegidas.
Read,LSPy las herramientas de escritura comprueban adónde lleva de verdad una ruta, enlaces simbólicos incluidos. El único archivo de control que una tarea puede leer es el/tmp/claude-reply-<session>.txtde su propia sesión, que aipager nombra en el mensaje cuando respondes a un mensaje anterior; los archivos de respuesta de otras sesiones, y cualquier escritura en él, siguen rechazados. En macOS,/tmpes/private/tmpy el disco no distingue mayúsculas, así que se comprueban las dos formas y cualquier combinación de mayúsculas. - Una comprobación que falla rechaza. Si decidir sobre una herramienta da un error (un byte NUL en una ruta, un bug), o al hook se le acaba la memoria mientras decide, la ejecución se rechaza, salvo que las reglas de la sesión se puedan leer y den la exención del propietario. Antes se dejaba pasar.
Tus propias rutas protegidas. La sección safety: de policy.yaml (deny_paths_no_access, deny_paths_no_write, deny_bash_patterns) se suma a las rutas protegidas y los patrones de comandos incorporados. Es parte del mínimo de seguridad que recibe cada tarea de cada rol excepto owner, admins incluidos, y se aplica a las herramientas de archivos, las búsquedas y los comandos de Bash exactamente igual que la lista incorporada. aipager escribe la lista combinada en /tmp/claude-policy-.floor-<uid>.json (que a su vez es una ruta protegida) para los casos de reserva del hook de seguridad; si ese archivo no está, no se puede leer o es de otro usuario del sistema, el hook usa la lista incorporada, nunca menos. Un cambio hecho con una recarga de la configuración se aplica a los mensajes enviados después de la recarga, no a una tarea que ya está en curso ni a un mensaje que ya espera. Hasta la 0.7.20 la sección se leía y aipager doctor --safety-check la mostraba, pero no se aplicaba.
Tareas que aipager no puede atribuir. Una tarea de Telegram sin un remitente al que someterla se ejecuta con el mínimo de seguridad, que coincide con el rol user incorporado (no con las restricciones extra que le añadas a user en policy.yaml) más tu sección safety:: sin herramientas para ejecutar código, escrituras y búsquedas limitadas, las rutas protegidas. Eso pasa con un mensaje cuya nota de permisos caducó (a las 24 horas) o no se pudo escribir, y con un mensaje con texto de Telegram con el que no coincide ninguna nota. El botón Reintentar se ejecuta como quien lo toca cuando esa persona también envió el mensaje que se reintenta, así que el Reintentar de un propietario sobre su propio mensaje conserva Bash; un Reintentar sobre el mensaje de otra persona, o sobre uno cuyo remitente aipager no conoce, se ejecuta con el mínimo de seguridad, para que el texto de nadie tome prestados los permisos de quien toca.
Las lecturas fuera de las rutas protegidas se permiten para un rol que no está limitado a una carpeta raíz (un rol personalizado con can_manage pero sin exenciones): puede leer otros proyectos del usuario del sistema. Los user y read_only incorporados no leen nada de tu carpeta personal fuera de su carpeta raíz (arriba). Tus archivos de credenciales son rutas protegidas para cada rol sin bypass_role_denies (los user y read_only incorporados, los roles que defines, y el mínimo de seguridad): ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.password-store, ~/.pgpass, ~/.npmrc y ~/.pypirc no se pueden leer, escribir ni buscar. Son un valor por defecto del rol, así que el deny_paths_no_access propio de un rol en policy.yaml los reemplaza para ese rol, salvo mientras otro mensaje o un mensaje escrito en la terminal se suma a una tarea en curso: desde entonces hasta que termina la tarea, vuelve a quedar sujeta a ellos (gana la regla más estricta). Un archivo de credenciales en otro sitio (un .env en un proyecto, un token en otra carpeta) no está cubierto: añádelo al deny_paths_no_access del rol. Los propietarios no están sujetos a nada de esto. Las escrituras de los admins no están limitadas. Su Grep/Glob puede buscar en cualquier carpeta que no llegue a una ruta protegida: una búsqueda se rechaza cuando la carpeta en la que busca (su path, o la carpeta de la sesión si no tiene; para un Glob, también la carpeta que nombra su patrón, como en /home/you/.config/aipager/*, o la carpeta por encima de un patrón sin comodines, en la que Claude Code busca ese nombre a cualquier profundidad), con los enlaces simbólicos resueltos, es una carpeta protegida, está dentro de una o contiene una. Así, Grep en ~, /, la carpeta por encima de tu carpeta personal o /tmp se rechaza (contienen ~/.config/aipager, ~/.claude o los archivos /tmp/claude-* de aipager), mientras que Grep en un proyecto o en /tmp/myproj no. Una regla de ruta protegida sin / ni ~ al principio (como **/.env) puede coincidir en cualquier carpeta, así que con ella en las reglas de un admin (o en la sección safety:, que recibe cada rol excepto el propietario) se rechaza cada búsqueda del admin, como para los roles restringidos; aipager doctor avisa de una así en la sección safety:. Los admins tienen Bash, así que sus reglas siguen siendo de mejor esfuerzo (abajo).
Límites conocidos. Esto no está cubierto:
- Si el propio hook falla antes de empezar a decidir (por ejemplo no puede leer su propio archivo de estado, o se le acaba la memoria al arrancar), Claude Code lo trata como «permitir». Un usuario restringido no puede provocar esto; cualquier error mientras decide rechaza.
- Las herramientas que no son de archivos pero pueden enviar datos fuera no se comprueban por ruta:
WebFetch(todo lo que la tarea pueda leer puede enviarlo a una URL) y las demás herramientas de entrega de Claude Code. Recházalas para un rol enpolicy.yamlsi eso te importa.SendUserFile, que envía archivos al chat, sí se comprueba (ver archivos que envía Claude). - Un agente en segundo plano que arrancó una tarea restringida sigue funcionando después de que termine la tarea, y sus herramientas posteriores se juzgan con el mensaje más reciente de la sesión: un mensaje posterior del propietario o de la terminal le quita las restricciones.
- Un rol que defines con
can_managepero sin ninguna exención no queda limitado a una carpeta raíz: puede seguir arrancando en cualquier carpeta de proyecto en la que ya trabaje su chat (una carpeta que usó una sesión anterior), incluida una tuya, y leer tus otros proyectos. - Una tarea a la que no se puede someter a ningún miembro (un remitente desconocido, un miembro quitado, un mensaje de otra sesión cuyo remitente no se puede verificar) se ejecuta con el mínimo de seguridad restringido, que no tiene carpeta raíz. En las lecturas ese mínimo es más débil que un
userincorporado: una tarea así puede leer cualquier archivo de tu carpeta personal que no sea una ruta protegida (tus otros proyectos, por ejemplo), mientras que la tarea de unusersolo lee dentro de su carpeta raíz. Una tarea así también puede leer los archivos subidos de todos los chats en/tmp/aipager-files/, que la tarea de unusersolo lee para su propio chat. Sus escrituras siguen limitadas a la carpeta de la sesión y al scratchpad. Un mensaje verificado de la sesión de un miembro lleva la carpeta raíz de ese miembro. - La carpeta raíz solo limita las herramientas de archivos. Un rol personalizado al que le des
Bashpuede leer fuera de ella a través del shell (losuseryread_onlyincorporados no tienenBash). Un rol personalizado conbypass_safetypero sincan_manageconserva su exención para todo menos las rutas de las herramientas de archivos: no se le rechaza ninguna herramienta ni patrón de comandos. - Un motivo de rechazo que nombra la carpeta raíz de otro miembro muestra la ruta de esa carpeta a Claude y en el chat.
- La tarea de un miembro que ya estaba en curso cuando llegan las carpetas raíz (una actualización) no tiene ninguna hasta el siguiente mensaje de ese miembro.
- Dentro del proyecto, un usuario restringido puede cambiar lo que tú luego ejecutas o lees (el código,
Makefile, los scripts depackage.json,conftest.py, unCLAUDE.mdque cargan tus propias tareas).
Un rol que tiene Bash (admin, que está exento de las reglas de rechazo de rol, o cualquier rol al que policy.yaml se lo devuelva: allow_tools que nombra Bash, o un deny_tools reemplazado) solo está sujeto a reglas de mejor esfuerzo: los patrones de comandos de abajo, que un usuario decidido puede esquivar. aipager doctor avisa una vez por cada rol así que tenga un miembro. Dale Bash solo a alguien a quien darías un shell en la computadora.
Las reglas de comandos incorporadas. Entre los patrones de Bash a los que está sujeta cada tarea de Telegram con Bash que no es del propietario, uno bloquea la palabra claude: detiene un claude anidado (con cualquier ruta, envoltorio u opción) y cualquier acceso a ~/.claude con una sola regla, y también atrapa los nombres simples de los archivos de control /tmp/claude-* de aipager, cuyos nombres comparten el prefijo (un patrón no es una protección real para esos archivos: una forma con glob lo esquiva). La única excepción es la carpeta scratchpad que crea Claude Code, /tmp/claude-<uid>/…: esa carpeta exacta, como componente completo de la ruta, está exenta, para que los comandos que se ejecutan desde el scratchpad de una sesión no se detengan. Es del usuario del sistema, no de una sesión, así que una tarea con Bash puede leer ahí los scratchpads de otras sesiones y la salida de sus tareas, igual que la herramienta Read. Un glob o algo que se le parezca (/tmp/claude-1*, claude-1000x), otra carpeta temporal, y todo lo que se alcance desde ella (…/../../home/you/.claude) siguen bloqueados. Las demás apariciones de la palabra (git log --grep=claude, un archivo llamado claude-notes.md, pip show claude-agent-sdk) también siguen bloqueadas, a propósito: la misma palabra nombra al ejecutable, sus SDK y los archivos de control de aipager, y un patrón no puede distinguirlos. Las reglas que añades en policy.yaml ven el comando exactamente como se escribió, scratchpad incluido.
Búsquedas por Bash. Algunas versiones de Claude Code no tienen herramienta Grep ni Glob (la 2.1.289 es una), y Claude entonces busca con grep, rg o find a través de Bash, donde la regla de Grep/Glob de arriba no se aplica. Para cada rol excepto el propietario, los patrones de comandos también rechazan una búsqueda o un listado recursivo de toda la carpeta personal o de /: grep -r (-R, --recursive, -d recurse, egrep, fgrep, rgrep), rg (y rg --files), ag, ack (y ack -f), find, fd, tree, du y ls -R cuya carpeta sea ~, ~user, $HOME, ${HOME} o "$HOME", /home, /home/<user> (o /home/$USER), /Users, /Users/<user>, /root o / (con o sin /, /. o /* al final), o ~/.config o ~/.local, que contienen las carpetas de aipager. Esto se cumple después de cd x &&, en una tubería, dentro de bash -c "...", después de xargs, env o timeout y después de ajustes como LC_ALL=C. La respuesta dice que busques dentro de una carpeta de proyecto. Las búsquedas en una carpeta de proyecto (grep -rn x ~/proj, rg x src/, find . -name y), las búsquedas dentro de un proyecto DE una ruta personal (grep -rn "$HOME" src; la primera palabra después de las opciones es el texto que se busca, no una carpeta) y las búsquedas no recursivas (grep x ~/.bashrc, ls ~) no se ven afectadas. Es de mejor esfuerzo, como todo patrón de comandos: un patrón ve las palabras, no dónde las ejecuta el shell, así que no puede ver una búsqueda sin carpeta, o con . o .., ejecutada desde la carpeta personal (o por debajo) (cd ~ && grep -r x .), una carpeta personal alcanzada a través de otra variable o un glob, ni una escrita completa fuera de /home, /Users y /root, ni \grep, ni una carpeta justo después de una palabra que termina en -e o -f (la toma por el texto que -e/-f le dan a grep y rg, así que ag x -f ~ pasa). Tampoco sabe qué opciones llevan un valor aparte, así que rg -t py "$HOME" . o grep -rn -A 3 "/home/you" src lee py o 3 como el texto y la ruta personal como carpeta, y se rechaza (escribe -A3, --include=*.py, o pon la opción después del texto); también una ruta personal dada a -e dentro de un grupo de opciones (grep -rne "/home" src; -r -n -e está bien); un comando citado dentro del texto de otro comando (echo "a; rg x ~") también se rechaza, y du -sh ~ también (solo tamaños).
Por qué un bloqueo detiene toda la tarea. Después de bloquear una herramienta, cada herramienta posterior de la misma tarea también se rechaza, hasta el siguiente mensaje. Sin esto, el agente simplemente reintenta lo mismo con otras palabras (un glob, otro comando, una ruta codificada) hasta que una forma se cuela por los patrones. Detener la tarea hace que el primer intento salga caro; no convierte un patrón en un límite, porque la primera forma que se intenta puede ser la que se cuela. El costo de un falso positivo es una tarea detenida, y por eso las reglas de arriba se mantienen estrechas.
Mira uso en equipo → cómo funcionan las reglas para las reglas que llevan estas notas, tal como las ves tú.
Archivos que envía Claude#
Cada sesión que inicia aipager tiene la herramienta SendUserFile de Claude Code (CLAUDE_CODE_ATTACHMENTS_BY_PATH=1), que lee archivos y los entrega en el chat de la sesión (comandos). El hook PreToolUse comprueba cada archivo de la llamada antes de que Claude Code lo lea:
- El mínimo de envío, para cada tarea, la del owner y un mensaje escrito en el terminal incluidos: la configuración y el estado de aipager (
~/.config/aipager/**,~/.local/share/aipager/**,~/.local/state/aipager/**y sus archivos de control en/tmp), todo~/.claude/**y~/.claude.json*(las credenciales y la cuenta de Claude), los archivos de credenciales de los que se aparta a cada rol restringido (~/.ssh/**,~/.gnupg/**,~/.aws/**,~/.git-credentials,~/.netrcy el resto de esa lista),/proc/**,/sys/**,/dev/**, y los nombres que parecen secretos, estén donde estén (.env,.env.*,*.pem,*.key,id_rsa*,id_ed25519*y los demás nombres de claves,*.p12,*.pfx,*.kdbx,*.jks,*.keystore,.pgpass,credentials,*credentials*.json). Se comprueba la ruta tal como está escrita y adonde lleva de verdad, así que también se rechaza un enlace simbólico a uno de ellos. Algunos archivos inofensivos tienen estos nombres (un*.keyde Keynote, un.env.example) y también se rechazan. - El propio archivo: un archivo normal una vez resueltos los enlaces simbólicos, con un solo enlace duro, no vacío, de 50 MB como máximo.
- Las reglas de la tarea: lo que la tarea no puede leer (Read), y para un miembro (un rol sin
can_manage, atado a una carpeta raíz) la regla más estricta de que cada archivo, tal como está escrito y adonde lleva de verdad, esté dentro de la carpeta raíz. A diferencia de lo que un miembro lee, no hay excepción para archivos fuera de la carpeta personal (/tmp,/etc) ni para el scratchpad de la sesión.
Un rechazo es un rechazo suave: Claude lee el motivo («SendUserFile refused, nothing was sent: ... Tell the person which file could not be sent and why.») y la tarea sigue; no detiene la sesión como lo hace una herramienta bloqueada. Una comprobación que falla rechaza el archivo, también para el owner. Cada archivo rechazado tiene un registro FileRefused en el registro de auditoría.
aipager no se fía de lo que el socket de control le diga sobre un archivo: cualquier proceso local puede escribir en él y nombrar cualquier sesión, llamada o conversación guardada, y basta el hook de una llamada a una herramienta para que una sesión inactiva pase a estar trabajando. Por eso una llamada solo cuenta durante una tarea que aipager vio empezar con un prompt (uno que escribió desde Telegram, o un UserPromptSubmit) y durante el trabajo en segundo plano de esa tarea, hasta que la sesión queda inactiva sin ningún trabajo en marcha; al empezar, aipager anota la hora y la conversación guardada que sigue para la sesión (un UserPromptSubmit posterior de la misma tarea vuelve a nombrar la conversación). Un comando de barra que aipager escribe no empieza ninguna tarea (uno que ejecuta una tarea envía su propio UserPromptSubmit), y una tarea que nunca empieza (la sesión sigue inactiva sin trabajo 15 segundos después) se cierra de nuevo. Claude Code empieza algunas tareas sin UserPromptSubmit: un mensaje enviado mientras corre una tarea que toma como su siguiente tarea, y la tarea que un hook de Stop mantiene en marcha (/goal). Una tarea así cuenta cuando aipager escribió ese mensaje él mismo, o en cuanto la conversación guardada que anotó muestra su comienzo después del último Stop (una línea de cola dequeue, o la línea Stop hook feedback: que escribe Claude Code); hasta entonces no se lee nada de la llamada, durante hasta 20 segundos. Toma la lista de archivos del PreToolUse que comprobó, o del PostToolUse cuando aquel se perdió (tras un reinicio); nunca del resultado de Claude Code. Copia cada llamada una sola vez: un id de llamada que ya copió nunca se vuelve a copiar ni a enviar (guarda los últimos 256 por sesión, también tras un reinicio). Averigua una sola vez la ruta real de cada archivo y lo comprueba otra vez en ella (el mínimo, las comprobaciones de archivo y la carpeta raíz de la sesión), abre exactamente esa ruta sin seguir enlaces, se asegura de que el archivo abierto es el que comprobó y sigue en esa ruta, y lo copia en ~/.local/state/aipager/outbox/<chat>/ (carpeta 0700, archivo 0600), un lugar del que ya se aparta a cada tarea restringida. Antes de enviar, busca la llamada en la conversación guardada que anotó para la tarea (para la llamada de un subagente, en la conversación propia de ese subagente, a su lado), nunca en una que nombre el datagrama de una llamada a una herramienta: una línea del asistente con un tool_use de SendUserFile con ese id y exactamente esos archivos, escrita desde la misma carpeta y no antes del inicio de la tarea, en un archivo .jsonl dentro de ~/.claude/projects (o $CLAUDE_CONFIG_DIR/projects, tal como está definido para el propio aipager) cuyo dueño es el propio usuario del sistema de aipager. Claude Code escribe su conversación guardada con retraso, así que aipager espera la llamada hasta 20 segundos, y mientras el trabajo de la sesión sigue en marcha (un agente en segundo plano, una llamada a una herramienta) conserva los archivos y vuelve a buscar más tarde. Una llamada que sigue sin encontrar cuando ya no hay nada en marcha no se envía y sus copias se borran; una llamada que llegó mientras no había ninguna tarea así, o que llegó por segunda vez, ni siquiera se copia. Cada uno de sus archivos recibe un registro FileFailed en el registro de auditoría con el motivo unverified. El chat recibe una línea por ellos (⚠️ No se pudo enviar report.pdf: aipager no pudo confirmar que Claude lo enviara en esta tarea.) solo cuando la propia conversación guardada de Claude muestra esa misma llamada durante la tarea, y la línea nombra los archivos que lista esa línea de la conversación, así que una llamada fingida no puede poner ni una línea ni un nombre en el chat; si no, no hay nada en el chat. Solo se envía la copia, solo al chat de la sesión, después de la respuesta de la tarea, después de los archivos anteriores de la sesión y sin frenar la siguiente tarea; se borra en cuanto se envía, o al cabo de un día.
Lo que estas comprobaciones no cubren: un proceso que se ejecuta como el propio usuario del sistema de aipager puede escribir las conversaciones guardadas de Claude igual que el socket (ya puede leer esos archivos por su cuenta). Un proceso de otro usuario que llegue al socket también puede fingir un UserPromptSubmit, que para estas comprobaciones empieza una tarea y nombra su conversación guardada; después puede hacer que aipager envíe al chat de esa sesión un archivo que nombró una llamada real de SendUserFile, si esa llamada se escribe en esa conversación desde la misma carpeta después de ese momento (por ejemplo, una que otra sesión está haciendo justo entonces), una vez por llamada. Para eso también tiene que enviar el id propio de esa llamada en curso (tool_use_id), que otro usuario no puede leer: está en las conversaciones guardadas bajo el ~/.claude del usuario de aipager, y el hook lo recibe por su entrada, no por su línea de comandos. Un Stop fingido termina la tarea para estas comprobaciones, así que los archivos que esa tarea envíe después no se envían. Una sesión iniciada con un $CLAUDE_CONFIG_DIR distinto del de aipager, o una tarea cuyo UserPromptSubmit se le escapó a aipager (escrita en el terminal mientras aipager se reiniciaba), no envía archivos. Tampoco envía archivos una tarea que Claude Code empiece sin hook de otra forma (las dos de arriba son las que se conocen); un archivo que la conversación guardada de Claude muestra que pidió ahí recibe la línea de arriba. El socket está en $XDG_RUNTIME_DIR, al que otros usuarios no llegan, cuando está definido; si no, está en /tmp.
Registro de auditoría#
Cada toque de Permitir o Rechazar, más cada respuesta a AskUserQuestion, añade una línea JSON a ~/.claude/aipager-audit.jsonl:
{
"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 (mira aipager/audit.py para la lista completa):
ts: marca de tiempo ISO 8601 en UTC, con precisión de segundos.session/label: el nombre interno y el nombre visible.action: qué pasó (Allowed,Denied,Allowed always, una respuesta aAskUserQuestion, un rechazo automático, …).tool/summary: el nombre de la herramienta y una entrada recortada o el texto de la pregunta.user_id/username: quién tocó, para saber quién fue en el uso en equipo;scope_label/scope_chat_iddonde hay varios chats configurados.denied: true para cualquier rechazo, por toque o por regla.
La escritura es de mejor esfuerzo. Si el disco se llena o ~/.claude/ deja de poder escribirse, aipager escribe un WARNING en el log y sigue funcionando: ni pérdida silenciosa, ni caída. Mira aipager/audit.py.
El registro de auditoría en disco solo crece. Úsalo junto con la respuesta de auditoría en el chat (un mensaje de Telegram por decisión, debajo de la tarjeta de actividad) para tener dos registros independientes.
Límite de permisos#
aipager nunca toma más permisos. Ni sudo, ni setuid, ni doas. Cada archivo que escribe está bajo tu carpeta personal, bajo /tmp (los archivos de control de cada sesión, las descargas y el log del reinicio) o en la carpeta de su socket de control ($XDG_RUNTIME_DIR); la lista completa está en arquitectura. Cada subproceso (claude, dtach, los instaladores, claude update) se ejecuta con el usuario de aipager.
Instalar la función de voz desde Telegram ([📦 Instalar la función de voz]) pasa por el instalador que es dueño de aipager (uv tool install --reinstall aipager[voice], pipx install --force aipager[voice]), y si no, instala los paquetes del complemento con el propio Python de aipager (<python> -m pip install): siempre en el entorno propio de aipager, nunca en el Python del sistema.
El botón [🔄 Reiniciar aipager], bajo el servicio de systemd, programa systemctl --user restart aipager.service en un servicio de usuario temporal aparte; en macOS ejecuta launchctl kickstart -k; un aipager que arrancaste tú lanza una copia separada de sí mismo que espera a que el viejo termine. Después de una actualización, solo el servicio de systemd se reinicia solo (mira comandos → actualizar). Todos se ejecutan con el mismo usuario; no se toman más permisos.
Conexiones de red#
aipager no escucha en ningún puerto TCP que no sea loopback. El servidor de la Mini App solo se abre en 127.0.0.1 (fijo en el código; a propósito no hay opción de host) y desde fuera solo se llega a él por el túnel gestionado que se describe abajo. Conexiones de salida:
- Long-poll HTTPS a
api.telegram.org(recibir las actualizaciones del bot). - HTTPS a
telegram.orgpara el script del SDK de la Mini App (telegram-web-app.js), que se descarga una vez cuando arranca el servidor de la Mini App y se renueva como mucho una vez al día; se guarda en~/.local/share/aipager/webapp-sdk/. aipager le sirve esos bytes a la página desde su propia dirección y nunca los ejecuta. aipager no redistribuye el script: no va nada dentro del paquete, y cuando aipager no tiene copia, la página lo carga directamente detelegram.org, como antes. - El túnel de la Mini App a Cloudflare, mientras está activado (por defecto). El ejecutable
cloudflaredque usa se descarga una vez de las versiones publicadas engithub.com/cloudflare/cloudflared, se comprueba con un SHA-256 fijo y se guarda en~/.local/share/aipager/cloudflared/; uncloudflaredque tengas en elPATHnunca se usa. Nada de esto pasa con tu propia URL para la Mini App (mira abajo). - La búsqueda de versiones, solo cuando alguien toca Buscar actualizaciones o Actualizar (
/update, las Actualizaciones de la Mini App), o cuando alguien que puede actualizar abre la pestaña Ajustes de la Mini App (como mucho una vez cada diez minutos por app abierta):pypi.orgpara aipager, ydownloads.claude.aioregistry.npmjs.orgpara Claude Code, según cómo se instaló. Luego una actualización ejecuta el instalador (que habla con PyPI o Homebrew) yclaude update.aipager updateen la línea de comandos solo ejecuta el instalador. - La instalación de la función de voz, cuando tocas su botón: el instalador descarga los paquetes de PyPI, y el modelo de voz se descarga de Hugging Face (
huggingface.co) la primera vez que se usa. - HTTPS a
raw.githubusercontent.com(el archivo de la clave de los informes) y al servidor de recepción de Sentry, solo cuando confirmas el envío de un informe de problemas: el informe que viste en la vista previa y un resumen corto hecho a partir de él, nada más. Mira Informes de problemas.
Conexiones de entrada:
- Socket datagram de Unix en
$XDG_RUNTIME_DIR/aipager.sock(o/tmp/aipager.sockcuando$XDG_RUNTIME_DIRno está puesto). aipager lo abre y le pone permisos al arrancar (aipager/dtach/hook_receiver.py). Permisos0o666para que cualquier proceso local pueda enviarle eventos de hook: la misma confianza que~/.claude/settings.json, que ya decide qué ejecuta los hooks.
Esto significa que aipager no se puede atacar a distancia. Un atacante en la red no puede llegar a él sin tener ya un pie en la computadora.
El texto de Claude y las descargas que hace Telegram. Los servidores de Telegram descargan por su cuenta algunas direcciones que aparecen en un mensaje: un bloque de imagen, video o audio en un mensaje enriquecido, y la página que hay detrás de una vista previa de enlace. Las respuestas de Claude, sus notas en la tarjeta de actividad, las descripciones de los agentes y los argumentos de las herramientas llegan a Telegram tal como los escribió Claude, así que una inyección de prompt podría hacer que Claude escriba una dirección que saque un secreto, sin ejecutar ninguna herramienta y sin ninguna solicitud de permiso. aipager cierra la primera vía, y la segunda donde Telegram tiene una opción para hacerlo:
- En cada mensaje enriquecido, una imagen escrita como
se envía como un enlace normal con su texto alternativo, y cualquier otra forma de escribir una imagen, y las etiquetas<img>,<video>,<audio>,<source>,<picture>,<figure>,<iframe>,<embed>,<object>,<tg-document>,<tg-collage>,<tg-slideshow>,<tg-button>y<tg-button-row>escritas tal cual, se muestran como texto (aipager/bot/media_guard.py). Dentro de un bloque de código, como un fragmento de HTML, esto muestra un\o un<de más. - Las vistas previas de enlaces están desactivadas en todos los mensajes de texto normal y HTML que aipager envía o edita, también los que solo llevan su propio texto (incluidas las versiones en texto normal de las respuestas y las tarjetas). Sus enlaces a la Mini App son botones, y Telegram no descarga los botones.
Telegram no documenta ninguna opción de vista previa para los mensajes enriquecidos; no se ha comprobado con el servicio real si genera una vista previa de enlace para uno.
El túnel de la Mini App#
La Mini App está activada por defecto, así que en una instalación normal aipager abre este túnel sin que nadie lo pida. Es una decisión consciente (una opción que nadie descubre es como si no existiera), pero significa que una instalación nueva se conecta a internet. Apágala con aipager miniapp disable, o pon enabled: false debajo de miniapp: en aipager.yaml; un false explícito siempre se respeta. Ten en cuenta que un bloque miniapp: que falta o está roto ahora vuelve a ACTIVADO, no a DESACTIVADO.
La Mini App (salvo que pongas otra dirección con --url) lanza un túnel rápido de Cloudflare gestionado que apunta al servidor loopback del propio aipager, y publica la dirección pública https://*.trycloudflare.com que le toque como botón de menú de Telegram y respuesta de /app.
Esa dirección no es secreta, ni pretende serlo: cada petición que atiende la Mini App se comprueba con la firma initData de Telegram (mira Límite de confianza arriba), la misma comprobación que la protege se haya conseguido la URL como se haya conseguido. Conocer el nombre de host por sí solo no le da a un atacante nada que no pudiera conseguir adivinando al azar un subdominio de trycloudflare.com; la verdadera puerta es la comprobación de la firma. Trátala como cualquier otra URL que no está publicada pero tampoco es secreta: puede quedarse en marcha, pero no la pegues sin motivo en un canal público.
El nombre de host solo se guarda en memoria; nunca se escribe en aipager.yaml ni en ningún sitio bajo ~/.config/aipager/. Un reinicio planificado (el reinicio de una actualización, Reiniciar ahora, el reinicio en el sitio sin servicio) deja el túnel en marcha, así que la dirección, el botón del menú y una Mini App abierta siguen funcionando: el siguiente aipager se lo queda solo después de comprobar que es el mismo arranque del sistema, el mismo puerto, que el proceso es ese cloudflared (su línea de comandos), que sigue dando la misma dirección (~/.claude/aipager-tunnel.json guarda un hash de ella, nunca la dirección) y que la dirección responde. Cualquier otra cosa lo para y arranca uno nuevo, y a un túnel conservado del que ningún aipager se hace cargo en 3 minutos lo para un pequeño proceso guardián. Cualquier otra parada (Ctrl+C, docker stop, systemctl stop) para el túnel, y reiniciar la computadora o el contenedor da una dirección nueva. MINIAPP_KEEP_TUNNEL=0 desactiva lo de conservarlo. Si el proceso de aipager muere de golpe (sin memoria, kill -9), el túnel sigue en marcha hasta que aipager vuelve a arrancar con el túnel gestionado activado (la Mini App activada, sin MINIAPP_PUBLIC_URL) y se lo queda o lo para, o hasta que la computadora se reinicia; aipager uninstall también lo para. Si el túnel se cae a mitad de funcionamiento, aipager lo vuelve a arrancar con esperas crecientes y publica la dirección nueva; si no vuelve después de varios intentos, se quita el botón en lugar de dejarlo apuntando a un puerto muerto.
Poner MINIAPP_PUBLIC_URL (o aipager miniapp enable --url https://…) apaga por completo el túnel gestionado (nunca se descarga ni se lanza ningún cloudflared) y la URL dada se usa tal cual, con la misma comprobación de initData debajo. La detección automática de Tailscale (tailscale status --json) sigue disponible como opción más sencilla para quien ya usa Tailscale y prefiere no depender en absoluto de Cloudflare.
Pasar audios a texto#
faster-whisper funciona dentro del propio proceso. El audio se descarga como .ogg en la carpeta propia del chat bajo /tmp/aipager-files/ (donde van los archivos subidos, 0700, el archivo 0600), se pasa a texto en esta computadora con la CPU, y el archivo se queda bajo tu control. Ningún audio sale de la computadora. Ninguna API de terceros. No hace falta ninguna clave más allá del token del bot para hablar con Telegram.
Si borras el .ogg después de pasarlo a texto, el único registro en texto del mensaje es el texto que se escribe en la sesión de claude (que sigue las reglas de privacidad del propio Claude Code).
Aislamiento entre sesiones#
Cada sesión de Claude Code funciona en su propio dtach. El socket de control en /tmp/claude-dtach-<name>.sock es del usuario de aipager; dtach no deja conectarse a otro usuario. Cada sesión arranca en su propia carpeta: la que se eligió con /new o la Mini App, o la carpeta donde ejecutaste aipager session.
aipager no aísla las sesiones entre sí a nivel del sistema de archivos: una sesión conectada a ~/projects/foo en principio puede leer ~/projects/bar si los permisos de Claude Code lo dejan. Para un aislamiento más fuerte, usa ajustes ~/.claude/settings.json por proyecto o un contenedor (imagen de Docker).
Mensajes entre sesiones de Claude#
Claude Code deja que una sesión le envíe un mensaje a otra (su herramienta SendMessage). En la sesión que lo recibe, ese mensaje es un mensaje como cualquier otro, así que aipager decide sus permisos igual que decide los de un mensaje de Telegram: por quién lo envió de verdad, nunca por lo que dice.
- El mensaje lleva los permisos de quien lo envía, nunca más. Cuando aipager deja que una sesión envíe un mensaje, escribe un recibo de un solo uso: una huella del texto exacto, el proceso de Claude que lo envía (su número de proceso y, en Linux, su hora de inicio) y los permisos que tenía esa tarea. Claude Code registra el mensaje recibido con el número de proceso del remitente, que verificó el kernel, sin fiarse nunca del nombre que el remitente dice tener. Antes de que la tarea que lo recibe ejecute cualquier herramienta, aipager empareja los dos y le da a la tarea exactamente los permisos de la tarea que lo envió: el mensaje de un propietario se ejecuta con los permisos del propietario, el de un miembro con los de ese miembro, y un mensaje escrito en la terminal de la sesión que lo envía, sin restricciones.
- Sin recibo, permisos restringidos. Un mensaje que aipager no puede emparejar (enviado por una sesión o un proceso de Claude que aipager no ejecuta, un recibo que caducó pasada una hora, un remitente en otra computadora) se ejecuta con las reglas más estrictas más ningún cambio de archivos y ningún acceso a la web (solo leer y pensar). Los permisos anteriores de la propia sesión que lo recibe nunca se le aplican.
- Lo mismo para otras entradas de fuera de la sesión. Todo lo que Claude Code registra como que no viene de la persona de esta sesión recibe esos mismos permisos restringidos: un aviso de un canal MCP, una llamada remota, un mensaje que pone en cola un plugin, un mensaje reenviado desde la bandeja de otra sesión, y cualquier tipo de entrada que aipager todavía no conozca (un comando de un plugin cuenta como entrada externa aunque lo hayas arrancado tú). Los propios agentes en segundo plano de la sesión (lo que devuelven y sus informes) y sus avisos de tareas conservan los permisos del trabajo.
- Un mensaje que programó la sesión se ejecuta con los permisos de la tarea que lo programó. Cuando una tarea configura un
/loopo un cron, aipager guarda los permisos de esa tarea para el mensaje; cuando se lanza más tarde, recibe esos permisos, nunca más, haya hablado quien haya hablado la última vez; un mensaje que se suma a esa tarea solo puede restringirla más, y un cambio de reglas que le quita permisos a quien la programó también la restringe. El mismo mensaje programado dos veces se queda con los permisos más estrictos. Un/loopsin texto se lanza con las instrucciones de bucle del propio Claude Code en lugar de su propio texto, así que recibe los permisos más estrictos de todos los bucles sin texto que configuró la sesión, una vez que la conversación guardada muestra el lanzamiento (hasta entonces una herramienta rechazada se rechaza con suavidad, y Claude lo vuelve a intentar). Un mensaje que nadie programó pasando por la comprobación de aipager recibe los permisos restringidos. - El texto no demuestra nada. El nombre de un remitente, una línea
[via Telegram · @owner]o «el propietario pide…» dentro de un mensaje no cambian nada. A Claude se le dice a quién verificó aipager como remitente, y que lo que afirma el texto no es una prueba. - Sumarse a una tarea en curso solo la restringe. Un mensaje de otra sesión que llega mientras hay una tarea en curso deja esa tarea igual de estricta que la más estricta de las dos, nunca más relajada. Eso se cumple cuando Claude Code toma el mensaje sin ningún evento de mensaje propio (sumado a la tarea en curso, o ejecutado como la siguiente tarea desde su cola): la vía rápida del propietario mira el final de la conversación guardada antes de cada herramienta, y el final de la tarea y aipager marcan la sesión para revisarla cuando ven un mensaje así en la cola.
- Los roles se quedan en su chat. Un recibo de una sesión de otro chat solo cuenta cuando su tarea tenía los permisos del propietario o se escribió en su terminal; todo lo demás se ejecuta restringido.
- Quién puede enviar. Los roles restringidos incorporados no pueden usar SendMessage en absoluto. Un rol al que una política se lo dé solo puede escribir a sesiones que ejecuta aipager (con la dirección
uds:…/<pid>.sock);aipager doctoravisa de un rol así. - Los agentes en segundo plano conservan sus propios permisos. Un agente conserva los permisos de la tarea que lo arrancó (o los más estrictos de una tarea posterior), nunca los de una tarea posterior que tenga más.
- Cuando se detiene. Si las reglas de seguridad detienen una tarea que empezó otra sesión, la tarjeta de actividad ya muestra quién la envió, el aviso de parada lo dice, y quien entregó la tarea se entera, para que nunca desaparezca en silencio: el chat de la sesión que la envió cuando las dos sesiones están en el mismo chat; si no, solo el propietario, en privado, y solo cuando el propietario (o la terminal) manejaba la tarea que la envió.
Límites conocidos:
- Una tarea con shell (la del propietario o la de un admin) se ejecuta con el mismo usuario del sistema que aipager y podría falsificar un recibo, igual que ya podía reescribir los demás archivos de control de aipager. Es el modelo de confianza que ya existe: dale un shell solo a las personas a las que les confiarías la computadora.
- aipager no gobierna en absoluto las sesiones de Claude que no ejecuta, les escriba quien les escriba.
- Fuera de Linux, el remitente se empareja solo por su número de proceso (Claude Code no registra ahí la hora de inicio). En Linux, un registro de recepción sin hora de inicio no coincide con nada.
- Claude Code puede escribir el mensaje en su conversación un momento después de la primera herramienta de la tarea. Hasta que lo hace, una ejecución que rechazarían los permisos restringidos se rechaza con suavidad (a Claude se le dice que lo vuelva a intentar; la tarea no se detiene). Después de 8 ejecuciones sin él, la tarea se ejecuta restringida.
- La vía rápida del propietario lee el último 1 MiB de la conversación guardada, nunca más. Un mensaje de otra sesión más largo que eso termina en una línea que la vía rápida toma por uno (cualquier línea de más de 256 KiB que corte la ventana lo es). Uno que queda entero más atrás (más de 1 MiB de conversación escrita después de él antes de la siguiente herramienta) lo atrapa aipager mientras la tarea está en curso, o el final de la tarea mientras sigue en la cola, no la vía rápida.
- Mientras una sesión tiene configurado un
/loopsin texto, cualquier tarea que no arranque un mensaje de Telegram (incluido un mensaje escrito en la terminal) puede tener sus primeras herramientas rechazadas con suavidad, hasta que Claude Code escriba el mensaje de esa tarea y aipager pueda saber que no es el bucle que se lanza; Claude entonces lo vuelve a intentar. - aipager lee la tarea de un mensaje programado a partir de lo que ganó la conversación guardada desde que empezó; pasados 8 MiB, de sus primeros y últimos 4 MiB (lo que queda en medio se comprobó cuando era la parte más reciente).
- Los permisos guardados de un mensaje programado se quedan hasta que la sesión se termina con
/kill: una sesión que terminó de otra forma, o que se reanudó, los conserva, igual que Claude Code conserva sus crons y una activación pendiente de/loop. Se guardan como mucho 256 por sesión, y programar otro más se rechaza para todos los de esa sesión, así que cualquiera que pueda programar puede agotarlos (entonces arranca una sesión con otro nombre, o haz/killde esta). Un mensaje que se lanza sin registro recibe los permisos restringidos o, en una sesión con un/loopsin texto, los de ese bucle, una vez que Claude Code lo escribe en la conversación guardada; hasta entonces aipager lee la tarea por la anterior. Eso pasa con un mensaje configurado antes de esta versión; después de reiniciar la computadora (los registros viven en la carpeta temporal del sistema, que un reinicio borra, mientras que Claude Code todavía puede recuperar la activación de una sesión reanudada dentro de la hora); y con un cron duradero, que Claude Code ejecuta en otra sesión de la misma carpeta cuando su propia sesión ya terminó. Una sesión con menos permisos que programe el mismo texto puede bajar los permisos de un mensaje (nunca subirlos). - Una sesión nueva creada con el nombre de una anterior (Reemplazar) conserva los registros de esa sesión, incluidos los permisos de su
/loopsin texto, hasta/kill. - Un mensaje programado que Claude Code ejecuta justo después de otra tarea, sin un evento de mensaje propio (desde su cola), recibe los más estrictos entre sus propios permisos y los de la tarea a la que sigue (después de una tarea escrita en la terminal, exactamente los suyos).
- Las primeras herramientas de una tarea escrita en la terminal pueden rechazarse hasta que Claude Code escriba su mensaje en la conversación guardada (hasta entonces aipager lee la tarea anterior).
- Un agente en segundo plano arrancado con permisos más estrechos puede devolver texto a una tarea posterior del mismo trabajo, que lo lee con los permisos del trabajo, igual que con los demás avisos de tareas de Claude Code.
- Claude Code puede retener un mensaje para aprobarlo en la terminal que lo recibe (su ajuste
crossSessionInbound, o cuando las dos sesiones funcionan en clases de permisos distintas, por ejemplo Acceso total y Automático). aipager no puede liberar un mensaje así desde Telegram; el recibo espera hasta una hora.
Resumen de amenazas#
| Amenaza | Cómo se mitiga |
|---|---|
| Un desconocido le envía un comando al bot | El filtro de ID de chat lo rechaza |
| Roban el token del bot | /revoke en @BotFather y volver a configurar |
| Una ejecución de herramienta comprometida en claude | La comprobación de seguridad de aipager rechaza lo que las reglas de una tarea de Telegram rechazan; en Modo Preguntar, las propias reglas de Claude Code en settings.json deciden sobre qué pregunta y el único permiso es el toque de una persona; en Modo Automático (el de por defecto de toda sesión nueva) la comprobación de seguridad propia de Claude Code ejecuta sin preguntar las llamadas que considera seguras y detiene las riesgosas para una persona; una sesión en Acceso total (admins, y cada persona en su propio chat privado) no pregunta nada, así que solo se interpone la comprobación de seguridad de aipager, y para las tareas propias de un propietario y los mensajes de terminal nada (quién decide) |
| Un usuario restringido de Telegram se hace pasar por el propietario | Sin shell, sesiones solo dentro de la carpeta raíz del propio miembro, escrituras y búsquedas limitadas al proyecto dentro de ella (nunca la carpeta personal), ninguna lectura en la carpeta personal fuera de la carpeta raíz, carpetas raíz de otros miembros y archivos subidos de otros chats inalcanzables, archivos de credenciales ilegibles, rutas protegidas para las herramientas de archivos, las comprobaciones que fallan rechazan (cómo se aplican las reglas en equipo); solo de mejor esfuerzo para un rol con Bash, y sin cubrir para los casos de «Límites conocidos» |
| El mensaje de una sesión dice tener la autoridad de un propietario | El mensaje se ejecuta con los permisos demostrados de la tarea que lo envía, nunca con los anteriores de la sesión que lo recibe; sin prueba, solo leer y pensar (mensajes entre sesiones) |
| Un miembro de un grupo toma prestados los permisos de otro | Cada mensaje se ejecuta con el rol de quien lo envía en el chat del que viene; el mensaje de otra persona espera a la tarea en curso; si aun así uno se suma, ganan las reglas más estrictas; los comandos de barra de los roles restringidos se limitan a los del teclado (cómo se aplican las reglas en equipo) |
| La charla del grupo llega a una sesión | En un grupo solo se atienden los comandos, las respuestas al bot, las menciones al bot y los toques marcados del teclado (uso en equipo) |
| Un miembro quitado conserva el acceso | aipager config vuelve a cargar la configuración de aipager sin reiniciar; sus mensajes retenidos se descartan y una tarea suya que sigue en curso pierde sus permisos anteriores; sacarlo del grupo de Telegram le impide seguir leyéndolo (uso en equipo → recargar la configuración) |
| Manipulación del registro de auditoría | Solo crece; evitarlo sin un programa de firma aparte queda fuera de este diseño |
| Una inyección de prompt hace que Claude escriba una dirección que saca datos | Las imágenes y las etiquetas multimedia en el texto de Claude se muestran como enlaces o como texto, nunca como imagen, video o audio, y las vistas previas de enlaces están desactivadas en los mensajes de texto normal y HTML (conexiones de red) |
| Atacante en la red | Ningún puerto de entrada, no se puede llegar directamente |
| Escalada de privilegios local | Ni sudo ni setuid; aipager se queda en el espacio del usuario |
| El audio se filtra a la nube | Se pasa a texto en esta computadora |
| El agente lee la credencial de Claude del propio aipager | Esperado, no es un bug; mira La credencial de Claude. Limita la propia credencial, no el archivo donde se guarda |
Ver también#
- Arquitectura: cómo funciona el proceso.
- Eventos de hook: lo que aipager ve de verdad de claude.
- Comandos del bot: el lado que manejas tú.