관련 프로젝트WhoAmI — 공부 기록을 발행 가능한 형태로 좁힌 파이프라인 →
AI가 승인 없이 지웠다고 보고했고, 나는 Git 삭제 이력을 조사했다
코딩 에이전트(Claude Code, Opus 5)가 내가 승인하지 않은 삭제를 실행하고 그 사실을 보고했다. 프로젝트 안에서는 되돌릴 근거를 찾지 못했고, Git이 추적한 삭제 이력과 현재 로컬 참조를 조사해 다른 커밋의 파일 삭제 111개를 확인했다. 사후 보고에만 기대지 않기 위해 만든 장치와 그 한계를 정리했다.
목차 5
2026-08-09 업데이트 — 파일 111개 삭제에 대한 기록과 복구 상태를 바로잡고,
Bash(rm *)차단 규칙이 모든 삭제를 막거나 휴지통 사용을 강제하지는 않는다는 한계를 추가했다.
승인한 적 없는 삭제가 실행됐다
2026년 8월 3일부터 5일까지 이 블로그 작업을 코딩 에이전트(Claude Code, Opus 5)에게 상당 부분 맡겼다. 아래 세 건은 그 사흘 동안 실제로 난 일이고, 화면은 그때 찍은 것이다.
첫 번째는 업로드 테스트를 마치고 정리를 시키던 중에 올라왔다.

.trash 디렉터리를 통째로 지웠습니다」
명령은 rm -rf public/images/zz-tmp-fixture .trash 였다. 테스트 파일 하나를 치우려던 것인데 인자가 하나 더 붙었고,
.trash는 관리자에서 글이나 이미지를 지울 때 원본을 옮겨 두는 되돌리기 버퍼였다.
내가 이 명령을 승인한 적은 없다. 당시 로컬의 .claude/settings.local.json에는 허용 목록이 100줄 넘게 들어 있었는데, 그중 rm으로 시작하는 항목은 하나도 없었다.
목록에 없는 명령이 그대로 돌아갔다는 뜻이고, 허용 목록에 안 적었다는 사실이 그 명령이 안 돌아간다는 보장은 아니었다.
이 로컬 설정 파일은 현재 작업 트리에 남아 있지 않아 줄 수와 당시 규칙은 Git으로 재현할 수 없다. 이 부분은 사고 직후 확인 기록의 범위다.
이 조합에서는 프로젝트 안에서 바로 되돌릴 경로를 찾지 못했다.
.trash는.gitignore대상이라 git에 아무 기록이 없다rm은 Windows 휴지통을 거치지 않는다- 그 안에 무엇이 들어 있었는지 확인할 목록조차 같이 사라진다
사고 직후 확인 기록과 기억으로는 그 안에 관리자에서 지웠다가 되살릴 생각으로 남겨 둔 글과 이미지가 있었다. 다만 삭제 전 목록이나 외부 사본이 남아 있지 않아 그 구성을 지금 다시 입증하거나 복원하지는 못했다.
외부 백업, 볼륨 스냅숏이나 파일시스템 복구 가능성까지 조사한 것은 아니다. 여기서 확인한 범위는 Git, Windows 휴지통과 .trash 목록을 이용한 복구 경로가 없었다는 것까지다.
보고를 받고 나서 무엇을 했나
이 사고에서 유일하게 제대로 작동한 것은 삭제를 막는 장치가 아니라 삭제했다고 말한 보고였다.
조용히 넘어갔다면 나는 지금도 .trash가 비어 있는 이유를 모른다. 관리자에서 되살리려다 그때 알았다면 원인을 좁히기가 훨씬 어려웠을 것이다.
동시에 그 보고는 복구가 아니었다. 화면에 글자가 뜬 시점에 이미 파일은 없었다. 「지웠습니다」는 승인 요청이 아니라 사후 통보이고, 사후 통보로는 되돌릴 수 없다는 것이 이 건의 결론이다.
그래서 이 프로젝트에서 Git이 추적한 삭제 이력과 현재 로컬에서 보이는 참조를 조사했다. 보고된 한 건만 확인하고 넘어가면, 보고되지 않은 나머지는 그대로 남는다. 이 범위에는 미추적 파일과 이미 사라진 참조가 들어가지 않는다.
다음 날 아침 조사에서 커밋 하나가 파일 111개를 지운 것을 확인했다. 다만 원문에 쓴 「커밋 제목과 본문 어디에도 그 사실이 없었다」는 말은 틀렸다. 실제 커밋 00e3e23의 제목은 style(dashboard): AI 사용량 대시보드 컴포넌트 UI 개선 및 불필요 파일 정리였다. 정리를 언급했지만 111개라는 규모와 삭제 경로는 드러내지 않았다.

삭제 목록의 대부분은 디자인 스킬 파일이었다. 당시에는 「재설치로 돌아왔다」고 적었지만, 2026년 8월 9일 Git 이력을 다시 보니 이 파일들을 한꺼번에 다시 추가한 후속 커밋은 없고 현재 작업 트리에도 남아 있지 않다. 따라서 Git으로 확인되는 사실은 복구 완료가 아니라 삭제 목록과 삭제 직전 내용이 이력에 남아 있어 필요하면 꺼낼 수 있었다는 것이다. 현재 배포는 별도로 wrangler.jsonc가 담당하며, 삭제된 옛 호스팅 메타데이터가 복원됐다는 뜻도 아니다.
두 건의 차이는 복구 완료 여부가 아니라 추적 가능성이다. 111개는 Git 추적 파일이라 삭제 목록과 이전 버전을 확인할 수 있다. .trash는 Git이 애초에 보고 있지 않았으므로 Git으로는 안에 있던 파일의 목록도 만들 수 없다.
Git이 추적하지 않는 경로에 되돌리기 버퍼를 두고 별도 백업도 없으면, Git 기준으로는 그 버퍼를 복구할 근거가 없다.
「2번 테마 토글까지만」은 몇 번을 뜻하나
다음 작업 후보를 세 개 받았다.

/tags 인덱스, 2번 테마 토글, 3번 스크린샷 캡션
여기서 「2번 테마 토글 까지만 진행하자」라고 답했다. 뜻한 것은 2번 하나였다.

「까지만」을 1번부터 2번까지로 읽은 것이다. 지적하자 바로 정정했다.

이 건은 앞의 삭제와 달리 AI만 탓하기 어렵다. 「2번까지만」은 한국어에서 실제로 두 가지로 읽히고, 내가 그렇게 썼다. 다만 가이드 문서에 모호하면 실행하기 전에 되묻는다는 규칙을 미리 걸어 두었는데도 되묻지 않고 진행했다. 규칙을 문서에 적어 두는 것과 그 규칙이 실행 시점에 지켜지는 것은 별개다.
이 건이 앞의 삭제와 갈린 지점은 정확히 하나다. 「둘 다 진행하겠습니다」가 작업을 시작하기 전에 화면에 떴다.
같은 오해가 조용히 지나갔다면 /tags 인덱스가 만들어진 뒤에야 알았을 것이고, 그때는 되돌리는 일이 남는다.
작업 지시에서 범위를 번호로 부르지 않게 됐다. 「테마 토글만」처럼 항목 이름을 그대로 쓰면 「까지」가 낄 자리가 없다.
메모리에 적힌 「미푸시」는 지금도 사실인가
가장 싼 요금제로 Claude Code와 ChatGPT를 쓰면 토큰에 계속 쫓긴다. 그래서 세션에서 결정한 것을 파일로 남겨 두고 다음 세션이 그것부터 읽게 했다. 새 세션이 코드를 처음부터 훑는 양을 줄이려는 것이다.
그 파일에 이렇게 적혀 있었다.

실제로는 이미 병합해서 올린 뒤였다.

main에 들어가 있고 원격과도 같다
토큰을 아끼려고 둔 장치가 확인 작업을 하나 더 만든 셈이다. 손해가 큰 사고는 아니지만 방향이 나쁘다. 메모리를 믿고 넘어갔다면 이미 올라간 브랜치를 다시 올리려 했을 것이다.
원인은 적은 내용의 종류에 있었다. 푸시 여부, 브랜치 존재, 커밋 해시는 git에 물어보면 1초 만에 나오는 값이고, 물어보지 않는 동안 조용히 바뀐다. 반면 「왜 이 방식을 골랐나」는 어디에도 안 남아 있고 시간이 지나도 잘 안 변한다. 뒤쪽만 메모리에 남기고 앞쪽은 그때그때 확인하는 편이 맞다.
다음부터 지키기로 한 것
세 건 모두 「AI가 틀렸다」로 끝나지 않는다. 갈린 것은 그 사실이 실행 전에 화면에 떴는가, 실행 뒤에 떴는가였다.
| 사고 | 알게 된 시점 | 결과 |
|---|---|---|
.trash 삭제 |
실행 뒤 보고 | 되돌리지 못했다 |
| 파일 111개 삭제 | 그 보고로 시작한 Git 삭제 이력 조사 | Git 이력에서 삭제 목록과 이전 버전을 확인했다 |
| 과추론 | 실행 전 화면 | 시작하기 전에 정정했다 |
| 낡은 메모리 | 실행 전 정정 보고 | 잘못된 재푸시를 막았다 |
넷 중 셋이 화면에 뜬 말 덕분에 정리됐다. 그런데 그 말이 실행 뒤에 오면 남는 것은 원인을 아는 것뿐이고, 파일은 안 돌아온다. 그래서 사후 보고에 기대는 자리를 하나씩 줄였다.
커밋을 잘게 자른다. AI와 작업할 때는 평소보다 커밋 수를 늘린다. 되돌릴 수 있는 지점이 촘촘해진다.
내가 삭제할 때는 휴지통 경로를 명시한다. Windows에서는 PowerShell로 휴지통에 넣을 수 있다.
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(
"C:\path\to\dir", 'OnlyErrorDialogs', 'SendToRecycleBin')
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(
"C:\path\to\file", 'OnlyErrorDialogs', 'SendToRecycleBin')규칙을 문서에 적어 두는 것으로는 부족했다. 허용 목록에 rm을 안 적어 둔 것도 마찬가지였다 — 안 적었다고 안 돌아가지는 않았다.
그래서 이번에는 자주 쓰이는 직접 삭제 명령을 거부 목록에 넣었다. Claude Code는 .claude/settings.json의 permissions.deny와 일치하는 호출을 거부한다.
{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(rm:*)",
"PowerShell(Remove-Item *)",
"PowerShell(Remove-Item:*)"
]
}
}넣은 뒤 rm --version을 시켜 직접 rm으로 시작하는 호출이 막히는 것은 확인했다. Permission to use Bash with command rm --version has been denied.가 나오고 명령은 실행되지 않았다.
하지만 이 규칙은 삭제를 휴지통 경로로 자동 전환하지 않는다. Claude Code 공식 문서도 Bash(rm *)가 리터럴 명령 문자열만 맞추므로 /bin/rm이나 find -delete를 막지 못한다고 설명한다. 다른 실행 파일이나 스크립트 언어를 통한 삭제도 별도 경계가 필요하다. 모든 삭제에 강제 보장이 필요하면 변형별 규칙을 계속 늘리는 대신 PreToolUse 훅이나 OS 수준 샌드박스를 함께 써야 한다. 현재 프로젝트 설정의 deny 목록은 직접 명령 몇 가지를 줄이는 방어선이지, 모든 삭제가 휴지통을 거친다는 보장은 아니다.
범위는 번호가 아니라 이름으로 지시한다. 「2번까지만」이 아니라 「테마 토글만」이라고 쓴다.
메모리에는 판단 근거만 남기고 상태는 적지 않는다. 푸시 여부·브랜치·해시는 명령으로 확인한다.
지웠다는 보고를 받으면 무엇부터 보나
보고된 한 건만 확인하고 넘어가면 보고되지 않은 것은 그대로 남는다. 저장소 전체에서 무엇이 없어졌는지 훑는 명령들이다.
# 현재 로컬에 보이는 모든 ref의 이력에서 삭제된 파일을 뽑는다
git log --all --diff-filter=D --name-only
# 특정 경로가 언제 지워졌는지
git log --all --diff-filter=D --name-only -- public/images/
# 지워진 파일을 그 직전 커밋에서 되살린다
git checkout <삭제한_커밋>^ -- <경로>
# 커밋 자체가 사라진 것 같으면 이동 기록부터 본다
git reflog추적되지 않던 파일에는 이 중 어느 것도 듣지 않는다. .gitignore에 든 경로에 지워지면 곤란한 것이 있는지 먼저 확인하는 편이 빠르다.
git status --ignored --short직접 확인한 것은 여기까지다. 이번 Git 삭제 이력 조사에서 111개는 커밋 이력으로 목록과 이전 내용을 확인할 수 있었지만, 일괄 복원한 후속 커밋은 없었다. .trash에 들어 있던 것은 Git으로 목록조차 만들지 못한 채로 끝났다.
deny 규칙이 직접 rm 호출을 막는 것은 확인했지만, 그 방식으로 막을 수 있는 것은 리터럴 패턴에 맞는 명령뿐이다. 자동으로 휴지통을 쓰게 하거나 모든 삭제 경로를 닫지는 않는다.
「모호하면 되묻는다」처럼 판단이 들어가는 규칙을 실행 시점에 지키게 하는 방법은 아직 못 찾았고, 그래서 지금도 사후 보고를 읽는 일이 남아 있다.