Tips & Tricks

Claude Code로 발생한 운영 장애 7가지 사례: RCA·재발 방지책 포함 완전 복구 절차

Claude Code 운영에서 실제로 발생한 장애 7건 공개. API 키 유출·DB 삭제·과금 폭발·서비스 다운의 원인·복구 절차·RCA·재발 방지책을 완전 해설.

“Claude Code는 편리하지만 운영 환경에서 쓰기엔 무섭다”—이런 감각을 갖는 엔지니어가 많습니다. 그 직감은 맞습니다.

Claude Code는 IDE보다 높은 권한으로 파일 시스템과 셸을 조작합니다. 동시에, 승인 버튼을 계속 클릭하면서 경계심이 느슨해지는 인간 심리의 취약점도 있습니다. 이 두 가지가 결합될 때 운영 장애가 발생합니다.

이 글에서는 실제로 Claude Code가 관련된 운영 장애 7가지 사례를 원인·피해 범위·복구 절차·RCA(근본 원인 분석)·재발 방지책과 함께 공개합니다. 귀사의 사고가 나기 전에 읽어두십시오.


사례 1: API 키 유출 → 부정 사용으로 360만 원 과금

타임라인

09:12  Claude Code에 "CI에 환경 변수를 전달하기 위해 .env를 commit해줘"라고 지시
09:13  git add .env && git push가 승인 없이 실행됨 (allow 목록이 너무 넓었음)
09:14  GitHub이 시크릿 스캔으로 감지, 이메일 알림 발송
09:31  AWS 크롤러가 OpenAI 키를 감지, 부정 사용 시작
11:00  OpenAI 대시보드에서 360만 원 과금 확인

복구 절차

# Step 1: 즉시 API 키 무효화 (최우선·5분 이내)
# → OpenAI/각 서비스 대시보드에서 키 revoke

# Step 2: git 이력에서 .env를 완전 삭제
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

# Step 3: 전체 브랜치 force push
git push origin --force --all
git push origin --force --tags

# Step 4: .gitignore에 추가하여 재발 방지
echo ".env" >> .gitignore
git add .gitignore && git commit -m "security: add .env to gitignore"

# Step 5: 새 API 키 발급하여 .env에 설정

RCA

  • 직접 원인: settings.jsonallowBash(git add*)가 있어서 확인 없이 실행됨
  • 근본 원인: 보안 설정을 나중으로 미루고 프로덕트 코드를 우선시했음

재발 방지

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash(git add*)",
      "hooks": [{
        "type": "command",
        "command": "git diff --cached --name-only | grep -E '\\.env' && echo '🚨 .env 감지! 중단' && exit 1 || exit 0"
      }]
    }]
  }
}

사례 2: rm -rf로 프로젝트 전체가 소멸

타임라인

14:33  "node_modules를 정리하고 재설치해줘"라고 지시
14:33  Claude Code가 rm -rf node_modules 실행 (여기까지는 정상)
14:34  이어서 "오래된 빌드 파일도 지워줘" 지시로 rm -rf dist/ 실행
14:34  경로 해석 오류로 rm -rf dist /src가 실행됨 (공백 구분)
14:35  src/ 디렉터리 전체 소멸. git 관리 외 설정 파일도 삭제됨
14:40  git checkout .으로 관리 대상은 복원했지만, .env·로컬 설정은 영구 소실

복구 절차

# git 관리 대상은 복원 가능
git checkout .
git clean -fd   # 불필요한 파일 삭제

# 삭제된 파일을 git stash나 reflog에서 찾기
git stash list
git reflog

# git 관리 외 (.env 등)는 백업에서 복원
# → 백업이 없는 경우 처음부터 재설정

RCA

  • 직접 원인: 공백 포함 경로를 적절히 인용 부호로 감싸지 않아 rm -rf dist /src가 됨
  • 근본 원인: rm -rfask가 아닌 allow에 있었음

재발 방지

{
  "permissions": {
    "deny": ["Bash(rm -rf ~*)", "Bash(rm -rf /*)"],
    "ask": ["Bash(rm*)"]
  },
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash(rm*)",
      "hooks": [{
        "type": "command",
        "command": "echo '⚠️ 삭제 명령 감지. 대상: $CLAUDE_TOOL_INPUT_COMMAND\n5초 후 실행. Ctrl+C로 중단.' && sleep 5"
      }]
    }]
  }
}

사례 3: git push --force로 동료 3명의 커밋이 소멸

타임라인

16:00  "리모트와 충돌하고 있어. 로컬을 우선해서 덮어써줘"라고 지시
16:01  git push --force origin main 실행
16:01  동료 3명이 그날 커밋한 약 200줄의 코드가 소멸
16:10  동료 A가 "어? 내 커밋이 사라졌는데"라고 Slack 발언
16:15  원인 판명. 동료 A/B는 로컬 PC에 남아 있었지만, C씨는 삭제 후 완전 소실

복구 절차

# reflog로 사라진 커밋 찾기 (실행한 본인 머신에서)
git reflog | head -30
# → 예: abc1234 HEAD@{3}: push --force 전의 커밋

# 사라진 커밋 복원
git checkout -b recovery abc1234
git push origin recovery

# main에 머지하여 정리
git checkout main
git merge recovery --no-ff
git push origin main

RCA

  • 직접 원인: 충돌 해소 의도가 --force-with-lease가 아닌 --force로 해석됨
  • 근본 원인: main 브랜치로의 force push가 deny에 없었음

재발 방지

{
  "permissions": {
    "deny": [
      "Bash(git push --force *main*)",
      "Bash(git push --force *master*)",
      "Bash(git push -f *main*)"
    ]
  }
}
<!-- CLAUDE.md -->
## git 규칙
- `git push --force`는 금지
- 충돌 해소에는 `git push --force-with-lease`를 사용
- main/master로의 push는 반드시 사용자 확인 후

사례 4: DB 마이그레이션 실패로 운영 데이터 4만 건이 소멸

타임라인

10:00  "users 테이블에 phone_number 컬럼을 추가하는 마이그레이션을 실행해줘"
10:01  Claude Code가 마이그레이션 스크립트를 생성·실행
10:01  스크립트 버그로 DROP COLUMN을 포함한 역방향 마이그레이션이 실행됨
10:02  운영의 users.email 컬럼(NOT NULL)이 소멸
10:02  전체 API가 500 에러 시작, 서비스 완전 정지
10:05  장애 인식, 원인 조사 시작
10:30  전날 스냅샷에서 복원 (1일분 데이터 소실)

복구 절차

# 1. 즉시 서비스를 유지보수 모드로
# nginx: return 503;  또는  Vercel: 유지보수 페이지

# 2. DB 현상 확인
psql $DATABASE_URL -c "\d users"

# 3. 백업에서 복원 (RDS의 경우)
aws rds restore-db-instance-to-point-in-time \
  --source-db-instance-identifier mydb \
  --target-db-instance-identifier mydb-restored \
  --restore-time 2026-04-17T23:00:00Z

# 4. 컬럼이 남아 있는 경우 수동으로 추가
ALTER TABLE users ADD COLUMN email VARCHAR(255);
UPDATE users SET email = '(복원 필요)' WHERE email IS NULL;

# 5. 서비스 재개

RCA

  • 직접 원인: 마이그레이션 스크립트 생성 시 up/down을 혼동함
  • 근본 원인: 스테이징 환경에서의 테스트 없이 운영에서 실행함

재발 방지

<!-- CLAUDE.md -->
## DB 마이그레이션 필수 규칙
1. 스테이징 환경에서 반드시 동작 확인한 후 운영으로
2. 마이그레이션 전 반드시 수동 백업:
   pg_dump $DATABASE_URL > backup_$(date +%Y%m%d_%H%M%S).sql
3. DROP COLUMN / TRUNCATE / DELETE (WHERE 없음)를 포함한 스크립트는
   반드시 사용자에게 확인 받은 후 실행
4. 운영 DATABASE_URL의 경우 실행 전 '운영 DB에 쓰기입니다. 계속하시겠습니까?' 표시

사례 5: 무한 API 호출로 하룻밤에 90만 원 과금

타임라인

23:00  "에러가 나면 자동으로 재시도해줘"라고 지시하고 배치 처리 시작
23:01  외부 API가 503을 반환하기 시작
23:01  재시도 로직이 상한 없이 동작, 1초 간격으로 계속 호출
07:00  다음 날 아침, Anthropic에서 "사용량이 상한에 가까워지고 있습니다" 알림
07:05  확인하니 28,000번의 API 호출로 90만 원 과금 발생

복구 절차

# 1. 즉시 프로세스 정지
pkill -f "node batch-process.js"

# 2. 과금 상황을 확인하고 Anthropic 지원에 연락
# → 성실한 대응으로 일부 환불되는 경우 있음

# 3. 사용량 알림 설정
# Anthropic 콘솔 → Usage Limits → 월별 예산 알림 설정

재발 방지

// utils/retry.ts — 반드시 이 유틸리티를 사용할 것
export async function withRetry<T>(
  fn: () => Promise<T>,
  { maxAttempts = 3, baseDelayMs = 1000, maxDelayMs = 30000 } = {}
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      const delay = Math.min(
        baseDelayMs * 2 ** (attempt - 1) + Math.random() * 500,
        maxDelayMs
      );
      console.warn(`Retry ${attempt}/${maxAttempts} in ${Math.round(delay)}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}
<!-- CLAUDE.md -->
## API 호출 규칙
- 재시도는 최대 3회, exponential backoff 필수
- while(true) + API 호출은 절대 금지
- 배치 처리는 건수 상한을 명시적으로 설정
- 운영 실행 전에 --dry-run으로 스모크 테스트

사례 6: 배포 후 의존성 파손으로 서비스 전체 정지

타임라인

15:00  "패키지를 최신 버전으로 업데이트해줘"라고 지시
15:01  npm update 실행, package-lock.json이 대폭 변경
15:05  로컬 빌드 통과
15:10  운영 배포 실행
15:12  운영에서 의존 패키지의 메이저 버전 불일치 발생, 기동 실패
15:12  503 에러, 서비스 전체 정지
15:30  이전 버전으로 롤백 완료

복구 절차

# Vercel / Cloudflare Pages의 경우: 이전 배포를 즉시 reactivate
# → 대시보드에서 1클릭으로 롤백 가능

# git revert로 코드도 되돌리기
git revert HEAD~1
git push

# package-lock.json을 이전 버전으로 되돌리기
git checkout HEAD~1 -- package-lock.json
npm ci  # package-lock.json을 엄격하게 사용

RCA

  • 직접 원인: npm update가 메이저 버전을 올려 breaking change가 발생
  • 근본 원인: 스테이징 환경에서의 배포 확인을 생략함

재발 방지

<!-- CLAUDE.md -->
## 패키지 관리 규칙
- npm update는 금지 (npm update --save-dev만 조건부 OK)
- 패키지의 메이저 버전 업은 사용자 확인 필수
- 배포 전 반드시 스테이징 환경에서 동작 확인
- 운영 배포 후 5분간 에러 로그 모니터링

사례 7: 권한 설정 실수로 전체 사용자 데이터가 열람 가능

타임라인

11:00  "관리 화면의 API 엔드포인트에 사용자 목록을 추가해줘"라고 지시
11:05  /api/admin/users가 인증 체크 없이 구현됨
11:10  운영 배포
11:10  누구나 전체 사용자의 개인정보에 접근 가능한 상태 발생
13:30  보안 감사 툴이 미인증 엔드포인트를 감지
13:35  엔드포인트를 즉시 무효화

복구 절차

# 1. 즉시 해당 엔드포인트 무효화
# nginx: location /api/admin { return 403; }

# 2. 액세스 로그를 확인하여 유출 범위 특정
grep "/api/admin/users" /var/log/nginx/access.log | \
  awk '{print $1}' | sort | uniq -c | sort -rn

# 3. 영향받은 사용자에게 통지 (개인정보 보호법 요건 확인)

# 4. 인증 미들웨어를 추가하여 배포

RCA

  • 직접 원인: “관리 화면에 추가해줘”라는 지시에서 인증 요건이 전달되지 않음
  • 근본 원인: CLAUDE.md에 보안 요건이 기재되어 있지 않았음

재발 방지

<!-- CLAUDE.md -->
## API 보안 필수 요건
- /api/admin/* 엔드포인트에는 반드시 관리자 인증 체크를 구현
- /api/user/*에는 반드시 로그인 인증 체크를 구현
- 인증 없이 접근 가능한 API는 /api/public/*만
- 새 API를 추가할 때는 인증 레벨을 코멘트에 명기

장애 대응 공통 플로 (포스트모텀 템플릿)

# 포스트모텀: [장애 제목]

## 요약
- 발생 일시: YYYY-MM-DD HH:MM
- 감지 일시: YYYY-MM-DD HH:MM
- 복구 일시: YYYY-MM-DD HH:MM
- 영향 범위: (사용자 수/기능/기간)
- 심각도: P0/P1/P2/P3

## 타임라인
| 시각  | 사건 |
|-------|------|
| HH:MM | 장애 발생 |
| HH:MM | 감지 |
| HH:MM | 대응 시작 |
| HH:MM | 원인 특정 |
| HH:MM | 복구 완료 |

## 근본 원인
- 직접 원인:
- 근본 원인:
- 확대 원인:

## 재발 방지책
| 대책 | 담당 | 기한 |
|------|------|------|
|      |      |      |

## 배운 점

정리: 장애를 막는 최소한의 설정

// 지금 바로 복사하여 .claude/settings.json에 붙여넣기
{
  "permissions": {
    "deny": [
      "Bash(rm -rf ~*)",
      "Bash(rm -rf /*)",
      "Bash(git push --force *main*)",
      "Bash(git push --force *master*)",
      "Bash(git push -f *main*)",
      "Bash(DROP TABLE*)",
      "Bash(TRUNCATE *)",
      "Bash(curl * | bash)",
      "Bash(wget * | sh)"
    ],
    "ask": [
      "Write(**)", "Edit(**)",
      "Bash(rm*)", "Bash(git commit*)",
      "Bash(git push*)", "Bash(*deploy*)",
      "Bash(npm install*)", "Bash(*migrate*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git add*)",
        "hooks": [{ "type": "command",
          "command": "git diff --cached --name-only | grep '\\.env' && echo '🚨 .env 감지! 중단' && exit 1 || exit 0" }]
      },
      {
        "matcher": "Bash(rm*)",
        "hooks": [{ "type": "command",
          "command": "echo '⚠️ 삭제 명령 감지. 5초 후 실행. Ctrl+C로 중단.' && sleep 5" }]
      }
    ]
  }
}

운영 장애는 “설정에 30분 투자하는 것”을 아낀 결과로 발생합니다. 이 글의 7가지 사례는 모두 적절한 settings.jsonCLAUDE.md가 있었다면 방지할 수 있었습니다.

관련 글

참고 자료

#claude-code #incident #production #sre #security #postmortem

Claude Code 워크플로우를 한 단계 업그레이드하세요

지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.

무료 제공

무료 PDF: 5분 완성 Claude Code 치트시트

이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.

개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.

Masa

이 글을 작성한 사람

Masa

Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.