El asistente que actúa#
En los capítulos anteriores usaste el asistente para responder preguntas, generar código y refinar prompts. Eso es útil. Pero los asistentes de código como Claude Code van un paso más allá: no solo responden, sino que actúan. Leen tus ficheros, los editan y ejecutan comandos en tu terminal.
Eso los convierte en agentes.
Un agente no espera a que copies y pegues su respuesta: cierra el bucle él mismo. Le dices “añade un test para esta función” y lo escribe, lo guarda en el fichero correcto y te muestra el resultado. Esa capacidad de actuar es lo que hace útil a un agente de verdad.
Y también es lo que exige que entiendas qué puede hacer por su cuenta.
Qué pueden hacer los agentes: los tres tipos de operación#
Cuando el asistente actúa sobre tu proyecto, sus operaciones caen en tres categorías:
Leer. Abrir un fichero, buscar en el directorio, ver el diff de git. Son operaciones de solo lectura: el agente puede ver cosas, pero no cambia nada. El riesgo es bajo.
Escribir. Crear un fichero nuevo, editar uno existente, moverlo o borrarlo. Aquí el agente modifica el estado de tu proyecto. El riesgo es real: un error se graba en disco.
Ejecutar. Correr un comando en la terminal: npm install, npm test, git commit. El agente
actúa sobre el sistema operativo, no solo sobre tus ficheros. El riesgo es el más alto: un
comando puede instalar paquetes, hacer push, o borrar cosas.
Permisos: tú decides el alcance#
Los permisos del agente son la forma de decirle hasta dónde puede llegar sin preguntarte. En Claude Code puedes configurar qué tipo de operaciones están permitidas por defecto y cuáles requieren que las apruebes una a una.
La lógica es simple: a más permiso, más fluidez en la sesión. A menos permiso, más control sobre lo que ocurre.
Hay dos extremos y un espacio entre ellos:
Máximo control. El agente te pide confirmación antes de cada escritura y cada ejecución. Ves exactamente lo que va a hacer antes de que ocurra. La sesión es más lenta, pero nunca hay sorpresas.
Máxima autonomía. El agente actúa dentro de los permisos que le has dado sin interrumpirte. Trabaja más rápido, pero también puede tocar más cosas de las que esperabas si interpreta la tarea de forma amplia.
La configuración de permisos en Claude Code vive en el fichero settings.json del proyecto
(dentro de .claude/). El nivel de autonomía es una decisión tuya, no del modelo.
Modos del agente: cómo se configura la autonomía#
Los modos del agente son configuraciones predefinidas que agrupan conjuntos de permisos. En lugar de configurar operación por operación, eliges un modo que define el perfil general de la sesión.
El modo más restrictivo pide confirmación en cada paso que modifica algo. El modo más autónomo actúa dentro de unos límites definidos sin interrumpirte. Entre ellos hay configuraciones intermedias.
Cambiar de modo no cambia la inteligencia del modelo: cambia el alcance de lo que puede hacer sin pedirte permiso. Cuál usar depende de la tarea y de qué parte del proyecto estás tocando.
Para tareas de exploración y análisis, más autonomía tiene poco riesgo: el agente solo lee. Para tareas que modifican código de autenticación, base de datos o configuración de producción, más control tiene más sentido aunque sea más lento.
La regla innegociable: revisar siempre#
Aquí viene la parte más importante del capítulo, y del nivel entero.
Todo lo que genere el agente lo revisas tú antes de darlo por bueno.
No importa que el código compile. No importa que los tests pasen. No importa que el agente sea muy capaz. Tú eres el senior que firma. Esa responsabilidad no la delega nadie.
La razón no es desconfianza ciega. Es que el agente y tú no veis las mismas cosas. El agente optimiza para que el código funcione en los casos que tiene en contexto. Tú conoces los casos límite que no están en los tests, las restricciones de seguridad del proyecto, la historia de por qué una parte del código está escrita así.
Cuando el agente genera código que “funciona”, eso significa que funciona en los escenarios que el agente probó o razonó. No significa que no haya casos que no vio.
La revisión no es un trámite. Es la capa de control que hace que el uso de IA sea seguro.
Patrones peligrosos del código generado por IA#
El código que genera el agente puede tener problemas específicos. No son errores al azar: son patrones que aparecen una y otra vez porque tienen algo en común. Conocerlos te permite revisarlos de forma sistemática.
Secretos en el cliente#
Un agente puede generar una llamada a una API con la clave directamente en el código del
cliente, en un fichero .js que el navegador descarga:
// Generado por la IA: funciona, pero la clave queda expuesta.
const respuesta = await fetch("https://api.ejemplo.com/datos", {
headers: { Authorization: "Bearer sk-abc123xyz" },
});El código funciona porque la llamada responde correctamente. El problema es que ese fichero llega al navegador en texto plano. Cualquiera puede abrir las herramientas de desarrollo y leer la clave. Habrás visto este patrón en el nivel de seguridad frontend: las claves van en el servidor, nunca en el cliente.
Falta de sanitización y XSS#
Cuando el agente genera código que muestra datos del usuario, puede usar innerHTML sin
escapar el contenido:
// La IA lo genera así: el más corto camino a "funciona".
function mostrarHeroe(nombre) {
contenedor.innerHTML = "<h2>" + nombre + "</h2>";
}Si nombre viene de un input que el usuario puede controlar y contiene HTML o JavaScript,
ese código lo ejecuta. Es XSS (Cross-Site Scripting). La alternativa segura es textContent
o escapar el valor antes de insertarlo. La IA no siempre elige el camino seguro: elige el
más directo.
Dependencias que no existen#
El agente puede proponer instalar paquetes que suenan plausibles pero que no están en npm:
# La IA propone esto con confianza total.
npm install react-winrate-calculatorNunca instales una dependencia que propone la IA sin verificar primero que existe en
npmjs.com y que es el paquete real (no uno que imita el nombre de otro).
La consecuencia no es solo que el npm install falle. Los modelos alucinan los mismos
nombres plausibles una y otra vez, y hay quien registra esos nombres a propósito para colar
código malicioso a quien los instala a ciegas. Por eso verificar no es manía: en el peor
caso, la diferencia está entre un comando que falla y uno que ejecuta código de un atacante
en tu máquina y en tu pipeline.
”Funciona pero está mal”#
Es el patrón más insidioso. El código pasa todos los tests, se comporta bien en los casos que usas a diario, y tiene un fallo que solo aparece en condiciones específicas:
// Funciona con héroes que tienen partidas. Revienta con partidas === 0.
function winrate(heroe) {
return (heroe.victorias / heroe.partidas) * 100;
}Este es exactamente el ejercicio que vas a hacer ahora. La función parece correcta porque en
el 99% de los casos lo es. El 1% restante da NaN o Infinity en silencio.
Parches que esconden el problema#
Cuando un código lanza una excepción, el agente a veces propone silenciar el error en lugar de resolver la causa:
// La función fallaba con null. La IA lo "arregla" así.
function procesarHeroe(heroe) {
if (!heroe) return;
// ... resto de la lógica
}El crash desaparece. Pero el dato que debería procesarse ahora se ignora en silencio. El usuario no ve error, pero tampoco ve resultado. Los tests del tipo “no lanza excepción” pasan en verde mientras el flujo de datos tiene un agujero.
La metáfora que cierra el nivel#
A lo largo de este nivel hemos descrito al asistente como un junior brillante: rápido, con acceso a mucho conocimiento, capaz de hacer en minutos lo que llevaría horas.
Ahora puedes completar la metáfora.
Un junior brillante tiene sus propias ideas sobre cómo hacer las cosas. Puede tomar atajos que no conoce el contexto del proyecto. Puede entregar código que funciona en los casos que probó y pasa por alto el caso límite que tú conoces porque lo viste explotar en producción hace dos años.
Tú eres el senior. Tu trabajo no es hacer todo tú mismo: es dar dirección clara, aprovechar la velocidad del equipo y revisar el trabajo antes de que salga. El asistente propone. Tú firmas.
Eso no cambia con la IA. Lo que cambia es la velocidad a la que llega el trabajo a tu mesa.
Comprueba lo que sabes#
Pregunta 1 de 17
¿Qué distingue a un agente de IA de un simple chatbot de preguntas y respuestas?
Tu turno#
El agente generó esta función del Team Builder. A primera vista parece correcta. Ejecútala, observa qué ocurre con el héroe sin partidas y corrígela. Las soluciones muestran tres formas de hacerlo: de la más simple a la más expresiva.
Ejercicio · en esta página
Revisa este código de la IA
El asistente generó esta función del Team Builder. A primera vista parece correcta, pero tiene un fallo que pasa desapercibido en el caso feliz. Ejecútala, observa el resultado con el héroe sin partidas y corrígela.
Paso 1: Que no reviente
- La función devuelve un número válido (no NaN ni Infinity) cuando el héroe no tiene partidas.
- Los héroes con partidas siguen mostrando su winrate correcto.
- Al pulsar "Ejecutar" ya no aparece NaN en la consola.
Paso 2: Que comunique la ausencia de dato
- La función devuelve null cuando no hay partidas (no 0, que sería una mentira: cero victorias no es lo mismo que sin datos).
- Hay una función de formateo separada que convierte el número o null en texto para mostrar.
- La salida en consola distingue visualmente entre "0.0%" y "sin datos".
Paso 3: Que sea robusto y expresivo
- La firma de winrate anota el tipo de retorno: number | null. El contrato queda en el código, no en un comentario.
- La función de formateo también tiene firma explícita: recibe Heroe y devuelve string.
- Los comentarios explican el POR QUÉ de cada decisión, no solo el qué: por qué null y no 0, por qué separar cálculo de presentación.
Ver soluciones
// Motor de estadísticas del Team Builder.
// Solución OK: protege contra la división por cero devolviendo 0.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const heroes: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 95, victorias: 61 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 80, victorias: 52 },
];
const heroeNuevo: Heroe = {
nombre: "Echo",
rol: "Daño",
partidas: 0,
victorias: 0,
};
// Comprueba si el héroe tiene partidas antes de dividir.
// Si partidas es 0, devuelve 0 en lugar de NaN.
function winrate(heroe: Heroe): number {
if (heroe.partidas === 0) {
return 0;
}
return (heroe.victorias / heroe.partidas) * 100;
}
for (const h of heroes) {
console.log(h.nombre + ": " + winrate(h).toFixed(1) + "%");
}
console.log("Winrate de " + heroeNuevo.nombre + ": " + winrate(heroeNuevo).toFixed(1) + "%"); Por qué este nivel
- Detecta el caso límite y devuelve 0 en lugar de dividir entre cero: la función ya no revienta.
- Es la corrección mínima: el bug desaparece y el código vuelve a ser utilizable.
- Su límite: 0% y "sin datos" son indistinguibles. Si un héroe pierde todas sus partidas, también devuelve 0; no hay forma de saber desde fuera si tiene 0 victorias o ninguna partida. Es un problema silencioso que puede aparecer más tarde.
// Motor de estadísticas del Team Builder.
// Solución mejor: anuncia explícitamente el caso sin datos con null,
// y el código que llama decide cómo mostrarlo.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const heroes: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 95, victorias: 61 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 80, victorias: 52 },
];
const heroeNuevo: Heroe = {
nombre: "Echo",
rol: "Daño",
partidas: 0,
victorias: 0,
};
// Devuelve null cuando no hay partidas todavía.
// Así el que llama sabe que no hay dato, no que el héroe tiene 0% de winrate.
// Un 0 puede confundirse con "pierde siempre"; null dice "sin datos".
function winrate(heroe: Heroe): number | null {
if (heroe.partidas === 0) {
return null;
}
return (heroe.victorias / heroe.partidas) * 100;
}
// Formatea el resultado: muestra el porcentaje o un guion si no hay datos.
function formatearWinrate(heroe: Heroe): string {
const wr = winrate(heroe);
if (wr === null) {
return "sin datos";
}
return wr.toFixed(1) + "%";
}
for (const h of heroes) {
console.log(h.nombre + ": " + formatearWinrate(h));
}
console.log("Winrate de " + heroeNuevo.nombre + ": " + formatearWinrate(heroeNuevo)); Por qué es mejor que el anterior
- Usa null para decir "no hay dato": es semánticamente honesto. 0 y null son cosas distintas en este dominio.
- Separa la responsabilidad: winrate calcula, formatearWinrate decide cómo mostrarlo al usuario. Si el día de mañana quieres mostrar "—" en vez de "sin datos", solo cambias un sitio.
- Su límite: la firma de winrate no anota el tipo de retorno explícitamente; TypeScript lo infiere, pero el contrato no es visible de un vistazo.
// Motor de estadísticas del Team Builder.
// Solución excelente: el tipo expresa la ausencia de dato,
// la función tiene firma explícita y el formateo está separado.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const heroes: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 95, victorias: 61 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 80, victorias: 52 },
];
const heroeNuevo: Heroe = {
nombre: "Echo",
rol: "Daño",
partidas: 0,
victorias: 0,
};
// Firma explícita: devuelve number (porcentaje) o null (sin partidas jugadas).
// El tipo de retorno en la firma es el contrato: si alguien cambia la lógica
// y accidentalmente devuelve NaN, TypeScript no lo atrapará solo, pero el null
// sí deja claro que "sin dato" es un caso legítimo que el que llama debe tratar.
function winrate(heroe: Heroe): number | null {
if (heroe.partidas === 0) {
return null;
}
return (heroe.victorias / heroe.partidas) * 100;
}
// Formatea para mostrar: responsabilidad separada de calcular.
// winrate calcula; formatearWinrate decide cómo presentar al usuario.
function formatearWinrate(heroe: Heroe): string {
const wr = winrate(heroe);
// null significa "sin partidas", no "0% de victorias"; son casos distintos.
if (wr === null) {
return "sin datos";
}
return wr.toFixed(1) + "%";
}
// Muestra todos los héroes, incluyendo los que no tienen partidas todavía.
for (const h of heroes) {
console.log(h.nombre + " [" + h.rol + "]: " + formatearWinrate(h));
}
console.log("Winrate de " + heroeNuevo.nombre + ": " + formatearWinrate(heroeNuevo)); Por qué es mejor que el anterior
- La firma number | null en winrate es el contrato: cualquier colega (o el asistente en la siguiente sesión) sabe sin ejecutar el código que hay dos casos posibles.
- Los comentarios explican el razonamiento, no el mecanismo: "null significa sin partidas, no 0% de victorias; son casos distintos" es información que no se puede deducir del código solo.
- Cada función tiene una responsabilidad única: calcular o presentar, nunca las dos. Eso hace que cada pieza sea testeable y reemplazable por separado.