Claude Code en Telegram: el puente mínimo para hablar con tu agente desde el móvil
Tabla de contenidos
Hermes Agent y OpenClaw prometen lo mismo: un agente que vive en un servidor, al que le escribes por Telegram y que hace cosas por ti. Si ya usas Claude Code a diario, la mitad de eso ya la tienes. El modelo, las herramientas, la memoria en Markdown y las skills están en tu terminal. Lo que falta es poder hablarle sin estar delante.
En este post montamos esa parte: un bot de Telegram y un puente de 70 líneas en Node que lleva cada mensaje a Claude Code y te devuelve la respuesta. Sin dependencias y sin frameworks de bots. Lo he probado con Claude Code 2.1, y la conversación de las capturas es la salida real de esas pruebas.
Al terminar tendrás esto funcionando:
Tú, en Telegram ──► bot ──► puente.mjs ──► claude -p ──► ~/agente
▲ │
└──── respuesta ◄────────┘
Y así se ve en el móvil. Es una captura real de mi bot de pruebas, ya con la versión completa del agente: por eso habla de tareas programadas, que en este post no montamos.

Una carpeta para el agente ¶
Claude Code trabaja siempre en una carpeta, y lee el CLAUDE.md que encuentra en ella. Así que lo primero es darle una casa propia, separada de tus proyectos:
mkdir -p ~/agente/.claude && cd ~/agente && git initCon git tienes un historial de todo lo que el agente cambie en su casa. Dentro van tres ficheros. El primero le dice quién es y cómo tiene que contestar:
# Quién eresEres el asistente personal de Dani. Te escribe desde Telegram, en el móvil, así que contesta corto:pocas líneas, sin tablas y sin Markdown que Telegram no pinte.Esta carpeta es tu casa. Lo que tengas que recordar entre conversaciones, apúntalo en `NOTAS.md`.## Límites- No lees ni copias ficheros `.env`, claves o tokens.- No haces `git push`, no publicas nada y no escribes a nadie que no sea Dani.- Si algo necesita un permiso que no tienes, dilo y explica qué comando querías lanzar.El segundo, NOTAS.md, es su memoria larga. Empieza con una línea:
# Notas- Dani vive en Madrid (hora de Europe/Madrid).El tercero son sus permisos. Aquí está la primera diferencia con usar Claude Code en la terminal: el puente lo lanza en modo no interactivo, y en ese modo nadie puede contestar a una pregunta de permiso. Lo que no esté permitido de antemano se deniega sin preguntar. Por eso le dejamos editar sus notas, y nada más:
{ "permissions": { "allow": [ "Edit(NOTAS.md)", "Write(NOTAS.md)" ], "deny": [ "Read(**/.env)", "Read(**/.env.*)", "Bash(git push:*)", "Bash(sudo:*)" ] }}Con esto el agente puede leer los ficheros de su carpeta, contestarte y apuntar cosas en NOTAS.md. No puede lanzar comandos ni editar otros ficheros.
El bot de Telegram ¶
Telegram te da un bot en dos minutos:
- Abre una conversación con @BotFather, escribe
/newboty elige un nombre. Te devuelve un token del tipo123456:ABC-…. - Escríbele cualquier cosa a tu bot nuevo.
- Abre en el navegador
https://api.telegram.org/bot<TOKEN>/getUpdates, con tu token. En el JSON, busca"chat":{"id":…}. Ese número es tuchat.id.
Con el token y tu chat.id ya tienes las dos variables que necesita el puente. Con el token, cualquiera maneja tu bot. No lo guardes dentro de ~/agente, donde el agente puede leerlo.
El puente ¶
El puente hace de cartero. Pregunta a Telegram si hay mensajes nuevos, lanza claude -p en la carpeta del agente con cada uno y manda la respuesta de vuelta. Usa el fetch que trae Node 22, así que no hay nada que instalar:
#!/usr/bin/env node// Puente mínimo Telegram ⇄ Claude Code. Cada mensaje lanza `claude -p` en la carpeta del agente// y retoma la sesión de ese chat con --resume, para que la conversación tenga memoria.// TG_TOKEN=… TG_CHAT=123456 node puente.mjs ~/agente// Sin dependencias: Node 22 (fetch nativo) y el comando `claude` con la sesión iniciada.import { spawn } from 'node:child_process';import { readFileSync, writeFileSync, existsSync } from 'node:fs';import { join } from 'node:path';const API = `${process.env.TG_API ?? 'https://api.telegram.org'}/bot${process.env.TG_TOKEN}`;const ALLOWED = new Set((process.env.TG_CHAT ?? '').split(',').filter(Boolean));const HOME_DIR = process.argv[2] ?? process.cwd();const CLAUDE = process.env.CLAUDE_BIN ?? 'claude';const STATE_FILE = join(HOME_DIR, '.puente.json'); // chat → sesión de Claude Codeconst sessions = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : {};const save = () => writeFileSync(STATE_FILE, JSON.stringify(sessions, null, 2));async function telegram(method, body) { const res = await fetch(`${API}/${method}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (!json.ok) throw new Error(`${method}: ${json.description}`); return json.result;}async function reply(chat, text) { for (let i = 0; i < text.length; i += 4000) await telegram('sendMessage', { chat_id: chat, text: text.slice(i, i + 4000) });}function askClaude(chat, text) { const args = ['-p', text, '--output-format', 'json']; if (sessions[chat]) args.push('--resume', sessions[chat]); return new Promise((done) => { const child = spawn(CLAUDE, args, { cwd: HOME_DIR, stdio: ['ignore', 'pipe', 'pipe'] }); let out = '', err = ''; child.stdout.on('data', (d) => (out += d)); child.stderr.on('data', (d) => (err += d)); child.on('close', (code) => { let r; try { r = JSON.parse(out); } catch { return done(`Error de claude (${code}): ${(err || out).trim().slice(0, 500)}`); } if (r.session_id) { sessions[chat] = r.session_id; save(); } const denied = (r.permission_denials ?? []).map((d) => d.tool_name); const note = denied.length ? `\n\n(sin permiso para: ${[...new Set(denied)].join(', ')})` : ''; done((r.result || `(sin respuesta: ${r.subtype})`) + note); }); });}let offset = 0;console.log(`puente listo · carpeta ${HOME_DIR} · chats permitidos: ${[...ALLOWED].join(', ')}`);for (;;) { let updates = []; try { updates = await telegram('getUpdates', { offset, timeout: 30, allowed_updates: ['message'] }); } catch (e) { console.error(e.message); await new Promise((r) => setTimeout(r, 5000)); continue; } for (const { update_id, message: msg } of updates) { offset = update_id + 1; if (!msg?.text) continue; const chat = String(msg.chat.id); if (!ALLOWED.has(chat)) { console.log(`ignorado: chat ${chat}`); continue; } if (msg.text === '/nuevo') { delete sessions[chat]; save(); await reply(chat, 'Conversación nueva.'); continue; } console.log(`→ ${chat}: ${msg.text.slice(0, 60)}`); const typing = setInterval(() => telegram('sendChatAction', { chat_id: chat, action: 'typing' }).catch(() => {}), 4000); const t0 = Date.now(); const answer = await askClaude(chat, msg.text).finally(() => clearInterval(typing)); console.log(`← ${chat}: ${answer.length} caracteres en ${((Date.now() - t0) / 1000).toFixed(1)} s`); await reply(chat, answer); }}Lo que hace cada sección:
- Configuración.
TG_CHATes la lista de chats a los que el bot hace caso. Cualquiera que encuentre tu bot puede escribirle, y sin esta lista un desconocido le daría órdenes a un agente que vive en tu máquina. Los mensajes de otros chats ni siquiera llegan a Claude Code: el bucle los descarta antes. - Telegram. Dos llamadas a la Bot API, una para leer y otra para escribir. Telegram no acepta mensajes de más de 4.096 caracteres, así que las respuestas largas salen en trozos de 4.000.
- Claude Code.
claude -pejecuta un turno y termina. Con--output-format jsondevuelve la respuesta, elsession_idde la conversación y la lista de herramientas que se le han denegado. El puente guarda esesession_idpor chat en.puente.jsony lo pasa con--resumeen el siguiente mensaje: así Claude Code retoma la conversación en vez de empezar de cero. - Bucle.
getUpdatescontimeout: 30es long polling: la petición se queda abierta hasta que llega un mensaje o pasan 30 segundos. No necesitas un servidor con dominio ni un webhook. Mientras el agente piensa, el bot enseña «escribiendo…».
Añade .puente.json al .gitignore de ~/agente y arráncalo:
echo .puente.json >> ~/agente/.gitignoreTG_TOKEN=123456:ABC-tu-token TG_CHAT=tu-chat-id node puente.mjs ~/agenteclaude -p usa la sesión de Claude Code que ya tienes iniciada, así que cada mensaje gasta de tu suscripción como cualquier otra conversación. Las condiciones del uso no interactivo han cambiado varias veces este año: antes de dejarlo encendido, mira la página de facturación de tu cuenta.
La carpeta tiene que ser de confianza ¶
Aquí falla casi todo el mundo la primera vez. Con el puente en marcha, le pedí al agente que apuntara una cosa en sus notas:

La regla allow estaba bien escrita, y aun así la edición se denegó (señalador 1). Al lanzar claude -p a mano en esa carpeta sale el motivo:
Ignoring 1 permissions.allow entry from .claude/settings.json: this workspace has not been trusted.
Claude Code no aplica las reglas allow de un proyecto hasta que confías en su carpeta. Así, un repositorio que clonas no se da permisos a sí mismo con su settings.json. Las reglas deny, en cambio, se aplican siempre: en la prueba, la del .env bloqueó la lectura en la misma carpeta sin confianza. Ese aviso sale por la salida de errores y en el modo no interactivo no hay diálogo que te lo pregunte. En Telegram solo ves la denegación. Por eso el puente añade al final qué herramienta se le negó (señalador 2). Sin esa línea, solo tendrías lo que el modelo cuenta de lo que le ha pasado.
La solución es abrir Claude Code una vez en esa carpeta, de forma interactiva, y aceptar el diálogo de confianza:
cd ~/agente && claudeResponde que sí a «¿confías en esta carpeta?», sal con /exit y vuelve a probar desde el móvil.
Memoria corta y memoria larga ¶
Con la carpeta de confianza, el mismo mensaje funciona. Y la prueba enseña los dos tipos de memoria que tiene ya este montaje:

- La conversación (señalador 2). «¿Qué te acabo de pedir?» funciona porque
--resumeretoma la misma sesión. Es la memoria corta: dura hasta que escribes/nuevo, que borra la sesión de ese chat. - Las notas (señalador 3). Después de
/nuevo, el agente ya no recuerda la conversación, pero sigue sabiendo que trabajas con Laravel porque lo lee enNOTAS.md. Es la memoria larga, y vive en un fichero que puedes abrir y corregir.
Las respuestas de tu agente serán distintas a las de las capturas: el modelo no contesta dos veces igual. Lo que tiene que coincidir es lo que pasa con los permisos y la memoria.
Lo que este puente todavía no hace ¶
Tienes un Claude Code que te contesta en el móvil, con memoria y con una lista de quién puede hablarle. Es un buen chat. Pero le faltan cosas para ser un agente:
- Solo habla cuando le hablas. No puede avisarte de nada por su cuenta ni despertarse a una hora. No hay reloj.
- No puede pedirte permiso. Todo lo que no está en
allowse deniega. Si le dasBashpara que haga cosas de verdad, las reglasdenydejan de ser una barrera: unheado unpython3 -cllegan al mismo fichero que bloquea uncat. - Atiende los mensajes de uno en uno. Mientras trabaja en algo largo, lo que escribas espera.
- Vive mientras viva tu terminal. Si la cierras, el bot se calla.
Con las piezas del artículo completo, la conversación cambia. Otra captura real del móvil, con el agente negándose a enseñar el fichero donde está su token:

Qué incluye el artículo completo ¶
El resto es un artículo premium para suscriptores de Web Reactiva. Convierte este chat en un agente que trabaja cuando no estás, con un servidor que no necesita ventana, y lo compara con Hermes Agent haciendo el mismo encargo. Todo está probado, con más de 30 capturas de salida real y el código entero:
- Un agente que se programa sus propias tareas, y por qué la primera falló
- Avisos de releases, caza de bugs en un log de producción, un resumen de newsletters y una utilidad pedida desde el sofá
- Un portero que hace que las vigilancias vacías gasten cero tokens
- Preguntas de permiso por Telegram, con el comando delante
- Un prompt injection escondido en el log, y lo que hizo el agente
- El sandbox que esconde los secretos y el agujero que destapó un bot real
- Hermes Agent con el mismo encargo: tiempos, aprobaciones y tokens
Artículo premium
Tu propio Hermes u OpenClaw con Claude Code, T3 Code y un bot de Telegram
Lo que has leído es el canal: un bot que lleva tus mensajes a Claude Code. El reloj, los cuatro casos de developer, los permisos por Telegram, el sandbox y la comparativa con Hermes están en un artículo premium que puedes comprar.
- El agente que se programa sus tareas
- Cuatro casos de developer, probados
- Vigilancias que no gastan si no pasa nada
- Permisos por Telegram y un sandbox de verdad
- Hermes Agent con el mismo encargo
12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.