Claude Code .claude 디렉토리 1.3GB 정리하기 — 세션 로그 자동 정리 스크립트

Claude Code의 .claude 디렉토리가 1.3GB로 불어난 원인을 분석하고, memory를 보호하면서 오래된 세션 데이터를 자동 정리하는 스크립트를 만들었습니다.

1. 문제 상황

Claude Code를 약 4주간 사용하고 나서 ~/.claude/ 디렉토리를 확인해봤습니다.

du -sh ~/.claude/
# 1.5G /Users/user/.claude/

1.5GB. IDE 하나 설치한 것도 아닌데, CLI 도구 하나가 이 정도 공간을 차지하고 있었습니다.

문제는 Claude Code에 자동 정리 기능이 없다는 점입니다. 세션을 열 때마다 로그가 쌓이고, 아무도 치워주지 않습니다.

2. .claude 디렉토리 구조 분석

가장 먼저 어디에 뭐가 쌓여있는지 확인했습니다.

du -sh ~/.claude/*/ | sort -rh

결과는 이렇습니다:

1.2G    projects/
199M    debug/
 84M    shell-snapshots/
 34M    file-history/
 31M    plugins/
8.6M    todos/
1.3M    plans/
916K    paste-cache/
700K    telemetry/
360K    tasks/
352K    image-cache/

각 디렉토리의 역할

디렉토리 역할 정리 가능?
projects/ 세션 대화 로그 + 서브에이전트 출력 7일 이상 된 것 정리 가능
debug/ 디버그 로그 정리 가능
shell-snapshots/ 셸 환경 스냅샷 정리 가능
file-history/ 파일 편집 이력 (undo용) 정리 가능
plugins/ 설치된 플러그인 유지
todos/ 세션별 TODO 파일 정리 가능
plans/ 플랜 모드 출력 파일 정리 가능
paste-cache/ 붙여넣기 캐시 정리 가능
tasks/ 태스크 추적 파일 정리 가능
image-cache/ 이미지 캐시 정리 가능

여기에 추가로 루트 레벨 파일도 있습니다:

ls ~/.claude/security_warnings_state_*.json | wc -l
# 41

세션마다 생성되는 security_warnings_state_*.json 파일이 41개나 쌓여 있었습니다.

3. 가장 큰 범인: projects/ 디렉토리

projects/ 디렉토리가 전체의 80%를 차지합니다. 구조를 살펴보면:

~/.claude/projects/
  -Users-user-projects-project-a/               ← 661.8 MB
    ├── a1b2c3d4-...-e5f6.jsonl          ← 세션 대화 로그
    ├── a1b2c3d4-...-e5f6/               ← 세션 서브디렉토리
    ├── ...                              ← (245개 파일 + 86개 디렉토리)
    └── memory/MEMORY.md                 ← 영구 메모리 (보존!)
  -Users-user-projects-project-b/               ← 132.1 MB
  -Users-user-projects-project-c/               ← 60.0 MB
  ...

디렉토리 이름 규칙

프로젝트 경로의 /-로 치환한 이름입니다:

/Users/user/projects/my-app
→ -Users-user-projects-my-app

세션 데이터 구성

각 프로젝트 디렉토리 안에는 두 종류의 세션 데이터가 있습니다:

  • UUID.jsonl: 세션 대화 전체 기록 (사용자 메시지 + Claude 응답 + 도구 호출)
  • UUID/ 디렉토리: 해당 세션의 플랜 파일, 서브에이전트 출력 등

이 데이터는 claude --resume <session-id>로 이전 세션을 이어갈 때 사용됩니다. 하지만 7일 이상 지난 세션을 다시 이어붙일 일은 거의 없습니다.

절대 건드리면 안 되는 것: memory/

projects/<project>/memory/MEMORY.md

이 파일은 Claude Code의 영구 메모리입니다. 세션 간에 유지되는 학습 노트이므로, 정리 스크립트에서 반드시 제외해야 합니다.

4. 정리 스크립트 작성

설계 원칙

  1. 기본 dry-run: 실수로 실행해도 아무것도 삭제되지 않음
  2. memory/ 절대 보호: UUID 패턴 검증 + memory 이름 체크 이중 안전장치
  3. 날짜 기준 설정 가능: 기본 7일, 인자로 변경 가능
  4. 삭제 전 요약: 몇 개 파일, 얼마나 절약되는지 미리 표시

전체 스크립트

~/.claude/scripts/cleanup-sessions.sh:

#!/bin/bash
set -euo pipefail

CLAUDE_DIR="$HOME/.claude"
PROJECTS_DIR="$CLAUDE_DIR/projects"
MAX_AGE_DAYS=7
DRY_RUN=true

numfmt_bytes() {
  local bytes=$1
  if [ "$bytes" -ge 1073741824 ]; then
    printf "%.1f GB" "$(echo "$bytes / 1073741824" | bc -l)"
  elif [ "$bytes" -ge 1048576 ]; then
    printf "%.1f MB" "$(echo "$bytes / 1048576" | bc -l)"
  elif [ "$bytes" -ge 1024 ]; then
    printf "%.1f KB" "$(echo "$bytes / 1024" | bc -l)"
  else
    printf "%d B" "$bytes"
  fi
}

cleanup_files() {
  local dir="$1" pattern="$2" label="$3"
  local count=0 bytes=0
  [ -d "$dir" ] || return 0
  while IFS= read -r -d '' file; do
    local size
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    bytes=$((bytes + size))
    count=$((count + 1))
    if ! $DRY_RUN; then rm -f "$file"; fi
  done < <(find "$dir" -maxdepth 1 -name "$pattern" -type f -mtime +"$MAX_AGE_DAYS" -print0)
  if [ "$count" -gt 0 ]; then
    echo "  $label: ${count} files ($(numfmt_bytes "$bytes"))"
    total_files=$((total_files + count))
    total_bytes=$((total_bytes + bytes))
  fi
}

cleanup_dir_contents() {
  local dir="$1" label="$2"
  local count=0 bytes=0
  [ -d "$dir" ] || return 0
  while IFS= read -r -d '' file; do
    local size
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    bytes=$((bytes + size))
    count=$((count + 1))
    if ! $DRY_RUN; then rm -f "$file"; fi
  done < <(find "$dir" -type f -mtime +"$MAX_AGE_DAYS" -print0)
  if [ "$count" -gt 0 ]; then
    echo "  $label: ${count} files ($(numfmt_bytes "$bytes"))"
    total_files=$((total_files + count))
    total_bytes=$((total_bytes + bytes))
  fi
}

for arg in "$@"; do
  if [[ "$arg" == "--execute" ]]; then DRY_RUN=false
  elif [[ "$arg" =~ ^[0-9]+$ ]]; then MAX_AGE_DAYS="$arg"; fi
done

if $DRY_RUN; then
  echo "=== DRY RUN (add --execute to actually delete) ==="
else
  echo "=== EXECUTE MODE ==="
fi
echo "Target: files older than ${MAX_AGE_DAYS} days"
echo ""

total_files=0
total_dirs=0
total_bytes=0

echo "[projects/ session logs]"
for project_dir in "$PROJECTS_DIR"/*/; do
  [ -d "$project_dir" ] || continue
  project_name=$(basename "$project_dir")
  project_files=0 project_dirs=0 project_bytes=0

  while IFS= read -r -d '' file; do
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    project_bytes=$((project_bytes + size))
    project_files=$((project_files + 1))
    if ! $DRY_RUN; then rm -f "$file"; fi
  done < <(find "$project_dir" -maxdepth 1 -name "*.jsonl" -type f -mtime +"$MAX_AGE_DAYS" -print0)

  while IFS= read -r -d '' dir; do
    dirname=$(basename "$dir")
    [[ "$dirname" == "memory" ]] && continue
    if [[ "$dirname" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
      size=$(du -sk "$dir" 2>/dev/null | cut -f1)
      project_bytes=$((project_bytes + size * 1024))
      project_dirs=$((project_dirs + 1))
      if ! $DRY_RUN; then rm -rf "$dir"; fi
    fi
  done < <(find "$project_dir" -maxdepth 1 -type d -mtime +"$MAX_AGE_DAYS" -not -path "$project_dir" -print0)

  if [ $((project_files + project_dirs)) -gt 0 ]; then
    echo "  $project_name: ${project_files} files, ${project_dirs} dirs ($(numfmt_bytes $project_bytes))"
    total_files=$((total_files + project_files))
    total_dirs=$((total_dirs + project_dirs))
    total_bytes=$((total_bytes + project_bytes))
  fi
done
echo ""

echo "[other temp data]"
cleanup_dir_contents "$CLAUDE_DIR/debug" "debug/"
cleanup_dir_contents "$CLAUDE_DIR/shell-snapshots" "shell-snapshots/"
cleanup_dir_contents "$CLAUDE_DIR/file-history" "file-history/"
cleanup_dir_contents "$CLAUDE_DIR/todos" "todos/"
cleanup_dir_contents "$CLAUDE_DIR/plans" "plans/"
cleanup_dir_contents "$CLAUDE_DIR/tasks" "tasks/"
cleanup_dir_contents "$CLAUDE_DIR/paste-cache" "paste-cache/"
cleanup_dir_contents "$CLAUDE_DIR/image-cache" "image-cache/"
cleanup_files "$CLAUDE_DIR" "security_warnings_state_*.json" "security_warnings_state"

echo ""
echo "--- summary ---"
echo "Files: ${total_files}"
echo "Directories: ${total_dirs} (UUID sessions)"
echo "Space saved: $(numfmt_bytes $total_bytes)"

if $DRY_RUN && [ $((total_files + total_dirs)) -gt 0 ]; then
  echo ""
  echo "To delete: $0 ${MAX_AGE_DAYS} --execute"
fi

아래에서 각 부분을 나눠서 설명하겠습니다.

#!/bin/bash
set -euo pipefail

CLAUDE_DIR="$HOME/.claude"
PROJECTS_DIR="$CLAUDE_DIR/projects"
MAX_AGE_DAYS=7
DRY_RUN=true

먼저 macOS에는 numfmt가 없으므로, 바이트를 사람이 읽을 수 있는 단위로 변환하는 함수를 만듭니다:

numfmt_bytes() {
  local bytes=$1
  if [ "$bytes" -ge 1073741824 ]; then
    printf "%.1f GB" "$(echo "$bytes / 1073741824" | bc -l)"
  elif [ "$bytes" -ge 1048576 ]; then
    printf "%.1f MB" "$(echo "$bytes / 1048576" | bc -l)"
  elif [ "$bytes" -ge 1024 ]; then
    printf "%.1f KB" "$(echo "$bytes / 1024" | bc -l)"
  else
    printf "%d B" "$bytes"
  fi
}

인자 파싱은 간단하게 처리합니다. 숫자는 날짜, --execute는 실제 삭제 모드입니다:

for arg in "$@"; do
  if [[ "$arg" == "--execute" ]]; then
    DRY_RUN=false
  elif [[ "$arg" =~ ^[0-9]+$ ]]; then
    MAX_AGE_DAYS="$arg"
  fi
done

헬퍼 함수

두 가지 정리 패턴이 있습니다:

# 특정 패턴의 파일 정리 (예: security_warnings_state_*.json)
cleanup_files() {
  local dir="$1" pattern="$2" label="$3"
  local count=0 bytes=0

  [ -d "$dir" ] || return 0

  while IFS= read -r -d '' file; do
    local size
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    bytes=$((bytes + size))
    count=$((count + 1))
    if ! $DRY_RUN; then
      rm -f "$file"
    fi
  done < <(find "$dir" -maxdepth 1 -name "$pattern" \
           -type f -mtime +"$MAX_AGE_DAYS" -print0)

  if [ "$count" -gt 0 ]; then
    echo "  $label: ${count}개 ($(numfmt_bytes "$bytes"))"
    total_files=$((total_files + count))
    total_bytes=$((total_bytes + bytes))
  fi
}

# 디렉토리 내 오래된 파일 전체 정리
cleanup_dir_contents() {
  local dir="$1" label="$2"
  local count=0 bytes=0

  [ -d "$dir" ] || return 0

  while IFS= read -r -d '' file; do
    local size
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    bytes=$((bytes + size))
    count=$((count + 1))
    if ! $DRY_RUN; then
      rm -f "$file"
    fi
  done < <(find "$dir" -type f -mtime +"$MAX_AGE_DAYS" -print0)

  if [ "$count" -gt 0 ]; then
    echo "  $label: ${count}개 ($(numfmt_bytes "$bytes"))"
    total_files=$((total_files + count))
    total_bytes=$((total_bytes + bytes))
  fi
}

cleanup_filesmaxdepth 1로 루트 레벨 파일만, cleanup_dir_contents는 서브디렉토리까지 재귀적으로 탐색합니다.

projects/ 세션 로그 정리

여기가 핵심입니다. .jsonl 파일과 UUID 디렉토리를 정리하되, memory/ 디렉토리는 절대 건드리지 않습니다:

for project_dir in "$PROJECTS_DIR"/*/; do
  [ -d "$project_dir" ] || continue

  # .jsonl 세션 로그 정리
  while IFS= read -r -d '' file; do
    size=$(stat -f%z "$file" 2>/dev/null || echo 0)
    project_bytes=$((project_bytes + size))
    project_files=$((project_files + 1))
    if ! $DRY_RUN; then
      rm -f "$file"
    fi
  done < <(find "$project_dir" -maxdepth 1 -name "*.jsonl" \
           -type f -mtime +"$MAX_AGE_DAYS" -print0)

  # UUID 세션 디렉토리 정리
  while IFS= read -r -d '' dir; do
    dirname=$(basename "$dir")
    [[ "$dirname" == "memory" ]] && continue  # ← 안전장치 1
    # UUID 패턴이 아니면 스킵                    # ← 안전장치 2
    if [[ "$dirname" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
      size=$(du -sk "$dir" 2>/dev/null | cut -f1)
      project_bytes=$((project_bytes + size * 1024))
      project_dirs=$((project_dirs + 1))
      if ! $DRY_RUN; then
        rm -rf "$dir"
      fi
    fi
  done < <(find "$project_dir" -maxdepth 1 -type d \
           -mtime +"$MAX_AGE_DAYS" -not -path "$project_dir" -print0)
done

memory/ 보호를 위한 이중 안전장치:

  1. 이름 체크: dirname == "memory"이면 무조건 스킵
  2. UUID 패턴 매칭: UUID 형식이 아니면 삭제하지 않음

기타 임시 데이터 정리

cleanup_dir_contents "$CLAUDE_DIR/debug" "debug/ (디버그 로그)"
cleanup_dir_contents "$CLAUDE_DIR/shell-snapshots" "shell-snapshots/ (셸 스냅샷)"
cleanup_dir_contents "$CLAUDE_DIR/file-history" "file-history/ (파일 편집 히스토리)"
cleanup_dir_contents "$CLAUDE_DIR/todos" "todos/ (TODO 파일)"
cleanup_dir_contents "$CLAUDE_DIR/plans" "plans/ (플랜 파일)"
cleanup_dir_contents "$CLAUDE_DIR/tasks" "tasks/ (태스크 파일)"
cleanup_dir_contents "$CLAUDE_DIR/paste-cache" "paste-cache/ (붙여넣기 캐시)"
cleanup_dir_contents "$CLAUDE_DIR/image-cache" "image-cache/ (이미지 캐시)"
cleanup_files "$CLAUDE_DIR" "security_warnings_state_*.json" \
              "security_warnings_state (보안 경고)"

5. 사용법

dry-run (미리보기)

# 기본 7일 기준
~/.claude/scripts/cleanup-sessions.sh

# 14일 이상 된 것만
~/.claude/scripts/cleanup-sessions.sh 14

출력 예시:

=== DRY RUN (실제 삭제하려면 --execute 추가) ===
대상: 7일 이상 된 데이터

[projects/ 세션 로그]
  -Users-user-projects-project-a: 245 files, 86 dirs (661.8 MB)
  -Users-user-projects-project-b: 50 files, 20 dirs (132.1 MB)
  -Users-user-projects-project-c: 141 files, 42 dirs (60.0 MB)
  ...

[기타 임시 데이터]
  debug/ (디버그 로그): 691개 (145.4 MB)
  shell-snapshots/ (셸 스냅샷): 481개 (83.0 MB)
  file-history/ (파일 편집 히스토리): 2646개 (23.4 MB)
  todos/ (TODO 파일): 1982개 (189.1 KB)
  ...

--- 요약 ---
파일: 6806개
디렉토리: 246개 (UUID 세션)
절약: 1.3 GB

실제 삭제

~/.claude/scripts/cleanup-sessions.sh 7 --execute

cron으로 자동화 (선택)

매주 일요일 새벽 3시에 자동 정리하도록 설정할 수도 있습니다:

crontab -e
0 3 * * 0 ~/.claude/scripts/cleanup-sessions.sh 7 --execute >> ~/.claude/scripts/cleanup.log 2>&1

6. 핵심 개념 정리

항목 설명
UUID.jsonl 세션 대화 전체 기록 (--resume용)
UUID/ 디렉토리 세션 부속 파일 (플랜, 서브에이전트 출력)
memory/ 영구 메모리 (MEMORY.md) — 절대 삭제 금지
debug/ 디버그 로그 — 문제 해결 후 불필요
shell-snapshots/ 셸 환경 스냅샷 — 세션 종료 후 불필요
file-history/ 파일 편집 이력 (undo) — 오래된 것 불필요
security_warnings_state_*.json 세션별 보안 경고 기록 — 세션 종료 후 불필요

7. 건드리면 안 되는 파일들

스크립트가 건드리지 않는 중요 파일/디렉토리 목록입니다:

경로 역할
CLAUDE.md 전역 지침 파일
settings.json / settings.local.json 사용자 설정
commands/ 커스텀 슬래시 커맨드
plugins/ 설치된 플러그인
hooks/ 이벤트 훅 설정
projects/*/memory/ 프로젝트별 영구 메모리
history.jsonl 커맨드 히스토리 (3.2MB이지만 유지 권장)
SOUL.md AI 정체성 설정
statsig/ 기능 플래그

8. 베스트 프랙티스

정리 주기

  • 주 1회: 활발히 사용하는 경우
  • 월 1회: 간헐적 사용인 경우
  • 디스크 경고 시: du -sh ~/.claude/ 확인부터

정리 전 체크리스트

  • [ ] 현재 진행 중인 세션이 없는지 확인
  • [ ] dry-run으로 삭제 대상 미리 확인
  • [ ] memory/ 디렉토리가 삭제 대상에 없는지 확인
  • [ ] 최근 세션 중 resume할 것이 있는지 확인

예방 방법

  1. cron 자동화: 위의 crontab 설정으로 주기적 정리
  2. 디스크 사용량 모니터링: du -sh ~/.claude/ 을 가끔 실행
  3. 프로젝트 정리: 더 이상 사용하지 않는 프로젝트의 세션은 적극적으로 정리

9. FAQ

Q: memory/MEMORY.md가 실수로 삭제되면 어떻게 되나요?
A: Claude Code가 새 세션에서 해당 프로젝트의 컨텍스트를 잃게 됩니다. 이전 세션에서 학습한 API 동작, 디버깅 팁 등이 사라집니다. 스크립트는 UUID 패턴 검증과 이름 체크 이중 안전장치로 memory/를 보호합니다.

Q: 삭제하면 이전 세션을 --resume으로 이어갈 수 없나요?
A: 맞습니다. 삭제된 세션의 UUID로 claude --resume을 실행하면 해당 세션을 찾을 수 없습니다. 7일 이상 지난 세션을 이어갈 일이 드물기 때문에 기본값을 7일로 설정했습니다. 필요하다면 1430으로 늘릴 수 있습니다.

Q: history.jsonl은 왜 정리하지 않나요?
A: 커맨드 히스토리 파일로, 터미널의 .bash_history와 비슷한 역할입니다. 3.2MB 정도로 크지 않고, 이전에 실행한 명령을 다시 찾을 때 유용하기 때문에 유지하는 것을 권장합니다.

Q: plugins/ 디렉토리는 왜 건드리지 않나요?
A: 사용자가 직접 설치한 플러그인이 들어 있습니다. 삭제하면 플러그인을 다시 설치해야 합니다. 31MB 정도로 크지만, 정리 대상이 아닙니다.

Q: macOS와 Linux에서 모두 동작하나요?
A: 현재 스크립트는 macOS의 stat -f%z를 사용합니다. Linux에서는 stat --format=%s로 변경해야 합니다. 크로스 플랫폼이 필요하면 wc -c < "$file"로 대체할 수 있습니다.

10. 참고 자료

  • 검색 키워드: Claude Code CLI documentation
  • 검색 키워드: claude code resume session