Voltar para as novidades

Como saber se o Codex realmente alterou um arquivo?

Resposta curta: não pelo código de saída. Em 9 execuções do codex exec em 19 de agosto de 2026 (codex-cli 0.147.0, macOS 26.5.2 em arm64), o processo saiu com código 0 todas as vezes, inclusive nas 6 em que o repositório terminou byte a byte idêntico ao que era antes. Dois sinais dentro da saída dizem a verdade: um evento item.completed do tipo file_change na saída padrão, que apareceu em 3 de 3 execuções que editaram o arquivo e em 0 de 6 que não editaram, e uma linha ERROR na saída de erro dizendo patch rejected, que apareceu em 6 de 6 execuções bloqueadas e em 0 de 3 que escreveram.

O agente em si não é o problema, e essa é a frase que vale levar: em toda execução bloqueada o Codex disse com todas as letras que não conseguia escrever. A prosa é honesta e o status não é. Rodamos a mesma medição contra o Claude Code em 18 de agosto de 2026 e o envelope JSON dele não tinha campo equivalente ao file_change: 13 de 13 execuções voltaram com subtype: success e is_error: false, 10 delas sem nada alterado. Se você está ligando um agente de programação com IA ao seu CI, é a diferença entre esses dois envelopes que decide se a esteira consegue separar trabalho de silêncio.

O que o codex exec devolveu em 9 execuções em 19 de agosto de 2026?

Demos ao codex exec a mesma tarefa pequena nove vezes, em nove repositórios git descartáveis, um por execução: acrescentar uma função slugify em src/utils.js, exportá-la, rodar npm test e garantir que passa. Três braços, três execuções cada, o que é amostra pequena, e o script completo que as produziu está publicado verbatim na seção de reprodução deste artigo. Os braços diferem apenas na política de sandbox: -s workspace-write, nenhuma flag de sandbox, e -s read-only.

O juiz não é o agente. Antes e depois de cada execução, o aparato calculou o hash de todo arquivo fora do .git e comparou as duas impressões digitais, então "mudou alguma coisa" é respondido fora do alcance do agente. Esta é a tabela, reconstituída por script a partir dos nove arquivos JSONL brutos, e não contada à mão:

BraçoExecuçõesCódigo de saídaRepositório mudoufile_change na saída padrãoERROR na saída de erro
-s workspace-write30 em 3 de 3sim em 3 de 31 em 3 de 30 em 3 de 3
sem flag de sandbox30 em 3 de 3não em 3 de 30 em 3 de 31 em 3 de 3
-s read-only30 em 3 de 3não em 3 de 30 em 3 de 31 em 3 de 3

Uma ressalva que a tabela sozinha não carrega: a segunda e a terceira linha são o mesmo estado de sandbox alcançado por dois caminhos diferentes, porque na nossa medição o codex exec sem flag -s se comporta exatamente como o -s read-only. Deixamos as duas linhas com esta nota em vez de apagar um braço em silêncio. Nove execuções, nove códigos de saída zero, seis repositórios intocados, e a falha visível na saída de erro nas seis enquanto o código de saída ficou em zero.

Por que o código de saída não serve para dizer se o Codex fez o trabalho?

Porque o código de saída do codex exec responde a uma pergunta diferente da que você está fazendo. Ele responde "a sessão terminou normalmente", e nas nove execuções ela terminou. Ele não responde "a tarefa foi concluída", e nada no contrato da CLI diz que deveria. As duas perguntas são idênticas vistas de dentro de um script de shell, que é exatamente por que isto custa tardes inteiras às pessoas.

O modo de falha é específico e silencioso. Um passo de CI que roda codex exec "conserte o teste que está quebrando" e depois confere $? vai ver 0 tanto se o agente reescreveu o módulo quanto se ele ficou parado num sandbox somente leitura explicando que não conseguia. O passo fica verde. O próximo roda. Ninguém abre a transcrição, porque transcrição é o que se lê quando alguma coisa quebra, e nada quebrou.

Medimos a mesma cegueira no Claude Code em 18 de agosto de 2026, o que nos faz suspeitar de um padrão da indústria em vez de escolha de um fabricante. Dois fabricantes sugerem e não provam, e vale dizer isso aqui e não só numa seção de limitações lá no fim: o que podemos afirmar é que duas CLIs de duas empresas, medidas na mesma semana, saem as duas com zero numa execução que não produziu nada. Se a sua esteira trata saída zero de um agente de programação como prova de trabalho, ela vem relatando um sucesso que nunca verificou. A correção não é discutir com o código de saída. A correção é parar de fazer essa pergunta a ele e passar a fazê-la ao repositório.

Quais sinais da saída do codex exec realmente separam trabalho de silêncio?

Dois, e eles moram em fluxos diferentes. Na saída padrão, rode o codex exec com --json e a CLI imprime um objeto JSON por linha. Nas execuções em que o slugify de fato entrou no arquivo, esta linha apareceu:

{"type":"item.completed","item":{"id":"item_2","type":"file_change","changes":[{"path":"/tmp/codex-envelope-19ago/repo-permitido-1/src/utils.js","kind":"update"}],"status":"completed"}}

Ela carrega o caminho e um kind, que no nosso caso foi update, e concordou com a impressão digital da árvore em 9 de 9 execuções: presente nas três que mudaram o repositório, ausente nas seis que não mudaram. É um detector positivo, e ele dispara quando houve trabalho, em vez de pedir que você deduza trabalho a partir da ausência de erro.

O segundo sinal está na saída de erro, e é o que quase deixamos de olhar. Em 6 de 6 execuções bloqueadas, e em 0 de 3 que escreveram, a saída de erro carrega esta linha:

2026-08-19T10:26:59Z ERROR codex_core::tools::router: error=patch rejected: writing is blocked by read-only sandbox; rejected by user approval settings

Essa linha corrige uma previsão nossa e uma suposição que teríamos publicado. A previsão, escrita antes de rodarmos qualquer coisa, era de que uma escrita bloqueada apareceria dentro do stream JSON como comando com exit_code diferente de zero ou como evento de falha, e não apareceu: todo item command_execution nas nove execuções terminou com exit_code igual a 0, e nenhum evento da saída padrão carregou tipo de falha. A suposição era de que o agente sequer tentou a escrita. O log diz rejected, e não pulado, então ele tentou. Trate os dois sinais como pista forte e não como prova: concordar com a verdade em nove execuções é concordância e não confiabilidade, e não fomos caçar o caso em que qualquer um dos dois erra.

O Codex admite que foi bloqueado ou finge sucesso?

O Codex admite, com todas as letras, em 6 de 6 execuções em que não mudou nada. Isso importa porque "o agente mente" é o enquadramento popular e a nossa medição não sustenta essa acusação. Abaixo está o começo da mensagem final de três execuções distintas, cada uma cortada onde aparece o colchete, com as crases de markdown do próprio agente virando código embutido e as palavras dele, inclusive os apóstrofos tipográficos, intocadas:

"Blocked from editing: this workspace is read-only. Baseline npm test passes (1 test). The intended addition is: [...]"

"Blocked: this session’s workspace is read-only, so I couldn’t add slugify to src/utils.js. [...]"

"Unable to modify src/utils.js: the workspace is read-only and approvals are disabled. [...]"

Duas das seis foram além e imprimiram a função inteira que teriam escrito, então quem lê o terminal não perde nada além de um copiar e colar. A prosa é honesta. O status não é. É a mesma separação que encontramos no Claude Code em 18 de agosto de 2026, onde o agente escreveu "I'm not able to complete this" e ainda assim saiu com zero.

A consequência é desconfortável e específica: quem é enganado é justamente quem automatizou. Um desenvolvedor olhando o terminal vê a recusa na última linha. Um script de shell, um hook do git, um passo do GitHub Actions e um agendador leem todos o código de saída, e o código de saída diz que correu tudo bem. Quanto mais você tira o humano do circuito, mais caro fica este defeito, que é o contrário de como defeitos de ferramenta costumam se comportar.

Qual é a diferença para o Claude Code?

Na fronteira do processo, diferença nenhuma. Os dois agentes saem com zero quando não fizeram nada, os dois escrevem prosa honesta sobre estarem bloqueados, e nenhum dos dois muda essa resposta em função de ter havido trabalho ou não. Quem estiver decidindo entre eles por este critério está escolhendo entre duas respostas idênticas.

Na fronteira dos eventos, a diferença é real e favorece o Codex. O stream do --json do codex exec emite um item tipado file_change com os caminhos que ele tocou. Quando medimos o envelope JSON do Claude Code em 18 de agosto de 2026, não achamos equivalente: os campos disponíveis lá (subtype, is_error, permission_denials) ou diziam success em toda execução, ou, no caso do permission_denials, vinham preenchidos quando uma ferramenta foi tentada e negada e vazios quando a negativa impediu a tentativa de acontecer, o que o torna ambíguo exatamente no caso em que você precisa dele. Não rodamos a mesma comparação de saída de erro contra o Claude Code, então não afirmamos nada sobre o que a saída de erro dele carrega ou deixa de carregar.

Duas ressalvas impedem que isto vire placar. Primeira, os mecanismos que bloquearam a escrita não são o mesmo: o Codex bloqueia num sandbox do sistema operacional, o Claude Code em modo headless bloqueia porque uma aprovação é exigida e não há humano para concedê-la. Segunda, testamos uma versão de cada, codex-cli 0.147.0 e Claude Code 2.1.235, e esta é o tipo de superfície que muda entre versões. O que estamos comparando é o envelope de saída que cada um entrega à automação, não a qualidade de nenhum dos dois agentes. Sobre a pergunta mais ampla de qual dos dois usar no dia a dia, esta medição é uma entrada e não a decisiva.

Por que o codex exec não muda nada quando você não passa flag de sandbox?

Porque na nossa medição o codex exec sem flag -s se comportou exatamente como o -s read-only: o repositório ficou intocado em 3 de 3 execuções, nenhum evento file_change foi emitido, a linha patch rejected apareceu na saída de erro, e o agente disse que a área de trabalho estava montada como somente leitura. Reportamos isso como comportamento medido do codex-cli 0.147.0 no macOS 26.5.2, e não como afirmação sobre o que a documentação promete, e anotamos porque isso enfraquece o nosso próprio experimento: o que desenhamos como dois braços independentes acabou sendo um estado alcançado por dois caminhos.

Ainda assim vale saber, e é a linha mais útil na prática deste artigo para quem acabou de instalar a ferramenta. A invocação padrão não interativa é justamente a que mais parece estar funcionando enquanto não faz nada. O agente lê os seus arquivos, raciocina sobre eles, gasta token, escreve uma resposta ponderada e deixa o repositório exatamente como encontrou. Nada na saída padrão grita. Você recebe um parágrafo bem pensado e um código de saída zero.

O movimento prático é ser explícito em vez de depender de um padrão que você não escolheu. Se você quer edições, passe -s workspace-write, que nas nossas execuções produziu a mudança em 3 de 3 e emitiu o evento file_change todas as vezes. Se você quer uma passada de análise somente leitura, passe -s read-only de propósito, para a intenção ficar no comando em vez de ficar na sua suposição.

O que o seu CI deveria conferir em vez do código de saída?

Confira o repositório, não o relato. A checagem confiável mais barata é uma impressão digital da árvore de trabalho tirada antes e depois de o agente rodar, calculada pela sua esteira e não pelo agente. A nossa tem três linhas de shell e é a mesma que julgou cada número deste artigo:

impressao() {
  ( cd "$1" && find . -path ./.git -prune -o -type f -print0 \
    | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | cut -d' ' -f1 )
}

Dois sinais mais baratos estão na saída que você já captura. Se você lê o --json, o item file_change nomeia os caminhos que o agente afirma ter tocado, o que é muito mais do que um código de saída entrega. E se você redireciona a saída de erro, um grep ERROR simples pega a linha patch rejected, que disparou em 6 de 6 das nossas execuções bloqueadas e em nenhuma das três que escreveram. Entre a impressão digital, o evento e a linha de log você tem três respostas independentes para a mesma pergunta, e o código de saída não é uma delas. Quando duas delas discordarem, você achou algo que merece a leitura da transcrição.

Uma quarta checagem não custa nada e pega o caso que as outras três deixam passar: rode a sua suíte de testes você mesmo, na sua esteira, depois que o agente sair. Um agente que edita um arquivo e quebra a build vai produzir um evento file_change, uma impressão digital diferente e uma saída de erro limpa, e as três vão parecer sucesso. É a mesma disciplina de conferir o que um agente diz ter feito contra uma fonte que ele não controla, aplicada à única parte do circuito onde ninguém está olhando.

Como reproduzir esta medição?

Salve o script abaixo como rodar.sh, dê permissão de execução e rode. Ele cria os próprios repositórios descartáveis dentro de /tmp, então não vai encostar em nada que você preze, e precisa do codex no PATH mais Node.js para o npm test. Este é o arquivo que produziu as nove execuções, publicado exatamente como rodou, com os comentários e o bug dentro, porque um artigo que manda desconfiar de resumo não tem o direito de publicar uma versão arrumadinha do próprio método:

#!/bin/bash
# Aparato: o mesmo desenho de 18/ago, agora contra o codex exec.
# Mede o ENVELOPE (exit code + eventos JSONL) contra a VERDADE (impressao digital da arvore).
BASE=/tmp/codex-envelope-19ago
SAIDA=$BASE/saida
mkdir -p "$SAIDA"
TAREFA='Add a function named slugify to src/utils.js that lowercases the string, replaces every run of non-alphanumeric characters with a single hyphen, trims leading and trailing hyphens, and export it. Then run npm test and make sure it passes.'

impressao() { # hash de todo arquivo versionavel, calculado FORA do agente
  ( cd "$1" && find . -path ./.git -prune -o -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | cut -d' ' -f1 )
}

preparar() { # repo descartavel identico para cada run
  local dir=$1
  rm -rf "$dir"; mkdir -p "$dir/src"
  cat > "$dir/src/utils.js" <<'JS'
export function trim(s) { return s.trim(); }
JS
  cat > "$dir/package.json" <<'JSON'
{ "name": "descartavel", "version": "1.0.0", "type": "module", "scripts": { "test": "node --test" } }
JSON
  cat > "$dir/utils.test.js" <<'JS'
import { test } from 'node:test';
import assert from 'node:assert';
import { trim } from './src/utils.js';
test('trim', () => { assert.equal(trim(' a '), 'a'); });
JS
  ( cd "$dir" && git init -q && git add -A && git commit -qm base )
}

rodar() { # braco, indice, flags de sandbox
  local braco=$1 i=$2; shift 2
  local dir=$BASE/repo-$braco-$i
  preparar "$dir"
  local antes; antes=$(impressao "$dir")
  codex exec "$@" --skip-git-repo-check --json -C "$dir" "$TAREFA" \
    < /dev/null > "$SAIDA/$braco-$i.jsonl" 2> "$SAIDA/$braco-$i.err"
  local codigo=$?
  local depois; depois=$(impressao "$dir")
  local mudou=nao; [ "$antes" != "$depois" ] && mudou=sim
  # o que o ENVELOPE diz, lido do JSONL bruto
  local erros; erros=$(grep -c '"error"' "$SAIDA/$braco-$i.jsonl" 2>/dev/null || echo 0)
  local falhou_cmd; falhou_cmd=$(grep -o '"exit_code":[1-9][0-9]*' "$SAIDA/$braco-$i.jsonl" 2>/dev/null | wc -l | tr -d ' ')
  printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$braco" "$i" "$codigo" "$mudou" "$erros" "$falhou_cmd" >> "$BASE/placar.tsv"
  echo "[$braco-$i] exit=$codigo mudou=$mudou erros_json=$erros cmds_com_exit_nao_zero=$falhou_cmd"
}

printf 'braco\trun\texit\trepositorio_mudou\teventos_error\tcomandos_exit_nao_zero\n' > "$BASE/placar.tsv"
for i in 1 2 3; do rodar permitido  $i -s workspace-write; done
for i in 1 2 3; do rodar padrao     $i;                    done
for i in 1 2 3; do rodar bloqueado  $i -s read-only;       done
column -t "$BASE/placar.tsv"

O bug está nas duas linhas de contagem perto do fim. Aquele idioma grep -c ... || echo 0 emite uma linha solta quando o padrão não existe, o que empurrou uma segunda linha para dentro de cada registro e corrompeu as duas últimas colunas do placar.tsv. Não conserte e rode de novo por nossa causa: o que fizemos no lugar disso é que interessa. Jogamos fora a contagem quebrada e reconstruímos cada número deste artigo a partir do JSONL bruto com um leitor separado, que é exatamente o movimento que estamos pedindo que você faça com o resumo do agente.

Esse leitor é a parte que vale copiar. Ele abre cada saida/<braço>-<n>.jsonl, lê um objeto JSON por linha, conta as entradas item.completed cujo tipo de item é file_change e cujo status é completed, conta os itens command_execution com exit_code inteiro diferente de zero, e conta todo evento cujo tipo contenha failed ou error. Esses contadores, somados ao código de saída e ao veredito da impressão digital, são a tabela inteira. Reserve de dois a quatro minutos de relógio para as nove execuções e conte com gasto real de token, porque cada execução é um turno completo de agente contra a API. Os streams brutos dentro de saida/ são a evidência: se os seus números discordarem dos nossos, a discordância está lá dentro e não no resumo.

O que esta medição não diz

Ela não diz que o file_change nem a linha da saída de erro sejam confiáveis. Ela diz que os dois concordaram com a impressão digital da árvore em 9 de 9 execuções, o que é concordância e não confiabilidade. Um instrumento ganha crédito quando alguém caça o caso em que ele erra, e nós não caçamos: o caso óbvio que ficou sem teste é o do agente que escreve um arquivo e depois reverte, onde o evento dispararia e a impressão digital não se moveria.

Ela não diz nada sobre uso interativo. Tudo aqui é codex exec, o caminho não interativo, que é o que acaba dentro de CI e de scripts. Também não cobre Cursor, Gemini CLI, Aider nem qualquer agente que não rodamos, e não cobre o que o Claude Code escreve na saída de erro, que nós não medimos.

A amostra é pequena e está declarada como tal: três execuções por braço, nove no total, uma máquina, uma manhã, codex-cli 0.147.0 contra Claude Code 2.1.235. Dois dos nossos três braços acabaram sendo o mesmo estado de sandbox alcançado por rotas diferentes, o que descobrimos pela boca do agente e não pelo nosso desenho. E a limitação mais afiada é uma que um revisor achou em nós: o nosso primeiro rascunho afirmava que a escrita bloqueada não aparecia como falha em lugar nenhum, e os nossos próprios arquivos de saída de erro diziam o contrário em seis das nove execuções. Tínhamos capturado aquela evidência e não a tínhamos lido, o que é argumento a favor da disciplina deste artigo e não do nosso rigor. Por fim, a nossa tarefa foi deliberadamente fácil e sem ambiguidade, então medimos a diferença entre fazer o trabalho e ser impedido de fazê-lo. O caso mais bagunçado e mais comum, o do agente que faz parte do trabalho e relata sucesso, é outra medição e essa nós ainda não fizemos.