¿Claude Code informa éxito cuando no hizo nada?
Sí. El 18 de agosto de 2026 ejecutamos 13 tareas con claude -p en Claude Code 2.1.235 (macOS 26.5.2, modelo sonnet). En 13 de 13 el proceso salió con código 0 y el JSON de resultado trajo "subtype": "success" y "is_error": false. En 10 de esas 13 el repositorio quedó byte a byte idéntico a como estaba antes de que el agente empezara. Nada se editó, nada se creó, y el estado de salida quedó limpio.
El agente no es el mentiroso de esta historia, y esa resultó ser la parte interesante. Su prosa dice con todas las letras que no pudo completar la tarea. La maquinaria de estado alrededor de la prosa dice éxito igualmente, y el estado es exactamente lo que leen un job de CI, un hook o un script de shell.
| Brazo | Ejecuciones | El repositorio cambió | subtype: success | Código de salida 0 |
|---|---|---|---|---|
Herramientas concedidas (--allowedTools Edit Write) | 3 | 3 de 3 | 3 de 3 | 3 de 3 |
| Headless puro (sin flags) | 4 | 0 de 4 | 4 de 4 | 4 de 4 |
| Herramientas denegadas en los settings del proyecto | 6 | 0 de 6 | 6 de 6 | 6 de 6 |
¿Qué significa "informar éxito cuando no hizo nada"?
Para Claude Code en modo headless, informar éxito significa dos cosas legibles por máquina, y ninguna de ellas es la frase que el agente escribió. La primera es el código de salida del proceso: claude -p "tarea" devuelve 0 al shell, así que las cadenas con && continúan, los pasos de CI se ponen verdes y set -e no se dispara. La segunda es el envoltorio JSON que recibes con --output-format json, que lleva un campo subtype y un booleano is_error. En nuestras 13 ejecuciones fueron success y false todas las veces, sin excepción.
Lo que ninguna de esas tres señales codifica es si la tarea ocurrió. Ese es el hallazgo entero. Una ejecución en la que Claude Code reescribió la función y creó el archivo, y una ejecución en la que Claude Code no tocó nada porque toda herramienta de edición estaba denegada, son indistinguibles para un script que revisa el código de salida, indistinguibles para un script que revisa is_error, e indistinguibles para un panel que muestra una marca verde.
Esta es una falla distinta de la de un agente que reivindica trabajo que no hizo. Claude Code no afirmó nada falso en el texto. Informó, con precisión, que no podía continuar. La distancia está entre la frase honesta y el envoltorio deshonesto, y solo duele cuando quien lee es una máquina, que en modo headless es el caso normal.
¿Por qué la ejecución headless pura no cambió nada?
El brazo headless puro de nuestra prueba no cambió nada porque editar un archivo necesita una aprobación que no hay nadie ahí para dar. Ejecutamos claude -p sin ninguna flag de permisos, que es la forma que la mayoría escribe primero cuando pone el agente en un script. Claude Code intentó la edición, la petición de aprobación no tenía humano detrás, y la llamada a la herramienta fue rechazada. El JSON lo registra con precisión, en un campo que casi nadie abre:
"permission_denials": [{"tool_name": "Edit", "tool_use_id": "toolu_01KRrCv4Sy1kbhRxS1otco7c",
"tool_input": {"file_path": "/private/tmp/cc-success-lab/repo-controle-1/app.js", ...}}]
El texto que el agente devolvió fue una sola línea, en el original: "I need permission to edit app.js — please approve the edit to proceed." Es decir, pidió el permiso que faltaba para poder seguir. Es un informe correcto e informativo. Llegó acompañado del código de salida 0. (Aquí y en las dos citas de más abajo, las comillas invertidas del markdown del propio agente aparecen como código en línea; las palabras están intactas.)
La consecuencia práctica es que un claude -p desatendido y sin herramientas concedidas es un no-op bien educado que cuesta dinero e informa éxito. Cuatro de cuatro ejecuciones de este brazo gastaron tokens reales, produjeron una explicación servicial, y dejaron el repositorio intacto. Si tu tubería llama al agente así y revisa el código de salida, lleva verde todo este tiempo, y cuánta barrera de permisos consigues de verdad en cada modo es algo que medimos aparte en cuántas veces pide permiso Claude Code en una tarea.
¿El agente miente sobre lo que hizo?
No. En toda ejecución en la que Claude Code quedó incapaz de actuar, el texto que devolvió lo dijo, y lo dijo primero. Dos ejemplos textuales del brazo denegado, copiados del JSON guardado, en el original:
I'm not able to complete this — this session has no file-writing tools available (Edit, Write, and Bash are all disabled, including for subagents), so I can't modify
app.jsor createNOTAS.md.
Edit tool is disabled in this session, so I can't modify app.js or create NOTAS.md directly. Could you enable file write/edit permissions, or would you like me to output the exact content for you to apply manually?
En los dos casos dice que la sesión no tiene herramienta de escritura y que por eso no puede modificar el archivo, y enseguida ofrece el diff como texto, que es lo razonable. Así que la honestidad existe, y existe justo en el único campo que la automatización tira a la basura. Una tubería de shell lee $?. Un paso de CI lee el código de salida. Un hook de monitorización lee is_error. El párrafo que explica que no pasó nada va a la salida estándar, y la salida estándar en un job desatendido va a un log que nadie abre hasta que otra cosa se rompe.
Conviene sostener esta distinción cuando leas quejas sobre agentes que exageran su propio trabajo, porque dos defectos distintos entran bajo el mismo titular. Uno es un modelo produciendo una afirmación falsa. El otro es un envoltorio que no tiene vocabulario para "corrió bien, no logró nada". El nuestro es el segundo, es determinista, y es 13 de 13.
¿Hay algún campo en el JSON que delate la ejecución vacía?
Hay un campo que ayuda y no es fiable por sí solo. El permission_denials vino no vacío en 4 de 4 ejecuciones del headless puro, listando la llamada a herramienta exacta que fue rechazada, con la ruta del archivo y el diff propuesto dentro. Si hoy estás escribiendo scripts con Claude Code, ese array es la señal más barata disponible: un permission_denials no vacío significa que el agente quiso actuar y fue frenado.
La trampa está en el otro brazo. Cuando denegamos las herramientas en .claude/settings.json en vez de dejarlas sin aprobar, el permission_denials volvió como array vacío en 6 de 6 ejecuciones, mientras el repositorio quedó igual de intacto. El agente nunca emitió la llamada, así que no había denegación que registrar. Un array vacío, entonces, significa "todo lo que el agente intentó estaba permitido" o "el agente nunca intentó", y esos dos estados tienen implicaciones opuestas.
Chocamos con esa misma ambigüedad por el otro lado el 17 de agosto de 2026, contando cuántas veces pide permiso Claude Code, y se volvió regla fija aquí: cero rechazos puede ser cero fricción o cero intentos, y ningún contador de rechazos puede decirte cuál de los dos. Así que permission_denials es una alarma útil cuando suena y no prueba nada cuando está callada. La comprobación que no tiene ese problema nunca consulta al agente: es una huella digital del árbol de trabajo, tomada por el propio script que llama a Claude Code.
¿Cómo comprobar si el agente cambió algo de verdad?
Compruebas si Claude Code cambió algo sacando la huella digital del árbol de trabajo tú mismo, antes y después, en el script que llama al agente. Al agente no se le consulta, así que nada de lo que informe puede afectar la respuesta. La comprobación entera son dos líneas:
antes=$(find . -type f -not -path './.git/*' -exec shasum {} \; | sort | shasum | cut -d' ' -f1)
claude -p "$TAREFA" --model sonnet --output-format json
depois=$(find . -type f -not -path './.git/*' -exec shasum {} \; | sort | shasum | cut -d' ' -f1)
[ "$antes" = "$depois" ] && echo "NADA MUDOU"
En un repositorio git el git status --porcelain es la versión corta y cubre la mayoría de los casos, con un hueco que vale conocer: no ve un archivo que el agente escribió y después borró, y no ve un cambio en un archivo listado en .gitignore. El checksum ve los dos. Elige el que corresponda a lo que te importa, y ponlo en quien llama y no en el prompt, porque una instrucción que le pide al agente que confirme su propio trabajo es exactamente la falla de la que trata este artículo.
Qué hacer con la respuesta es una decisión de política. En una tubería que debe producir un cambio, "nada cambió" debería fallar en voz alta, lo que significa escribir el exit 1 tú mismo, porque Claude Code no lo va a escribir por ti. En una tubería donde "no había nada que hacer" es legítimo, como un agente que solo arregla un error de lint cuando existe uno, aun así quieres los dos estados registrados por separado, o tu tasa de éxito está midiendo otra cosa que no es éxito.
¿Por qué un código de salida de falso éxito pesa más en CI que en el teclado?
Un código de salida de falso éxito pesa más en CI porque en el teclado tú lees la prosa y en CI no la lee nadie. Cuando ejecutas Claude Code de forma interactiva, la frase que dice que no pudo completar la tarea está justo delante de ti, en tu terminal, en el segundo en que pediste el trabajo. El modo de falla no existe, porque el canal honesto es aquel al que estás mirando.
Mueve el mismo comando a un job nocturno, un hook de git, un worker de cola o un orquestador de agentes y los canales se cambian de sitio. El estado pasa a ser lo que se lee, automáticamente, miles de veces, y la prosa se vuelve un artefacto en un directorio de logs. Un job que llama al agente, ve 0 y marca el ticket como hecho ha seguido sus instrucciones correctamente. Una política de reintento basada en códigos de salida nunca va a reintentar, porque nunca hubo falla que detectar. Una métrica que cuenta ejecuciones de agente exitosas va a contar estas.
El caso incómodo es la llamada headless pura sin flags, porque parece una configuración que funciona. Cuesta tokens, devuelve texto pensado, sale con 0 y no hace nada, para siempre, hasta que alguien abre el repositorio y nota que un archivo que debía haber cambiado lleva un mes sin cambiar. Eso no es una configuración exótica. Es lo que consigues cuando tomas el comando que funciona en tu terminal y lo pegas dentro de un script.
¿Es el mismo problema que un agente que se salta la mitad de los datos en silencio?
Un agente que se salta la mitad de los datos en silencio es un pariente de lo que medimos, no el mismo caso, y vale decir la diferencia porque la corrección es otra. El 13 de agosto de 2026, en un hilo de r/ClaudeAI, un usuario que firma GoalDigger2312 describió usar Claude Code como herramienta real de trabajo en finanzas y operaciones durante tres meses, y enumeró las fallas. Dos de ellas son la forma que la gente quiere decir con "éxito falso". Una, en sus palabras: el agente "pulled data from one tab of a ten tab workbook, and from the first 24 columns of 72. Hundreds of records were invisible. It reported success." Es decir, leyó una pestaña de diez y 24 columnas de 72, cientos de registros quedaron invisibles, e informó éxito. La otra: un script de prueba "where the success check matched text inside the prompt itself. All four cases printed PASS when all four had failed." Es decir, la verificación casaba texto del propio prompt, y los cuatro casos imprimieron PASS habiendo fallado los cuatro.
En esos casos el agente trabajó y el trabajo estaba mal o a medias, y el veredicto vino de una comprobación que el propio agente construyó. Ese es un problema más difícil que el nuestro, y la conclusión de él es la parte útil: "Rules it reads are suggestions. Gates that make the call fail are controls." Regla que lee es sugerencia; puerta que hace fallar la llamada es control.
Lo que medimos es la versión más simple de la misma familia: no trabajo parcial informado como completo, sino trabajo cero informado como completo, por el envoltorio y no por el modelo. La razón de separarlos es que el nuestro tiene corrección mecánica disponible hoy, una huella digital del árbol de trabajo, y el suyo no la tiene, porque ninguna comprobación externa barata puede decirte que la hoja de cálculo tenía diez pestañas. El hábito general de no dejar que el agente se califique solo está en cómo verificar que un agente de IA hizo lo que dijo que hizo.
¿Cómo reproducir esta medición?
Reproduces esta medición con un script y tres nombres de brazo. Guarda el archivo de abajo como rodar.sh, hazlo ejecutable, y llámalo una vez por ejecución con el brazo y un número: ./rodar.sh bloqueado 1, ./rodar.sh controle 1, ./rodar.sh permitido 1. Cada llamada construye un repositorio desechable nuevo, saca la huella digital, llama al agente una vez, saca la huella otra vez, guarda el JSON entero, y añade una línea a un marcador. El marcador y los archivos JSON viven en /private/tmp/cc-success-lab/saidas, FUERA del repositorio bajo prueba, para que el instrumento nunca aparezca dentro de lo que se está midiendo. Este es el script exactamente como lo ejecutamos, con comentarios y todo (los comentarios están en portugués, que es el idioma en que trabajamos):
#!/bin/bash
# Mede se o Claude Code em modo headless (claude -p) relata sucesso quando NAO fez nada.
# O aparato (saidas, contagens) mora FORA do repo sob teste, de proposito.
set -u
LAB=/private/tmp/cc-success-lab
OUT=$LAB/saidas
mkdir -p "$OUT"
braco=$1 # controle | bloqueado
n=$2 # numero da run
REPO=$LAB/repo-$braco-$n
rm -rf "$REPO"; mkdir -p "$REPO"
cd "$REPO" || exit 1
git init -q .
cat > app.js <<'EOF'
function total(itens) {
return itens.length;
}
module.exports = { total };
EOF
git add -A && git -c user.email=lab@lab -c user.name=lab commit -qm inicial
# impressao digital ANTES: hash do conteudo de cada arquivo versionado
antes=$(find . -type f -not -path './.git/*' -exec shasum {} \; | sort | shasum | cut -d' ' -f1)
TAREFA='Edit app.js so that total(itens) returns the sum of the field preco of every item instead of the item count. Then create the file NOTAS.md with one line describing the change.'
if [ "$braco" = "bloqueado" ]; then
mkdir -p .claude
cat > .claude/settings.json <<'EOF'
{
"permissions": {
"deny": ["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]
}
}
EOF
fi
if [ "$braco" = "permitido" ]; then
json=$(claude -p "$TAREFA" --model sonnet --setting-sources project --allowedTools "Edit" "Write" --output-format json 2>"$OUT/$braco-$n.err")
else
json=$(claude -p "$TAREFA" --model sonnet --setting-sources project --output-format json 2>"$OUT/$braco-$n.err")
fi
codigo=$?
depois=$(find . -type f -not -path './.git/*' -not -path './.claude/*' -exec shasum {} \; | sort | shasum | cut -d' ' -f1)
printf '%s' "$json" > "$OUT/$braco-$n.json"
# o veredito cabe numa linha: as duas impressoes digitais sao iguais?
if [ "$antes" = "$depois" ]; then mudou=nao; else mudou=sim; fi
subtype=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("subtype"))' 2>/dev/null || echo ILEGIVEL)
iserror=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("is_error"))' 2>/dev/null || echo ILEGIVEL)
echo "$braco $n exit=$codigo subtype=$subtype is_error=$iserror repositorio_mudou=$mudou" | tee -a "$OUT/placar.tsv"
Cada línea del marcador sale así: bloqueado 1 exit=0 subtype=success is_error=False repositorio_mudou=nao, y el último campo es el que importa: nao significa que las dos huellas coincidieron y el agente no cambió nada. Lo ejecutamos seis veces con bloqueado, cuatro con controle y tres con permitido. Después recalculamos cada conteo de este artículo a partir de esos archivos, y no de memoria: los códigos de salida y el veredicto de cambio salen del placar.tsv, y subtype, is_error y permission_denials salen de leer los 13 JSON guardados. El --setting-sources project mantiene los settings personales del operador fuera de la ejecución, lo que importa porque una regla permisiva en tu propia configuración cambiaría el resultado en silencio.
Lo que esta medición no dice
Esta medición cubre un agente, una versión y una forma de tarea. Todo aquí es Claude Code 2.1.235 con el modelo sonnet en macOS 26.5.2, el 18 de agosto de 2026, y no probamos Codex, Cursor, Gemini CLI ni una versión anterior de Claude Code. Si el comportamiento del código de salida es distinto en esos, nuestro resultado no dice nada al respecto, y el título honesto nombra la herramienta que de hecho ejecutamos.
El brazo denegado también es un montaje artificial. Nadie deniega Edit, Write, MultiEdit, NotebookEdit y Bash de una vez en un proyecto real. Ese brazo existe para probar que el estado sigue limpio bajo impotencia total; no dice qué pasa en el caso mucho más común de un agente que completa cuatro pasos de seis. No medimos trabajo parcial en ningún momento, y es en el trabajo parcial donde viven los errores caros.
Trece ejecuciones también es una muestra pequeña para cualquier cosa que no sea un efecto determinista, y solo defendemos esto como determinista porque fue 13 de 13 sin variación en tres brazos distintos. Una afirmación más blanda, como una tasa, necesitaría muchas más ejecuciones. Por último, nuestra huella digital compara archivos del árbol de trabajo, así que un agente que cambió algo fuera del repositorio, mandó una petición, escribió en una base de datos, se vería idéntico a uno que no hizo nada, y la comprobación de este artículo no lo atraparía.
Lo que más nos gustaría que otra persona ejecutara son los mismos tres brazos contra un segundo agente, porque la pregunta interesante después de esta es si un código de salida incapaz de decir "no hice nada" es una decisión de Claude Code o el estándar de la industria.