Voltar para as novidades

Dá para desfazer o que um agente de IA fez no seu repositório?

O git devolve exatamente o que ele chegou a ver ao menos uma vez, e a linha divisória é o índice, não o commit. Em 15 de agosto de 2026, com o git 2.50.1, a gente destruiu o mesmo pedaço de trabalho de seis jeitos diferentes, em seis repositórios descartáveis, e tentou recuperar. Quatro voltaram e dois sumiram. Os dois que sumiram nunca tinham passado por um git add. É essa a regra inteira: um único git add que você nem chegou a commitar já basta para recuperar o arquivo, e o que um agente de IA criou e destruiu sem passar pelo índice nunca existiu do ponto de vista do git.

O script que destrói o trabalho que um agente de IA poderia destruir

A gente escreveu um script que cria um repositório novo para cada cenário, coloca a mesma string dentro de um arquivo, destrói aquilo com um comando diferente em cada caso e depois tenta recuperar. Nada aqui pede que você acredite na nossa palavra: este é o script inteiro, e rodar ele é o ponto.

#!/usr/bin/env bash
# What git gives back after an AI coding agent destroys work.
# The six scenarios live in one temporary directory, created once and
# removed on exit. The check after mktemp is not ceremony: git -C ""
# does not fail, it falls back to the current directory.
set -uo pipefail

LAB=$(mktemp -d)
[ -n "$LAB" ] && [ -d "$LAB" ] || { printf 'could not create a temporary directory, aborting\n' >&2; exit 1; }
trap 'rm -rf "$LAB"' EXIT

new_repo() {
  d="$LAB/$1"
  git init -q "$d"
  git -C "$d" config user.email lab@example.com
  git -C "$d" config user.name lab
  printf 'base\n' > "$d/tracked.txt"
  git -C "$d" add tracked.txt
  git -C "$d" commit -qm base
  printf '%s' "$d"
}

GOLD='work worth keeping'
check() { [ "$2" = "$GOLD" ] && printf '%-44s %s\n' "$1" "RECOVERED" || printf '%-44s %s\n' "$1" "LOST"; }

# 1. The agent committed, then reset --hard threw the commit away.
r=$(new_repo one)
printf '%s\n' "$GOLD" > "${r}/tracked.txt"
git -C "$r" commit -qam work
git -C "$r" reset -q --hard HEAD~1
git -C "$r" reset -q --hard 'HEAD@{1}'
check "committed, killed by reset --hard" "$(cat "${r}/tracked.txt" 2>/dev/null)"

# 2. The agent edited a tracked file, never staged it, checkout threw it away.
r=$(new_repo two)
printf '%s\n' "$GOLD" > "${r}/tracked.txt"
git -C "$r" checkout -q -- tracked.txt
check "edited, never staged, git checkout --" "$(cat "${r}/tracked.txt" 2>/dev/null)"

# 3. Same edit, but it reached the index once before being thrown away.
r=$(new_repo three)
printf '%s\n' "$GOLD" > "${r}/tracked.txt"
git -C "$r" add tracked.txt
git -C "$r" reset -q --hard HEAD
b=$(git -C "$r" fsck --lost-found 2>/dev/null | awk '/dangling blob/ {print $3; exit}')
check "edited, git add once, reset --hard" "$(git -C "$r" cat-file -p ${b} 2>/dev/null)"

# 4. The agent created a new file, never added it, git clean removed it.
r=$(new_repo four)
printf '%s\n' "$GOLD" > "${r}/newfile.txt"
git -C "$r" clean -qfd
check "created, never added, git clean -fd" "$(cat "${r}/newfile.txt" 2>/dev/null)"

# 5. The agent deleted the branch its commits lived on.
r=$(new_repo five)
git -C "$r" checkout -q -b feature
printf '%s\n' "$GOLD" > "${r}/tracked.txt"
git -C "$r" commit -qam feature-work
git -C "$r" checkout -q -
git -C "$r" branch -qD feature
s=$(git -C "$r" reflog 2>/dev/null | awk '/feature-work/ {print $1; exit}')
check "committed on a branch, branch -D" "$(git -C "$r" show ${s}:tracked.txt 2>/dev/null)"

# 6. The agent stashed the work and then dropped the stash.
r=$(new_repo six)
printf '%s\n' "$GOLD" > "${r}/tracked.txt"
git -C "$r" stash -q
git -C "$r" stash drop -q
c=$(git -C "$r" fsck --lost-found 2>/dev/null | awk '/dangling commit/ {print $3; exit}')
check "stashed, then git stash drop" "$(git -C "$r" show ${c}:tracked.txt 2>/dev/null)"

printf '\ngit %s\n' "$(git --version | awk '{print $3}')"

Cada um dos seis blocos destrói a mesma string, work worth keeping, de um jeito diferente, e depois tenta ler ela de volta. O script imprime RECOVERED quando o conteúdo recuperado bate exatamente com o original, e LOST quando não bate, então todo veredito é uma comparação e não uma opinião. As duas linhas que validam o diretório temporário antes de qualquer coisa não são cerimônia, e a seção sobre git -C mais abaixo explica contra o que elas defendem.

O que os seis cenários mostram sobre recuperar trabalho de agente de IA

Esta é a saída transcrita daquele script, no git 2.50.1 no macOS. A gente rodou sob bash, zsh e sh, e a saída saiu idêntica byte a byte nos três, então o resultado abaixo não é artefato de um shell:

committed, killed by reset --hard            RECOVERED
edited, never staged, git checkout --        LOST
edited, git add once, reset --hard           RECOVERED
created, never added, git clean -fd          LOST
committed on a branch, branch -D             RECOVERED
stashed, then git stash drop                 RECOVERED

git 2.50.1

Leia a lista pelo que os seis cenários têm em comum, e não um a um. Toda linha RECOVERED é um caso em que o conteúdo chegou ao banco de objetos do git: o commit escreve lá, o git add escreve lá, e o git stash escreve lá porque um stash é um commit com outro nome. Toda linha LOST é um caso em que o conteúdo só existiu no diretório de trabalho. O comando que destruiu não é o que decide o resultado, e essa é a parte que quase todo mundo entende ao contrário: reset --hard soa muito mais violento que checkout --, e ainda assim é o sobrevivível.

Por que um único git add salva um trabalho que você nunca commitou?

Porque git add não é anotação, é escrita. Quando você põe um arquivo no índice, o git calcula o hash daquele conteúdo exato e grava um objeto blob dentro de .git/objects na hora. O índice então aponta para o blob. Um git reset --hard depois disso devolve o índice e a árvore de trabalho ao estado anterior, mas ele não sai atrás do blob que acabou de deixar órfão. O objeto continua no disco sem ninguém apontando para ele, que é exatamente o que o git fsck --lost-found reporta como dangling.

É por isso que o terceiro cenário recupera. Um agente de IA colocou o arquivo no índice, alguém rodou git reset --hard, e o conteúdo continua ali, sob um hash que ninguém lembra. Você traz de volta com dois comandos, sem precisar saber o hash de antemão:

git fsck --lost-found          # lista blobs e commits soltos
git cat-file -p <hash>         # imprime o conteúdo de um deles

A consequência prática para quem trabalha com agentes de IA merece ser dita com todas as letras, porque é barata e não é óbvia: colocar no índice é backup. Se um agente está prestes a fazer alguma coisa larga e você tem trabalho não commitado que importa, o git add -A não custa nada, não exige mensagem de commit, e move o seu trabalho da categoria irrecuperável para a categoria recuperável.

O que você nunca mais recupera depois que um agente de IA apaga?

Duas coisas, e as duas têm a mesma causa. A primeira é uma alteração em arquivo rastreado que nunca foi para o índice e foi descartada por git checkout -- arquivo, ou pela grafia moderna git restore arquivo. A segunda é um arquivo que o agente criou e nunca adicionou, removido por git clean -fd. Nos dois casos o git não tinha cópia nenhuma do conteúdo, porque nada nunca pediu para ele fazer uma.

Não existe recuperação pelo lado do repositório para nenhum dos dois, e vale ser exato sobre o alcance dessa frase: ela quer dizer que o git não tem nada para te devolver, não que os bytes necessariamente sumiram da sua máquina. O que quer que ainda guarde uma cópia vive inteiramente fora do git, num backup, num snapshot do sistema de arquivos ou num editor que mantenha histórico local próprio, e nada disso é o que a gente mediu aqui. Se alguma dessas coisas existe na sua máquina é pergunta que este artigo não responde por você.

Como fazer a recuperação na prática?

Cada cenário recuperável tem um comando que faz o serviço, e vale ter esses comandos na frente dos olhos antes de precisar deles, não depois. Um commit destruído por reset --hard volta com git reset --hard 'HEAD@{1}', que é a entrada do reflog de onde a branch apontava um movimento atrás. Uma branch apagada com branch -D volta achando o último commit dela no git reflog e rodando git branch <nome> <sha>. Um stash descartado volta pelo git fsck --lost-found, que o reporta como dangling commit, que você inspeciona com git show e reaplica com git stash apply <sha>.

A primeira coisa a fazer, antes de qualquer uma dessas, é parar de escrever no repositório. Toda recuperação acima depende de objetos que estão sem referência e ainda não foram coletados, e o comando que coleta é o git gc, que o próprio git também dispara sozinho: a página de manual diz que as operações comuns de porcelana conferem se o repositório cresceu de forma substancial desde a última manutenção e, se cresceu, rodam git gc automaticamente. Um agente de IA que continua trabalhando naquele diretório está rodando exatamente os comandos capazes de fechar a janela pela qual você está tentando passar.

Por que o git -C com caminho vazio apaga arquivo no repositório errado?

Porque o git -C "" não falha. Ele cai no diretório em que você está parado, e a gente mediu quanto isso custa. Num repositório descartável com dois commits, um arquivo rastreado e um não rastreado, o git -C "" log --oneline imprimiu o histórico daquele repositório em vez de dar erro. Depois o git -C "" clean -qfd apagou o arquivo não rastreado, e o git -C "" reset -q --hard HEAD~1 jogou fora o commit mais novo e reverteu o arquivo rastreado ao conteúdo anterior. Dois commits viraram um, e nada disso aconteceu onde o caminho vazio apontava, porque caminho vazio não aponta para lugar nenhum e o git resolveu como aqui.

É por isso que o script lá em cima valida o diretório temporário antes de fazer qualquer coisa, e a falha contra a qual ele se defende está mais perto do que parece: qualquer variável de shell que termine vazia transforma todo git -C "$dir" em git -C "". Dois hábitos que parecem proteção não protegem aqui. O set -u recusa variável não definida, não variável vazia, então ele fica calado. E o exit dentro de uma função cuja saída você captura com $(...) encerra só o subshell, então o script segue em frente com a variável vazia. É o mesmo acidente de que o resto deste artigo trata, chegando por um script em vez de por um agente.

Por que o primeiro commit inalcançável é o errado de confiar?

Este aqui custou um resultado errado antes de virar achado. A nossa primeira versão do cenário do stash reportou LOST, e o problema não era o dado: era a chamada. O git stash cria dois commits, não um. Um guarda a sua árvore de trabalho ("WIP on main") e o outro guarda o índice daquele instante ("index on main"). O nosso script pegava o primeiro commit que o git fsck --unreachable imprimia, que calhou de ser o do índice, cujo conteúdo é a versão velha. Ele comparou a versão velha com o conteúdo esperado, achou diferente, e reportou honestamente uma perda que não tinha acontecido.

Duas coisas saíram do conserto. A ordem em que o git fsck --unreachable imprime os objetos não é coisa em que se construa nada: entre as nossas execuções, o mesmo repositório listou os dois commits em ordens diferentes. E o --lost-found é o instrumento melhor para esta tarefa específica, porque ele reporta só o commit do stash como dangling: o commit do índice é pai do commit do stash, então é alcançável a partir dele e corretamente não aparece. A lição generaliza para além do git, e é a que a gente reaprende sempre: quando uma medição devolve um zero surpreendente, desconfie da sua própria chamada antes de desconfiar do mundo.

Quanto tempo a janela de recuperação fica aberta?

Tempo suficiente para o pânico ser o risco maior, e não para sempre. Os padrões estão documentados no git help gc, que nesta máquina é o git 2.50.1 (Apple Git-155): o gc.reflogExpire remove entradas de reflog mais velhas que 90 dias; o gc.reflogExpireUnreachable remove, depois de 30 dias, as entradas que não são alcançáveis a partir da ponta atual, que é justamente o caso dos commits orfanados por um reset; e o gc.pruneExpire faz o git gc chamar prune --expire 2.weeks.ago, que é o que de fato apaga os blobs e commits soltos.

Então a versão honesta é que um commit que o agente destruiu hoje de manhã fica recuperável por semanas, e a janela de duas semanas do prune é a mais apertada das três. Nenhum desses relógios recomeça porque você percebeu. Se você estiver recuperando trabalho que um agente de IA destruiu no mês passado, confira os números acima contra a sua própria configuração com git config --get gc.pruneExpire, porque repositório que define isso explicitamente segue a regra dele, e plataforma hospedada pode rodar coleta de lixo no calendário dela.

O agente de IA que quebrou deve ser o que conserta?

A nossa resposta é não para a recuperação em si, e o motivo é mecânico, não é questão de confiança. Recuperar é uma leitura curta contra o banco de objetos: achar o objeto, imprimir, devolver ao lugar. O agente que causou o estrago carrega o contexto que produziu o estrago, e o modo de falha é específico e ruim. Mandado consertar um repositório, um agente puxa os mesmos comandos largos que destroem a evidência que sobrou, e git clean e git checkout -- são exatamente as duas operações que a nossa medição mostra serem irrecuperáveis.

A sequência que custa menos é parar o agente, rodar git fsck --lost-found você mesmo, e só então decidir o que restaurar. Delegar o conserto depois disso é razoável, uma vez que os objetos de que você precisa já estão identificados e a salvo. O que não sobrevive ao contato com a realidade é pedir para a coisa que acabou de apagar o seu trabalho ir descobrir como trazê-lo de volta, no mesmo diretório, com o relógio de duas semanas correndo.

Onde esta medição para

Seis cenários em repositórios descartáveis não são um levantamento de como o trabalho se perde. O script roda sobre um repositório com um arquivo e um commit, sem remoto, sem submódulo, sem hook e sem LFS, e cada um desses muda o quadro: uma branch que já foi enviada é recuperável do remoto independentemente de tudo que está acima, que é a rede de segurança mais barata de todas e a razão pela qual este artigo serve menos para trabalho já publicado. Rodamos no git 2.50.1 no macOS e confirmamos que a saída é idêntica em três execuções seguidas e em três shells; não rodamos em versões mais antigas do git, e os padrões de expiração que citamos vêm da documentação instalada com aquela versão, não de uma medição de expiração.

O que a gente não mediu de jeito nenhum é com que frequência cada uma dessas seis coisas de fato acontece quando há agentes de IA envolvidos. Dá para dizer o que é recuperável. Não dá para dizer, a partir disto, qual erro o seu agente tem mais chance de cometer.