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. 정리 스크립트 작성
설계 원칙
- 기본 dry-run: 실수로 실행해도 아무것도 삭제되지 않음
- memory/ 절대 보호: UUID 패턴 검증 + memory 이름 체크 이중 안전장치
- 날짜 기준 설정 가능: 기본 7일, 인자로 변경 가능
- 삭제 전 요약: 몇 개 파일, 얼마나 절약되는지 미리 표시
전체 스크립트
~/.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_files는 maxdepth 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/ 보호를 위한 이중 안전장치:
- 이름 체크:
dirname == "memory"이면 무조건 스킵 - 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할 것이 있는지 확인
예방 방법
- cron 자동화: 위의 crontab 설정으로 주기적 정리
- 디스크 사용량 모니터링:
du -sh ~/.claude/을 가끔 실행 - 프로젝트 정리: 더 이상 사용하지 않는 프로젝트의 세션은 적극적으로 정리
9. FAQ
Q: memory/MEMORY.md가 실수로 삭제되면 어떻게 되나요?
A: Claude Code가 새 세션에서 해당 프로젝트의 컨텍스트를 잃게 됩니다. 이전 세션에서 학습한 API 동작, 디버깅 팁 등이 사라집니다. 스크립트는 UUID 패턴 검증과 이름 체크 이중 안전장치로 memory/를 보호합니다.
Q: 삭제하면 이전 세션을 --resume으로 이어갈 수 없나요?
A: 맞습니다. 삭제된 세션의 UUID로 claude --resume을 실행하면 해당 세션을 찾을 수 없습니다. 7일 이상 지난 세션을 이어갈 일이 드물기 때문에 기본값을 7일로 설정했습니다. 필요하다면 14나 30으로 늘릴 수 있습니다.
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