터미널을 닫으면 모든 맥락이 사라지는 Claude Code에게, 쉘 훅과 슬래시 커맨드로 기억 시스템을 붙인 과정
Claude Code는 기억을 못 한다
Claude Code로 작업하다 보 면 한 가지 불편함을 마주하게 된다. 터미널을 닫는 순간, 그 세션에서 나눈 모든 대화와 맥락이 사라진다는 것이다.
같은 프로젝트 폴더에서 터미널 두 개를 열어 작업하면, 각각의 Claude Code 인스턴스는 서로의 존재조차 모른다. CLAUDE.md에 프로젝트 규칙을 적어둔다 해도, 그건 규칙이지 "어제 내가 뭘 했는지"에 대한 기억이 아니다.
처음엔 수동으로 기억을 남기려 했다
Claude Code에게 기억을 시키기 위해 처음에는 커스텀 커맨드를 만들었다. ~/.claude/ 하위에 MEMORY.md 파일을 두면 다음 세션에서 자동으로 읽어온다. 그래서 세션을 끝내기 전에 "지금까지 한 거 MEMORY.md에 정리해"라고 말하면, 다음에 이어서 작업할 수 있다.
문제는 이걸 매번 기억하고 말해야 한다는 것이다. 까먹으면 그냥 날아간다. 그리고 터미널 여러 개를 동시에 쓸 때는 이 방법으로는 한계가 있다.
1단계: .zshrc의 preexec 훅으로 명령어 로깅
해결의 실마리는 의외로 Claude Code 바깥에 있었다. .zshrc는 zsh 셸이 시작될 때마다 실행되는 설정 파일인데, 여기에 훅 함수를 등록할 수 있다.
그중 preexec는 사용자가 명령어를 입력하고 Enter를 누른 직후, 명령이 실행되기 직전에 호출되는 함수다. 이걸 이용하면 터미널에서 실행하는 모든 명령어를 가로채서 로그로 남길 수 있다.
터미널에서 명령어 입력 → preexec 실행 → 로그 파일에 기록 → 원래 명령어 실행핵심 아이디어는 이랬다. Claude Code가 스스로 기억하지 못하더라도, 터미널이 명령어 이력을 파일로 남겨두면나중에 Claude Code가 그 파일을 읽어서 맥락을 복구할 수 있다. RAG(Retrieval-Augmented Generation)와 비슷한 발상이다. 기억을 내부에 저장하는 게 아니라, 외부 파일에서 필요할 때 꺼내 읽는 것이다.
.zshrc에 추가한 코드는 이게 전부다.
preexec() {
local project_root
project_root=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ -n "$project_root" && -d "$project_root/.claude" ]]; then
local project_name=$(basename "$project_root")
local local_log="$project_root/.logs"
local global_log="$HOME/.claude/logs/$project_name"
mkdir -p "$local_log" "$global_log"
local entry="[$(date '+%H:%M:%S')] $1"
echo "$entry" >> "$local_log/$(date '+%Y-%m-%d').log"
echo "$entry" >> "$global_log/$(date '+%Y-%m-%d').log"
fi
}
동작 방식을 정리하면 이렇다.
git rev-parse --show-toplevel로 현재 디렉토리가 속한 git 프로젝트의 루트를 찾는다
그 루트에 .claude/ 폴더가 있는지 확인한다 — Claude Code를 사용하는 프로젝트라는 의미다
조건이 맞으면 타임스탬프와 함께 명령어를 로그 파일에 기록한다
로그는 두 곳에 저장된다.
프로젝트 내부 (프로젝트/.logs/2026-02-27.log) — 해당 프로젝트 작업 이력을 바로 확인할 때
중앙 저장소 (~/.claude/logs/프로젝트명/2026-02-27.log) — 여러 프로젝트의 작업 이력을 한 곳에서 볼 때
.logs/ 폴더가 git에 올라가지 않도록 글로벌 gitignore도 설정했다.
git config --global core.excludesfile ~/.config/git/ignore
# ~/.config/git/ignore 내용: .logs/근데 내가 기대한 바람과 다르게 실제 로그는 이렇게 나왔다.
[09:15:23] git status
[09:15:45] bun dev
[09:20:11] claude
[09:35:02] git add -A && git commit -m "feat: add login page"
[10:12:33] bun test
명령어만으론 부족했다
기록되는 건 git status, bun dev 같은 터미널 명령어뿐이었다. Claude Code와 나눈 대화 — "이 구조로 갈까?", "아니 그건 오버엔지니어링이야", "그러면 이렇게 바꿔" 같은 맥락은 전혀 남지 않았다.
명령어 로그만으로는 뭘 했는지는 알 수 있어도 왜 했는지는 알 수 없었다.
transcript의 발견과 좌절
대화 내용을 기록할 방법을 찾다가, Claude Code가 이미 모든 세션의 대화를 저장하고 있다는 걸 발견했다.
~/.claude/transcripts/
├── ses_3f70b5850ffe.jsonl
├── ses_3f2c355eaffe.jsonl
├── ses_3f708da61ffe.jsonl
└── ...세션마다 .jsonl 파일이 생기고, 안에는 사용자 입력과 도구 호출 기록이 담겨 있었다. "이미 있었잖아!" 싶었는데, 실제로 열어보니 두 가지 문제가 있었다.
첫째, 프로젝트별로 분류되어 있지 않다. 세션 ID 기반의 파일명만 있어서, 어떤 프로젝트에서의 대화인지 바로 알 수 없다.
둘째, 더 치명적인 문제가 있었다. transcript에는 user 타입(사용자 입력)과 tool_use/tool_result 타입(도구 호출)만 기록되어 있고, Claude의 텍스트 응답은 저장되지 않았다. 대화의 절반이 빠져 있는 셈이다.
# transcript 내 메시지 타입 분포
tool_use: 87
tool_result: 85
user: 6
assistant: 0 # Claude 응답이 없다
자동 추출 스크립트를 만들어서 해결하려 했지만, 애초에 데이터가 없으니 추출할 수도 없었다.
결국 Claude가 직접 쓰게 했다
결국 transcript에서 추출하는 대신, Claude Code 자신이 현재 세션의 대화를 회고해서 직접 기록하게 만들었다. 다시 돌고 돌아 Claude Code의 슬래시 커맨드 기능을 활용해서 /memory라는 커맨드를 만들었다.
현재 세션의 대화 내용을 프로젝트에 저장하는 커맨드.
## 작성 규칙
- 사용자 메시지는 원문 그대로 기록한다. 요약하지 않는다.
- Claude 응답은 사용자에게 보여진 텍스트만 기록한다.
tool call, 시스템 메시지, 내부 사고 과정은 제외한다.
- 코드 블록이 포함된 응답은 코드 블록도 함께 기록한다.
- 대화 순서를 빠짐없이 유지한다. 건너뛰지 않는다.
세션이 끝날 때 /memory를 입력하면, Claude가 그 세션에서 나눈 모든 대화를 마크다운으로 정리해서 프로젝트의 .logs/transcripts/ 폴더에 저장한다.
프로젝트/.logs/transcripts/
├── 2026-02-27_09-30.md ← 오늘 오전 세션
├── 2026-02-27_14-00.md ← 오늘 오후 세션
└── 2026-02-28_10-15.md ← 내일 세션
결과물은 이런 형태다.
# 대화 기록 — 2026-02-27 09:30
## 나
.zshrc가 무슨 역할이길래 클로드 초기 관련 세팅이 가능한 거야?
## Claude
Claude 전용 설정 파일이 아니야. zsh 셸이 시작될 때마다
자동으로 실행되는 스크립트야...
## 나
이거는 그럼 이 프로젝트에서만 작동하는 거야?
## Claude
.zshrc에 설정했으니까 모든 프로젝트에서 작동해...
내가 너무 깊이 생각을 하지 않고 작업한 걸 수도 있다. 분명히 다른 사람들도 각각의 터미널에서 병렬로 작업을 할 때에도 맥락이 이어지길 했으면 하는 생각은 한 번쯤은 했을 것이다. 레퍼런스를 좀 더 찾아보면 사실 이미 방법이 나와있을지도 모르겠다. 생각난 김에 오늘 방법을 다시 한 번 찾아봐야겠다.