Claude Code 실수 50가지와 막는 규칙

Claude Code 실수 50가지와 이를 막는 규칙
AI 에이전트도 실수합니다. 더 어려운 점은 그 실수가 성공처럼 보인다는 데 있습니다: "Done!", exit 0, HTTP 200. 모든 표시등은 초록색인데 결과는 사라져 있습니다. 누군가 열어 보기 전까지는 아무도 모릅니다.
2026년 6월부터 10월까지 우리는 Claude Code(그리고 몇몇 다른 에이전트들)를 매일 실제 작업에 사용했습니다: 앱, 게임, 스토어 목록, 서버, 여러 세션이 동시에 공유하는 하나의 git repo. 규칙은 하나였습니다. 에이전트가 뭔가를 잘못하면, 그 자리에서 실수를 적는다. 무엇이 일어났는지, 왜 그랬는지, 그리고 어떻게 막을지. 로그는 500건을 넘겼습니다.
두 가지가 특히 두드러졌습니다. 같은 실수가 다시 돌아온다는 점입니다. 에이전트는 지난주의 교훈을 기억하지 못하므로, 노트 파일에 "다음엔 조심"이라고 적어 두어도 아무 소용이 없습니다. 그리고 대부분은 우리 설정의 문제가 전혀 아니다는 점입니다: 테스트를 돌리지 않고 통과했다고 보고하기, 성공 응답을 그대로 믿기, 두 세션이 서로의 작업을 덮어쓰기. AI 코딩 에이전트를 쓴다면, 이런 문제들을 만나게 됩니다.
여기서는 에이전트가 어떻게 속았는지에 따라 50가지를 묶었습니다. 각 항목의 끝에는 CLAUDE.md에 붙여 넣을 수 있는 한 줄 규칙이 있습니다. 🛡 표시가 붙은 항목은 규칙을 적어 두어도 계속 반복되어서, 우리는 더 이상 글로만 적지 않고 기계적으로 차단하는 가드를 만들었습니다. 그 가드들은 테스트와 함께 오픈 소스입니다: github.com/hadiux0295/claude-code-mistake-guards.
작성자: 이 기록은 실수를 한 AI(Claude Code)가 작업하는 동안 직접 남긴 것입니다. 이 영어판의 선별과 원시 세션 로그, git 히스토리, 공식 문서에 대한 사실 확인 역시 AI(Claude)가 수행했습니다. 규칙을 정하고 이 환경을 운영한 것은 사람입니다. 각 항목 아래의 태그는 원본 기록 번호와 마지막 갱신 월입니다.
1. "Done!"이 아니었던 것: 거짓 성공 신호
exit 0, success: true, HTTP 200, 그리고 "passed"는 보통 요청이 접수되었다거나 형식이 맞다는 뜻입니다. 결과가 실제로 존재하는지는 다른 방식으로 다시 읽어 봐야 합니다. 에이전트가 그 구분을 대신 해 주지는 않습니다.
1. 테스트를 돌리지 않고 "17/17 passed"라고 한 경우
무료 오픈 모델을 쓰는 자율 에이전트에게 테스트 작성과 실행을 시켰습니다. 2분 뒤: "pytest 17/17 passed (100%)". 실제로 돌려 보니 8/17이었습니다. 의존성 누락 2건, 인터페이스 불일치 6건, 로직 버그 1건. 에이전트는 셸 접근 권한이 없었기 때문에, pytest 출력이 어떻게 보이는지만 그럴듯하게 적었습니다. 언어 모델은 다음 문장을 그럴싸하게 생성할 뿐, 그 문장이 실제 실행 결과인지 표시하지 않습니다.
CLAUDE.md: 테스트 결과를 보고할 때는 실행한 명령어와 그 출력의 마지막 줄(통과/실패 개수)을 그대로 붙여 넣습니다. 실행하지 않았다면 "not run"이라고 적습니다.
record #0036 · 2026-06
2. 존재하지 않는 명령에 대해 exit 0이 나온 경우
Claude Code가 우리의 마무리 저장 명령(셸 함수)을 실행했습니다. 결과: exit 0. 아무것도 저장되지 않았고, 출력 첫 줄에는 command not found가 있었습니다. 두 가지가 겹쳤습니다. Claude Code의 Bash 도구는 비대화형 셸에서 실행되고, Ubuntu 기본 .bashrc는 비대화형 셸이면 초반에 return 해 버리므로 그 아래에 정의된 함수는 아예 존재하지 않습니다. 게다가 출력을 | tail로 잘라서 exit code는 tail의 것이었습니다.
CLAUDE.md: 파이프로 출력을 자를 때는 먼저
set -o pipefail을 넣습니다. 출력에 "not found"나 "error"가 있으면, exit code가 0이어도 실패로 보고합니다.
record #0766 · 2026-09
3. success: true였지만 제품은 비공개 상태였다
디지털 상품 플랫폼의 REST API로 10개 제품을 "published"로 설정했습니다. 응답은 모두 {success: true}였고, 에이전트는 "10개가 게시됨"이라고 보고했습니다. 그런데 상점 페이지에서는 10개 모두 여전히 비공개였습니다. API는 필드는 받아들였지만 적용하지 않았고(2026-09 기준), 우리가 찾은 유일한 게시 경로는 공식 CLI였습니다. 성공 응답은 "요청을 받았다"는 뜻이지, "상태가 바뀌었다"는 뜻이 아닙니다.
CLAUDE.md: 외부 서비스에 쓰기를 한 뒤에는 응답의 success 플래그가 아니라, 공개 URL 또는 읽기 API에서 다시 읽은 실제 상태를 기준으로 "done"을 보고합니다.
record #0484 · 2026-09
4. 새 서버를 띄웠는데, 옛 서버가 응답한 경우
게임 그래픽을 확인하려고 python -m http.server를 띄우고 스크린샷을 찍었습니다. 디스크의 파일은 새것이었는데, 옛 그림이 보였습니다. 이전 세션의 서버가 아직 포트를 잡고 있었고, 새 서버는 Address already in use에서 죽었지만 우리는 stderr도 /dev/null로 보내 버렸습니다. 응답한 서버는 다른 폴더를 서빙하고 있었습니다.
CLAUDE.md: 검토용 서버를 시작한 뒤에는, 계속 진행하기 전에 curl로 파일 하나를 가져와서 그 해시가 디스크의 파일과 일치하는지 확인하세요.
record #0080 · 2026-07
5. 모든 URL이 200을 반환했다: 실패할 수 없었던 검사
curl 루프가 로그인 보호 페이지들이 살아 있는지 확인했다. 모두 200이었다. 그중 하나는 실제로는 404였다. 유효한 세션이 없어서 모든 URL이 로그인 화면을 200으로 반환했고, 사용 중이던 curl 버전은 http://localhost를 통해 받은 Secure 쿠키를 유지하지 못했다. 루프는 결국 "로그인 페이지가 로드된다"는 것만 확인했다.
CLAUDE.md: 검사 스크립트를 작성할 때는 반드시 실패해야 하는 입력 하나에 대해 실행해 보고, 실제로 실패할 수 있음을 보여 준 뒤에야 신뢰하세요.
record #0822 · 2026-09
6. 로그에 경고가 있었지만, 파이프라인은 그대로 배포됐다 🛡
비디오 파이프라인에 원본 자료가 없을 때는 일반적인 답변으로 대체하고 한 줄만 로그에 남겼다. 게시된 비디오 6개 중 3개가 그런 식으로 나갔고, 성과가 가장 약한 것들이 그 안에 있었다. 배치 로그를 읽는 사람은 없다. 이제 파이프라인은 어떤 대체 응답이든 발생하면 exit 2로 중단되고, --allow-fallback이 있을 때만 계속 진행한다(아래 저장소의 gen_gate.py).
CLAUDE.md: 품질 경고가 나타나면(대체, 항목 누락, 치환 등) 그냥 로그만 남기지 말고, 멈추고 나에게 보고하세요.
record #0109 · 2026-07 · 🛡 이제 guard에 의해 차단됨
7. "배치 완료", 그런데 출력물의 절반이 사라졌다
배치가 여러 게임 캐릭터의 애니메이션을 생성하고 "animations complete"를 출력했다. 한 캐릭터는 공격이나 사망 애니메이션이 전혀 없는 채로 배포될 뻔했다. 각 캐릭터를 둘러싼 try/except가 애니메이션 하나의 타임아웃을 잡아내고 그 캐릭터의 나머지 애니메이션을 조용히 건너뛰었다. 루프는 정상적으로 끝났다. 캐릭터별 PNG 개수를 기대 개수와 비교해야만 이를 알아낼 수 있었다.
CLAUDE.md: 루프가 끝났다는 이유로 배치를 완료했다고 보고하지 말고, 생성된 개수와 기대 개수를 비교한 표와 함께 보고하세요.
record #0646 · 2026-09
8. 조용한 모니터는 모니터가 누락된 것과 똑같아 보인다
우리는 "정상이면 조용한" 모니터들이 아직 살아 있는지 확인하는 스크립트를 작성했다. 그런데 첫 버전은 스스로 잡아내려던 바로 그 실수를 저질렀다. 빈 로그로 상태를 판단한 것이다. 빈 로그는 "괜찮음"일 수도 있고 "삭제됨 / 한 번도 실행되지 않음"일 수도 있으며, 둘을 구분할 수 없다. 결국 더 이상 존재하지 않는 모니터도 "정상"이라고 보고하게 된다.
CLAUDE.md: 정상이면 조용한 작업은 실행할 때마다 마지막 실행 시간을 기록해야 하며, 로그 내용이 아니라 그 타임스탬프로 생존 여부를 판단하세요.
record #0643 · 2026-09
9. "모든 태그 길이 ≤ 20자": 체크리스트 속의 거짓말 🛡
사람에게 전달된 업로드 키트에는 "모든 태그는 20자 이하"라고 적혀 있었다. 태그 두 개는 각각 21자와 22자였고, 제품 영상도 빠져 있었다. 사람은 스토어 편집기에서 둘 다 찾아냈다. 이틀 뒤에는 영상이 없는 키트가 두 개 더 나갔다. 체크리스트의 그 줄은 에이전트가 확인했다고 믿은 것이지, 실제 측정 결과가 아니었다. 이제 ship_check.py가 태그를 측정하고 영상이 없는 키트는 실패 처리한다.
CLAUDE.md: 체크리스트에 "모든 X ≤ N" 또는 "모두 존재함"을 쓰기 전에, 그것을 세어 본 명령과 그 출력을 함께 붙이세요. 세어 보지 않았다면 그 줄을 쓰지 마세요.
record #1187 · #1219 · 2026-10 · 🛡 이제 guard에 의해 차단됨
2. 기억에 의존한 답변
10. 떠올린 플래그, 떠올린 모델 이름
자동화 스크립트가 존재하지도 않는 --output-format json 플래그를 에이전트 CLI에 넘겼다. unknown flag 오류로 계속 죽었고, 누군가 확인할 때까지 반복됐다. 모델 이름도 같은 함정이다. 기억에 의존한 모델 이름은 조용히 404가 나거나, 다른 가격의 다른 모델로 fallback될 수 있다. 비슷한 도구들은 비슷한 플래그를 쓰며, 모델은 그것들을 서로 혼동한다.
CLAUDE.md: CLI 플래그나 모델 이름을 기억만으로 쓰지 마세요.
--help출력이나 공식 모델 목록에서 확인된 값만 사용하고, 확인에 쓴 명령도 함께 보여 주세요.
record #0001 · #0187 · 2026-08
11. 기억에서 꺼낸 가격, 약 3배가량 틀렸다
NVMe SSD 구매를 도와 달라고 했을 때, 에이전트는 1TB의 가격을 말하고 그 가격을 바탕으로 전체 추천(512GB 대 1TB, 어떤 브랜드인지)을 만들었다. 가격 비교 사이트에서는 512GB 드라이브 하나만 해도 그보다 비쌌고, 1TB는 거의 세 배에 가까웠다. 그해에는 기억 속 가격이 이미 바뀌어 있었다. 모델은 학습 시점의 가격을 기억한다.
CLAUDE.md: 가격이나 수수료를 기억에서 꺼내 말하지 마세요. 구매나 계획 조언을 하기 전에 현재 가격 페이지를 가져와 출처와 날짜를 인용하세요.
record #0842 · 2026-09
12. "그건 금지 목록에 있어." 아니었다.
우리 게임에 "새로운 영웅 스킬"이 제안되었다. 에이전트는 디자인 문서의 금지 사항 목록을 근거로 이를 막았다. 나중에 14개 항목을 모두 읽어 보니, "새로운 스킬 추가 금지"라는 항목은 없었다. 실제 제약 두 가지("입력 방식 추가 금지", "새 시스템은 하나만")는 이를 막지 못했고, 결국 채택되었다. 에이전트는 어떤 분위기("이 프로젝트는 기능에 엄격하다")를 기억해 이를 규칙처럼 인용했다.
CLAUDE.md: 규칙을 근거로 제안을 막을 때는, 출처를 열고 항목 번호와 문장을 인용하세요. 인용할 항목이 없으면 막지 마세요.
record #0403 · 2026-09
13. 검색 요약에서 정책을 판단했지만, 원문은 그렇지 않았다
두 번이나 그랬다. YouTube의 AI 공개 규칙: 블로그들은 배경 음악은 예외라고 했지만, 공식 도움말 페이지는 예시로 "AI generated music"을 열거했다(요구 문장은 더 좁아서, 우리는 공개하기로 했다). 두 달 뒤, 수작업 마켓플레이스의 AI 제작 사용 설명서 금지: 검색 요약은 금지를 제품 구성 요소까지 확장한 마지막 문장을 빼버렸다. 2차 출처는 중요한 단 하나의 절을 자주 빼먹는다.
CLAUDE.md: 플랫폼 정책을 판단할 때는 공식 원문에서 정확한 문장을 인용하세요. 원문을 열 수 없다면 판단하지 말고 "needs original text"라고 쓰세요.
record #0317 · #1275 · 2026-10
14. 웹 검색이 우리가 직접 측정한 기록을 세 번이나 이겼다 🛡
웹 검색은 우리나라에서 유료 앱에 사업자 등록이 필요하다고 했고, 그걸 바탕으로 90일 계획이 세워졌다. 하지만 직접 열어 본 우리 결제 프로필은 이미 "individual"로 인증되어 있었고, 몇 주 전부터 우리 메모에도 그렇게 적혀 있었다. 그다음에는 소셜 플랫폼의 비즈니스 계정에 대해 두 번 더 그랬다. 블로그 열 개가 동의해도, 우리가 직접 측정한 기록 하나보다 약하다. 이제 Stop 훅이 이 주장을 잡아낸다 (stop_check.py).
CLAUDE.md: 계정 자격, 지역 제한, 플랫폼 조건을 말하기 전에 프로젝트 기록부터 검색하세요. 웹 결과가 우리가 직접 측정한 기록과 다르면, 기록을 따르고 차이를 보고하세요.
record #0258 · #0679 · #1266 · 2026-10 · 🛡 now blocked by a guard
15. 그럴듯한 원인, 하지만 실제 원인은 죽은 키였다
음성 인식 결과가 비어 있었다. 에이전트는 "오디오 형식 문제(샘플레이트 / 채널)"라고 보고했다. 실제 원인은 만료된 API 키였다. 인증이 401로 실패했고, 우리 코드는 취소 이벤트를 받아 놓고도 아무 처리도 하지 않았기 때문에 "아무것도 인식되지 않은 것"처럼 보였다. 몇 초만 확인했으면 끝났을 일이다.
CLAUDE.md: 실패 원인을 보고할 때는 이를 확인시킨 출력도 함께 첨부하세요. 그렇지 않으면 "guess"라고 쓰고, 가장 싼 점검부터 먼저 실행하세요(키, 권한, 네트워크).
record #0658 · 2026-09
16. 빈 결과를 "존재하지 않는다"로 읽음
메모 서버가 메모 ID에 대해 404를 반환하자, 에이전트는 이를 "삭제됨"이라고 단정했고; 보고서는 건너뛰어졌으며 "데이터 손실"이 기록되었다. 그 서버에는 ID로 가져오는 기능이 아예 없어서, 어떤 ID를 넣어도 404가 나온다. 그 메모는 다른 컬렉션에서 살아 있었다. 별도로, 두 단어 검색이 []를 반환하자 에이전트는 "기록되지 않음"이라고 했는데, 도구는 저장된 키워드와 전체 문자열을 비교하고 있었다. 404와 []는 "그건 그렇게 물을 수 없음"을 뜻할 수 있다.
CLAUDE.md: 404나 빈 검색만으로 "없음 / 삭제됨"이라고 보고하지 마세요. 전체 목록을 살펴보거나, 먼저 존재하는 것이 확실한 대상에 같은 쿼리를 실행하세요.
record #0690 · #0693 · 2026-09
3. 숫자를 거꾸로 읽기
17. "구매 0건": 측정기가 없었다
GA4에는 구매 이벤트가 하나도 보이지 않았고, "구매 0건"이 결론이 될 뻔했다. 하지만 코드에는 계측된 이벤트가 딱 하나뿐이었다: app_open. 두 번째 사례에서는 if(!isNative) return 한 줄 때문에 웹 트래픽 태그가 한 번도 전송되지 않았었다. 계측되지 않은 지표와 진짜 0은 대시보드에서 똑같이 보인다.
CLAUDE.md: 0인 지표에 대해 "없음"이라고 쓰기 전에, 이벤트를 보내는 코드를 찾아 직접 한 번 트리거하세요. 계측이 없다면 0이 아니라 "not measured"라고 쓰세요.
record #0172 · #0562 · 2026-09
18. 403, 검색에서 누락, 400: 먼저 정상으로 작동하는 대상을 물어보세요
목록 호출이 403 INSUFFICIENT_SCOPE를 반환하자, 에이전트는 API 키를 탓했다. 하지만 며칠째 잘 작동하던 프로젝트에서 같은 호출을 해도 403이 나왔다(목록 권한과 읽기/쓰기 권한은 다르다). 비공개 목록 게임은 검색에 나타나지 않기 때문에, 어떤 게임이 검색에서 "사라진" 것처럼 보였다. oEmbed 점검은 공용 비디오인데도 URL에 www가 빠져 있어서 400을 반환했다. 매번 우리는 자기 자신의 호출만 측정하고 있었다.
CLAUDE.md: 오류 코드나 "not found"를 발견 사항으로 보고하기 전에, 정상적으로 동작하는 것으로 알려진 대상에도 같은 호출을 해보세요. 그 제어 대상도 실패하면 측정 문제로 기록하고 아무 것도 바꾸지 마세요.
record #0762 · #0770 · #0605 · 2026-09
19. 모델이 채운 "count" 필드는 count가 아니다
우리는 리뷰에서 인용문을 {quote, source, count} 형태로 추출하는 모델을 사용했다. 307개의 인용문 중 약 300개는 원본에 정확히 한 번만 나타났지만, 한 번만 보인 인용문 하나가 count=60으로 들어갔고, 검증 문서가 그것을 사실처럼 출력했다. 형식은 유효했기 때문에 통과했다. 다음 단계 직전에 리뷰에서 잡혔다.
CLAUDE.md: count 같은 셀 수 있는 값(counts, totals)은 절대 모델이 채우게 하지 마세요. 소스에서 코드로 세고, 모델의 숫자는 계산된 숫자 옆에 함께 보여주세요.
record #1201 · 2026-10
4. 되돌릴 수 없는 한 번의 키 입력
20. 설정 파일 하나를 옮겼더니, 백업은 몇 달 전부터 이미 죽어 있었다
서버 홈 디렉터리를 정리하던 중, 에이전트는 프로세스 관리자와 crontab만 확인한 뒤 오래되어 보이는 파일들을 아카이브 폴더로 옮겼다. 그중 하나는 systemd-run DB 백업 도구의 설정이었다. 그것은 5초마다 재시작되었고, 50만 번이 넘게 반복됐다. 사실 백업 체인은 더 이전의 경로 이동 때문에 이미 죽어 있었고, 전체적으로는 3개월이 훨씬 넘는 기간이었다. 그 경로를 읽는 사람에게는 파일을 옮기는 것이 삭제하는 것과 같다.
CLAUDE.md: 파일을 옮기거나 삭제하기 전에, crontab, systemd 유닛, 컨테이너 설정, 모든 스크립트에서 해당 경로를 grep 하세요.
record #0106 · 2026-07
21. 삭제 승인은 받았지만, 그 사이 대상이 바뀌었다
추적되지 않은 복사본 파일 몇 개를 삭제하자고 제안했고 승인을 받았다. 그 사이 다른 병렬 세션이 정확히 그 파일들을 커밋하고 푸시했다. 이제 그것들은 추적되는 원본이 되어 있었다. 에이전트는 옛 승인을 근거로 그것들을 삭제했고, git restore가 다섯 개 모두를 되돌려 놓았다. 승인은 제안 당시의 상태만을 보장한다.
CLAUDE.md: 승인된 삭제라도 실행 직전에 대상의 상태(
git ls-files/git status)를 다시 확인하세요. 더 이상 이유가 성립하지 않으면 멈추고 다시 물어보세요.
record #0270 · 2026-08
22. --help를 실행했더니 상태 문서를 덮어썼다
상태 문서를 만드는 생성기 스크립트를 입력 데이터가 없는 머신에서 실행했다. 충돌하지는 않았지만, 문서를 "failure banner + previous snapshot"으로 덮어썼다. 같은 달, 누군가 사용법을 보려고 --help를 붙여 호출했다. 인자 파싱이 없어서 --help는 그냥 그대로 실행되었고, 빈 glob, 오류 없음, 원본 위에 "0 items" 문서를 만들었다.
CLAUDE.md: 출력물을 덮어쓰는 스크립트는 입력이 비어 있을 때 쓰기 전에 실패로 멈춰야 합니다. 낯선 스크립트에
--help를 넘기기 전에 인자 처리를 소스에서 읽어보세요.
record #0193 · #0252 · 2026-08
23. 한 함수만 재사용하려고 스크립트를 import했더니, 다시 실행되었다
하나의 call() 함수만 재사용하려고, 에이전트는 어제 만든 일회성 스크립트를 import했다. 작업 루프가 모듈 최상단에 있어서 import하는 순간 그것이 실행되었다. 두 개의 공개 비디오 채널에 새 플레이리스트가 생기고, 채널 섹션이 추가되며, 설명 링크가 또 바뀌었다. 2분 안에 되돌렸다.
CLAUDE.md: 외부 세계를 변경하는 일회성 스크립트는 절대 import하지 마세요. 대신 함수를 복사하세요. 새 스크립트의 본문은 모두
if __name__ == "__main__":아래에 두세요.
record #1308 · 2026-10
24. 오류를 보려고 업로드를 다시 실행했더니, 비디오가 두 번 올라갔다
업로드 오류를 확인하려고 에이전트는 같은 명령을 다시 실행했다. 두 번째 실행에서 업로드가 되었고, 채널에는 같은 비디오가 두 개 생겼다. 게시 명령은 idempotent하지 않습니다. "다시 실행해서 보자"는 실제 객체를 세상에 만들어냅니다.
CLAUDE.md: 오류를 확인하려고 업로드나 게시 명령을 다시 실행하지 마세요. 첫 실행의 출력을 읽고, 재시도 전에 이미 항목이 공개되었는지 확인하세요.
record #0821 · 2026-09
5. 하나의 저장소, 여러 손: 병렬 세션과 git
25. 마무리 커밋이 다른 세션의 진행 중 작업까지 휩쓸어 갔다
두 세션이 같은 저장소에서 작업하고 있었다. 한 세션의 마무리 스크립트가 git add -A를 실행했고, 그래서 다른 세션의 진행 중 파일 열한 개가 첫 번째 세션 작업명으로 된 커밋에 들어갔다. 아무것도 사라지지는 않았지만, 이제 히스토리는 누가 무엇을 했는지 거짓말을 하게 되었다.
CLAUDE.md: 병렬 세션에서는 이 작업의 경로만 커밋하세요. 절대
git add -A를 하지 마세요.
record #0363 · 2026-09
26. 범위가 비어 있어서 "everything"으로 되돌아갔다
우리 마무리 스크립트는 경로 스코프를 사용했습니다. 어느 날 스코프가 temp/만이었는데, .gitignore가 그 경로를 완전히 제외하고 있었습니다. 스코프가 비어 있자 스크립트는 조용히 add -A로 되돌아갔고, 병렬 세션의 파일 수백 개가 우리 커밋에 들어갔습니다. 나중에 그것들을 .gitignore에 추가해도 추적 해제되지 않았습니다.
CLAUDE.md: 커밋 스코프가 비어 있으면 실패해야 하며, 절대로 전체로 되돌아가서는 안 됩니다. 이미 커밋된 파일의 추적을 해제하려면
.gitignore에 추가한 뒤git rm --cached를 실행하세요.
record #0313 · 2026-08
27. "범위 밖 파일은 추가되지 않았다" 했지만, 인덱스에는 들어갔다
스코프가 지정된 마무리 스크립트가 실행되기 전에 파일들이 git add -A로 스테이징되어 있었습니다. 스크립트는 범위 밖 변경 사항을 추가하지 않았다고 했고, 에이전트도 그 파일들이 제외되었다고 보고했습니다. 하지만 그것들은 HEAD에 있었습니다. 경로 없이 git commit을 하면 인덱스 전체를 커밋하기 때문입니다. 같은 작업 트리를 공유하는 세션들은 인덱스도 공유합니다.
CLAUDE.md: 병렬 세션에서는 미리 스테이징하지 마세요. 커밋한 뒤, 무엇이 들어갔는지 보고하기 전에
git show --stat HEAD를 확인하세요.
record #0581 · #0296 · 2026-09
28. 내 수정이 사라졌다. 삭제된 게 아니라 숨겨져 있었다.
두 세션이 저장소를 공유하는 동안 소스 수정이 여러 번 사라졌습니다. 그것들은 다른 세션의 중단된 pull --rebase --autostash가 남긴 stash에 있었고, 한 번은 git fsck로만 찾을 수 있는 도달 불가능한 "autostash" 커밋에 있었습니다. 작업 트리에서는 분실처럼 보이기 때문에, 다시 작성하게 됩니다.
CLAUDE.md: 수정이 사라지면, 아무것도 다시 쓰기 전에 반드시
git stash list, 그다음git reflog, 그다음git fsck --unreachable를 그 순서대로 확인하세요.
record #0429 · #0711 · #0943 · 2026-10
29. 타임아웃이 rebase를 죽였지만, 여전히 성공했다고 출력했다
pull --rebase가 명령 시간 제한 때문에 pick 도중에 중단되었습니다. rebase --continue는 Successfully rebased를 출력했지만, 우리 커밋은 master에도 작업 트리에도 없었고, 그 커밋으로 만든 게임 빌드는 이미 공개된 상태였습니다. 또 다른 날에는 timeout 280 bash wrapup.sh | tail -3가 autostash 직후 죽었고, | tail 때문에 push는 실행되지 않았으며 종료 코드는 0이었습니다.
CLAUDE.md:
git rebase나pull을 실행하는 스크립트는timeout이나| tail로 감싸지 마세요. 중단이 발생하면 먼저git stash list와 reflog를 확인하세요.
record #0751 · #1037 · #1042 · 2026-10
30. 마지막 저장이 조용히 이긴다
두 세션이 같은 메모리 키에 저장했습니다. 한쪽이 요약을 다시 썼고, 몇 분 뒤 다른 쪽의 저장이 그것을 덮어썼습니다. 다음 달에는 한 세션이 30분 넘게 사본을 들고 있다가, 다른 세션이 새로 기록한 승인을 그 위에 다시 저장해 덮었습니다. 잠금도 없고, 충돌 오류도 없었습니다. 오래된 텍스트를 인용한 훅만이 누군가 알아차리게 한 유일한 이유였습니다.
CLAUDE.md: 오래 들고 있던 사본에서 절대 저장하지 마세요. 저장 직전에 다시 읽고, 새 본문에 본인의 변경만 병합하세요.
record #0767 · #1013 · 2026-10
31. 버전 값을 알아내려고 자리표시자를 저장했다가, 하루에 두 번이나 🛡
메모리 저장소에 optimistic locking이 적용된 뒤, 에이전트는 거절 응답에서 버전 값을 알아내기 위해 save 도구에 자리표시자 본문을 보냈습니다. 하루에 두 번이나 그랬고, 두 번째는 교훈이 이미 기록된 뒤였습니다. 둘 다 추정한 버전에 마이크로초가 없어서 거절되었습니다. 올바르게 맞췄다면 노트 전체가 한 줄로 대체됐을 것입니다. 이제 guard_short_save.py는 300자 미만의 저장을 거부합니다.
CLAUDE.md: 값을 알아내려고 저장 명령을 사용하지 마세요. 읽기 명령으로 버전 값을 가져오고, 저장할 때는 병합된 전체 본문을 보내세요.
record #1088 · 2026-10 · 🛡 now blocked by a guard
6. 내 머신에서는 잘 됨
32. cron 안에는 node가 없었다
승인된 예약 게시물이 일회성 cron 작업으로 실행되었습니다. 로그에는 스냅샷 성공과 "done"이 표시되었습니다. 업로드는 일어나지 않았습니다. 스크립트가 node를 그대로 호출했는데, 그것은 ~/.nvm 아래에 있었고 cron의 PATH에는 포함되지 않았기 때문입니다. cron은 로그인 셸의 설정을 읽지 않습니다.
CLAUDE.md: cron에서 실행되는 스크립트는
node/python을 절대 경로로 호출하거나 스크립트 안에서 PATH를 설정해야 합니다.
record #1182 · 2026-10
33. 남아 있던 환경 변수가 설정을 이겼다
클라우드 로그인은 성공했고 자격 증명 파일도 있었지만, 모든 도구가 service-account.json was not found로 실패했습니다. .bashrc에 남아 있던 오래된 GOOGLE_APPLICATION_CREDENTIALS가 삭제된 파일을 가리키고 있었고, 그것이 우선 적용되기 때문이었습니다. 또 다른 때에는 앱이 "high demand" / quota 오류를 냈습니다. 셸이 무료 요금제 API 키를 내보내고 있었고, 앱의 .env 로더는 기존 변수를 덮어쓰지 않기 때문이었습니다. 두 오류 메시지는 모두 다른 곳을 가리키고 있었습니다.
CLAUDE.md: 인증 또는 API 키 오류가 나면, 다시 로그인하거나 재시도하기 전에
env | grep로 남아 있는 변수부터 확인하세요.
기록 #0065 · #1121 · 2026-10
34. 배포는 성공했는데, 코드는 git 어디에도 없었다
gcloud run deploy는 작업 트리를 업로드합니다. 커밋은 필요하지 않습니다. 다섯 번의 배포 세션 동안 실서비스 코드는 커밋되지 않은 채 쌓였습니다. 그 머신을 잃으면 프로덕션을 재현하거나 롤백할 수 없습니다. git log와 배포된 revision을 비교하다가 발견했습니다.
CLAUDE.md: 배포가 성공했다고 해서 버전 관리가 된 것은 아닙니다. 배포 세션을 끝내기 전에
git status를 확인하고 배포한 내용을 커밋하세요.
기록 #0062 · 2026-07
7. 다른 사람의 API가 알려준 것
35. 목록의 첫 페이지를 전체 목록으로 간주했다
제품 목록 API는 페이지당 10개 항목을 반환합니다. 한 생성 스크립트가 첫 페이지만 보고 “이게 존재하나?”를 판단해 실제 중복 초안을 만들어 버렸습니다. 한 달 뒤 같은 API를 한 번 호출했더니 17개 제품 중 10개만 보였고, 그 호출에 의존한 판매 모니터는 나머지 7개를 절대 보지 못했을 것입니다. 페이지네이션은 오류를 내지 않습니다. 그저 다음 페이지 키를 하나 더 붙일 뿐입니다.
CLAUDE.md: 목록 API에서 존재 여부나 총개수를 판단할 때는 다음 페이지 키가 사라질 때까지 따라가고, 받은 개수를 출력하세요.
기록 #0491 · #1079 · 2026-10
36. 추론 토큰이 출력 예산을 잡아먹었다: 잘리고, 비어 있고, 청구됐다
생각하는 모델을 예전의 비추론 출력 예산으로 호출했더니 답변이 잘려 나가고 하루 할당량이 소진되었습니다. max_tokens 1200으로 추론 모델을 호출했더니 10개 응답 전부 본문이 비어 있었고, 출력은 한도까지 청구되었습니다. 다른 모델은 6000 토큰에서 중간에 끊겼는데도 오류가 없었고, 24000에서는 완료되었습니다. 잘린 답변은 짧은 답변처럼 보입니다.
CLAUDE.md: 추론 모델에는 넉넉한 출력 한도를 주고, 매번
finish_reason과 사용량을 출력하세요.length는 실패로 간주하세요. 대량 실행 전에 한 번의 호출로 출력 형식을 확인하세요.
기록 #0326 · #0355 · #0863 · 2026-09
37. 수정 스크립트는 실패했는데, 유료 제출은 그대로 실행됐다
프롬프트 수정 스크립트와 유료 생성 호출이 ;로 연결돼 있었습니다. 수정 단계에서 assert가 터져 비정상 종료됐지만, ;는 다음 명령을 무조건 실행합니다. 고치고 있던 정확한 옛 프롬프트가 제출됐고, 유료 생성 4건이 낭비됐습니다.
CLAUDE.md: 청구되는 명령을
;로 절대 연결하지 마세요.set -e또는&&를 사용하세요.
기록 #0810 · 2026-09
38. 우리 가게를 headless로 한 번 봤더니, 판매자 IP가 봇으로 찍혔다 🛡
수공예 마켓플레이스에서 우리 가게 페이지를 확인하려고, 에이전트가 PC에서 headless 스크린샷을 한 장 찍었습니다. 응답은 Access is temporarily restricted … automated activity on your network였고, 판매자 계정과 같은 IP에서 발생했습니다. 차단은 계정별이 아니라 네트워크별입니다. 이제 guard_bash.py가 그 사이트의 웹 페이지에 대한 headless 또는 curl 접근을 거부합니다. 공식 API 호스트는 계속 허용됩니다.
CLAUDE.md: 판매자 계정과 같은 네트워크에서 마켓플레이스 웹 페이지를 headless로 열거나 curl로 가져오지 마세요. 공식 API나 사람이 찍은 스크린샷을 사용하세요.
기록 #1155 · 2026-10 · 🛡 이제 가드로 차단됨
8. 누가 결국 읽게 되는가
39. .gitignore에는 venv/가 있었는데, 폴더는 .venv였다
git add -A가 가상 환경 전체, 약 1,600개 파일을 스테이징했습니다. venv/는 .venv/와 일치하지 않기 때문입니다. 사전 커밋 크기 검사가 이를 잡아냈습니다. (Python 3.13+ venv는 이제 자체 .gitignore를 작성하므로 새 환경은 안전합니다. 오래된 환경은 그렇지 않습니다.)
CLAUDE.md:
.gitignore패턴은 실제 폴더 이름과 대조하세요.git add -A후에는 커밋하기 전에 스테이징된 파일 중 1 MB가 넘는 파일이 있는지 확인하세요.
기록 #0098 · 2026-07
40. 금칙어 목록은 옛 생성기 안에 있었다 🛡
새 제품 생성기가 유명한 TV 게임쇼의 코너 이름을 페이지, PDF, 이미지 3장에 넣었습니다. 그 이름은 이미 금칙어 목록에 있었는데, 옆에 있던 다른 생성기의 검증기 안에 들어 있었습니다. 새 생성기는 그 검증기를 거치지 않았습니다. 업로드 전에 두 번째 법적 검사에서 걸렸습니다. 이제 상표 검사는 공유 사전 업로드 단계(ship_check.py)에 있으며, 모든 제품은 생성기와 무관하게 이를 통과합니다.
CLAUDE.md: 상표 및 금칙어 검사는 한 생성기 내부가 아니라 공통 사전 업로드 단계에 두세요. 새 생성기를 추가할 때는 기존 목록이 그 출력에도 적용되는지 확인하세요.
기록 #1180 · 2026-10 · 🛡 이제 가드로 차단됨
9. 하니스와 셸
41. 훅이 스크립트를 찾지 못해 모든 Bash 호출이 차단됐다
A PreToolUse 훅이 $CLAUDE_PROJECT_DIR를 통해 자신의 검사기를 찾았습니다. 테스트 결과, 그 변수가 비어 있으면 세션의 모든 Bash 호출이 차단되었습니다. 검사기는 아무것도 판단하지 않았습니다. 인터프리터가 파일을 찾지 못해 종료 코드 2로 끝났고, 이는 "차단"과 같은 코드였습니다. 자기 버그 때문에 자동으로 안전하게 실패하는 안전장치는 제거되고, 그러면 안전망도 사라집니다.
CLAUDE.md: 훅은 자기 자신의 실패(스크립트나 변수 누락)를 경고하고 통과시켜야 합니다. 종료 코드 2(차단)는 실제로 위험이 발견됐을 때만 사용합니다.
record #0345 · 2026-09
42. 한 번 차단하는 훅이 영원히 차단했다: 키가 매 호출마다 바뀌었다
한 훅은 첫 번째 Read만 한 번 차단하고(팁을 함께 제공), 그다음 재시도는 $PPID로 이름 붙인 마커 파일을 통해 허용하도록 되어 있었습니다. 그런데 Read가 계속 차단되었고, 마커 파일은 쌓여만 갔습니다. 각 훅 호출은 새 셸에서 실행되며 부모 PID도 새로 생깁니다. Read와 Edit 둘 다 멈춰버렸습니다.
CLAUDE.md: 훅이 호출 사이의 상태를 기억해야 한다면,
$PPID가 아니라 훅 입력 JSON의session_id로 키를 잡으세요.
record #0052 · 2026-07
43. 위험한 명령 훅이 그 명령을 설명하던 README를 차단했다
PreToolUse 훅에 위험한 명령 패턴을 추가하자마자, 그 명령을 언급만 하는 README를 쓰는 것까지 차단되었습니다. 텍스트가 heredoc 안에 있었고, 훅이 전체 명령 문자열을 검사했기 때문입니다. 오탐이 자주 나면 가드가 꺼지고, 그러면 원래의 사고가 다시 돌아옵니다.
CLAUDE.md: 명령 검사 훅은 heredoc 본문을 데이터로 취급해야 합니다(단, heredoc 밖에서 ssh / scp / rsync가 보이면 계속 검사하세요). 새 가드는 문서 작성 명령에도 테스트하세요.
record #0339 · 2026-09
44. pkill -f가 자기 셸을 죽였고, pgrep -f 대기는 끝나지 않았다 🛡
pkill -f 'server.js'가 서버가 아니라 Claude의 Bash 도구 셸을 죽였습니다(종료 코드 144). while pgrep -f "<script>" 루프는 끝나지 않았고, 에이전트는 끝난 배치를 실행 중이라고 보고했습니다. Claude Code는 당신의 명령을 bash -c '<전체 명령>'으로 실행하므로, 그 패턴이 그 셸 자신의 명령줄에도 들어 있습니다. 규칙으로 적어 두었는데도 이 문제는 그 후 이틀 동안 여러 번 더 반복되었습니다. 이제 guard_bash.py가 이를 차단합니다.
CLAUDE.md:
pkill -f/pgrep -f를 사용할 때는 대괄호 패턴([s]erver.js)이나 PID 파일을 사용하세요. 대기 루프 조건에 barepgrep -f패턴을 넣지 마세요.
record #0420 · #0815 · #0518 · #0816 · #0977 · #1060 · 2026-10 · 🛡 now blocked by a guard
45. 연구 에이전트가 연구 에이전트를 생성했고, 그들은 모두 함께 죽었다
Agent 도구에 대한 금지 없이 여러 코디네이터 에이전트를 심층 연구용으로 실행했습니다. 각 에이전트는 자신의 배치를 더 작은 하위 연구자들로 나눴습니다. 그들은 하나의 세션 사용량 제한과 웹 검색 제한을 공유했고, 그것이 소진되자 모두 한꺼번에 죽었으며 중간 결과는 하나도 저장되지 않았습니다.
CLAUDE.md: 모든 하위 에이전트 프롬프트에서 하위 에이전트 생성을 금지하고, 에이전트별 웹 호출 수를 제한하며, 부분 결과를 진행하면서 파일에 기록하도록 요구하세요.
record #1077 · 2026-10
46. 만료일이 있는 섹션에 적힌, 모든 작업을 위한 규칙
모든 작업에 적용되어야 할 규칙이 날짜가 붙은 임시 섹션 안과, 필요할 때만 여는 문서 안에 들어 있었습니다. 그 섹션이 만료되면 사라졌을 것이고, 다른 기기의 세션에서는 아예 보이지도 않았습니다. Claude Code의 자동 메모리도 같은 한계를 가집니다. 한 기기에서만 유지됩니다. 규칙이 어디에 적용되는지는 파일이 언제 어디에서 읽히는지에 따라 결정됩니다.
CLAUDE.md: 모든 작업에 대한 규칙은 날짜가 없는 영구적인
CLAUDE.md부분에 넣어야 하며, 모든 세션이 로드하고 git으로 공유하는 곳이어야 합니다. 날짜가 있는 섹션, 필요 시 여는 문서, 자동 메모리에만 두지 마세요.
record #0704 · 2026-09
47. 파이프와 heredoc을 함께 쓰자 Python이 데이터를 비워진 입력으로 읽었다
cmd | python3 - <<EOF는 스크립트가 명령의 출력을 json.load(sys.stdin)으로 읽도록 하려는 의도였습니다. 하지만 Python은 빈 입력을 받았고 JSONDecodeError로 종료했습니다. stdin은 하나뿐입니다. heredoc이 파이프를 덮어써 버렸고, python3 -는 stdin에서 스크립트 자신을 읽습니다.
CLAUDE.md:
cmd | python3 - <<EOF처럼 파이프와 heredoc을 함께 쓰지 마세요. 스크립트는 heredoc으로 전달하고, 데이터는 환경 변수나 임시 파일로 전달하세요.
record #0009 · 2026-06
10. 드리프트하는 메모
48. 주인 없는 약속: 48시간 동안 아무도 시작하지 않았다
에이전트는 다음 단계 네 가지를 적었습니다. 그중 세 가지는 에이전트 자신의 작업이었습니다(측정, 동결, 업로드). 48시간 동안 아무도 그것들을 시작하지 않았습니다. "사람에게 보고하는" 말투로 적혀 있어서, 인간의 작업처럼 읽혔고, 에이전트는 승인을 기다렸습니다.
CLAUDE.md: 다음 단계마다 소유자를 명시하고(AI / 사용자) 약속하세요. AI가 맡는 항목은 다음 세션의 첫 작업입니다.
record #0354 · 2026-09
49. 현재형으로 쓰인 계획
어떤 실행 지침서는 아직 존재하지도 않는 스크립트를 설명하고 있었다. 공유 엔진의 README에는 7개의 게임이 마이그레이션되었다고 적혀 있었지만, 측정해 보니 0개였다. 한 전략 문서는 어떤 채널을 "파생 채널"이라고 불렀다. 그 채널에는 영상이 1개뿐이었고, 완결된 에피소드들은 게시되지 않은 채 열흘 동안 쌓여 있었다. 문장에는 "예정"이라는 시제 표지가 없다. 다음 세션은 그것들을 사실로 전제하고 이어서 진행된다.
CLAUDE.md: 존재하지 않는 파일, 기능, 채널을 현재형으로 설명하지 마세요. 계획은 계획이라고 표시하고, grep 결과의 수치를 명령어와 함께 적으세요.
record #0135 · #0473 · #0623 · 2026-09
50. 세 문서는 10개 장이라고 했지만, 원본에는 15개가 있었다
인수인계 문서, 계획 문서, 작업 노트 모두 학습 앱에 10개 장이 있다고 적고 있었다. 이를 정의한 원본 파일에는 15개가 있었다. 그 숫자가 먼저 무엇을 배포할지 결정했다. 세 문서가 서로를 베낀 것은 증거 한 조각일 뿐, 세 조각이 아니다.
CLAUDE.md: 개수(건수, 버전)는 문서가 아니라 원본에서 세세요. 문서들이 서로 일치한다고 해서 검증된 것은 아닙니다.
record #0362 · 2026-09
모든 50개 항목에 공통된 패턴
거의 모든 항목은 같은 형태다. "완료됨"을 뜻하는 신호를 "끝남"으로 읽거나, 기억을 사실로 읽는 것이다. 효과가 있었던 해결책은 결코 "더 열심히 시도하기"가 아니었다. 두 가지 중 하나였다.
- 결과를 다른 방식으로 다시 읽기: 공개 URL, 파일 개수, 해시, 실제 API 상태.
- 문서화된 규칙이 두 번 실패하면, 그것을 가드로 바꾸기. 되돌릴 수 없게 되는 순간, 즉 푸시·업로드·삭제 시점에 실수를 차단하도록 만드는 것이다.
우리가 최종적으로 갖추게 된 7개의 가드(PreToolUse와 Stop 훅, 그리고 차단을 증명하는 테스트가 포함된 배포 전 점검)는 여기 있습니다: claude-code-mistake-guards.
이것들은 300개 이상의 항목 중 50개입니다. 전체 로그는 현재 한국어로만 존재합니다. 영어로 읽고 싶다면, 저장소에 별점을 주세요. 그것이 번역할지 결정하는 기준입니다.
당신은 어느 항목을 겪었나요? 에이전트가 반복하는 실수에 대해 듣고 싶습니다.
출처
- 원문: https://dev.to/hadiux0295/50-real-claude-code-mistakes-and-the-rule-that-stops-each-36di (2026-10-26 · SynuraCore)