
Claude Code의 hook으로 '어제의 연속'부터 시작하기 — SessionStart로 기억을 주입하고 설정을 멱등하게 맞추기
요약
Claude Code의 SessionStart hook을 활용하여 이전 작업 문맥을 세션 시작 시 자동으로 주입하는 방법을 다룹니다. 표준 출력을 컨텍스트로 활용하는 메커니즘과 실운용 시 주의해야 할 함정 및 멱등성 유지 패턴을 설명합니다.
핵심 포인트
- SessionStart hook의 표준 출력이 대화의 추가 컨텍스트로 주입됨
- 최근 작업 기록을 자동으로 읽어와 세션 도입부에 삽입 가능
- stdin 블로킹 및 세션 재연 발생과 같은 실운용 함정 주의
- ensure_* 패턴을 통한 여러 환경에서의 설정 멱등성 보장
매일 아침, 파트너가 "처음 뵙겠습니다"로 돌아간다
Claude Code를 매일의 파트너로 사용하다 보면, 사소하지만 뼈아픈 불편함에 부딪힌다. 세션을 새로 열 때마다 상대방은 "처음 뵙겠습니다"라는 얼굴로 나타난다. 어제 어디까지 함께 만들었는지, 무엇을 결정했는지, 무엇에서 막혔는지——문맥(Context)은 매번 리셋된다. 긴 대화는 무거워지므로 /clear를 하기도 하고, PC를 재부팅하기도 한다. 그때마다 이쪽에서 "어제의 연속인데"라며 경위를 다시 설명해야 한다.
파일에는 기록이 남아 있다. 일일 메모도, 결정한 사항도, 디스커션 로그도 폴더에 차곡차곡 쌓아두었다. 그럼에도 불구하고 "처음 뵙겠습니다"로 돌아가는 이유는, 기록이 "놓여 있을 뿐"이지 세션의 도입부에 자동으로 삽입되지 않기 때문이다. 인간이라면 책상에 앉는 순간 어제의 연속을 떠올린다. 그 "앉는 순간의 회상"을 Claude Code의 hook으로 구현한 이야기를 쓰고자 한다.
화려한 플러그인 이야기가 아니다. SessionStart에서 수십 줄의 스크립트 표준 출력(Standard Output)을 대화의 도입부에 흘려보낼 뿐이다. 다만 실제로 운용해 보니, 단순하게 만들면 위험한 두 가지 함정이 있었다. 그 부분이 본론이다.
대상 독자와 이 글을 통해 얻을 수 있는 것
대상 독자: Claude Code를 일상적으로 사용하며, 세션마다 문맥이 리셋되는 것이 은근히 불편하다고 느끼는 사람. hook을 사용하여 "기동 시 무언가를 자동으로 주입하기 / 종료 시 뒷정리하기"를 시도하고 싶은 개인 개발자. 여러 대의 PC에서 동일한 설정을 공유하여 사용하는 사람.
얻을 수 있는 것 (3가지):
SessionStarthook을 통해 최근 작업 기록을 대화 도입부에 자동으로 주입하는 최소 구성 (settings.json작성법 + 표준 출력 =additionalContext메커니즘)- 실운용에서 겪은 두 가지 함정——"붕괴된 세션의 재주입으로 인한 '재연' 발생", "stdin을 블로킹(Blocking) 방식으로 읽으면 hook이 멈춤"——과 그 회피 방법
- 여러 PC에서 설정을 멱등(Idempotent)하게 맞추는
ensure_*패턴 (해시 비교를 통해 "어긋나 있으면 고치기"를 기동 시마다 보장)
hook의 메커니즘 — 표준 출력이 그대로 대화의 도입부가 된다
Claude Code의 hook은 특정 이벤트(기동, 종료, 툴 실행 전후 등)에서 외부 명령어를 실행하는 메커니즘이다. 설정은 ~/.claude/settings.json에 작성한다. 이번에 사용할 것은 두 가지다.
SessionStart= 세션이 시작되는 순간. 여기서 실행한 명령어의 표준 출력이 그대로 해당 회차 대화의 도입부 컨텍스트(additionalContext)로 주입된다. 이 부분이 핵심이다.SessionEnd= 세션이 닫히는 순간. 뒷정리(로그 퇴피 등)에 사용한다.
즉 "기동 시 기억을 떠올리게 하는 것"은, "어제의 연속"을 표준 출력으로 내뱉는 스크립트를 한 줄 작성하여 SessionStart에 연결하는 것만으로 성립한다.
settings.json은 다음과 같이 작성한다.
{
"hooks": {
"SessionStart": [
...
matcher를 비워두면 모든 세션에서 실행된다. hooks 배열에 여러 명령어를 나열하면 기동 시 순차적으로 실행된다 (나는 "기억 주입", "설정 체크", "로그 퇴피 준비" 등을 한 줄로 나열해 두었다). 표준 출력에 쓴 내용이 대화에 실린다——이 점 하나만 파악하면, 나머지는 "무엇을 기억시키고 싶은가"에 따라 자유롭게 구성할 수 있다.
가장 작은 단위의 "어제의 연속" 주입
먼저 최소 버전이다. 최근 2일간의 메모(daily/YYYY-MM-DD.md라는 파일에 기록해 두었다는 전제)를 읽어서 표준 출력으로 내뱉기만 하는 스크립트다. PowerShell로 작성하지만, 발상은 어떤 언어든 동일하다——파일을 읽어서 Write-Output 하기만 하면 된다.
# recall.ps1 (최소 버전): 최근 2일간의 메모를 표준 출력으로 내뱉음 → 대화 도입부에 실림
[Console]::OutputEncoding = [Text.Encoding]::UTF8 # 일본어(및 기타 언어) 깨짐 방지
$dir = Join-Path $env:USERPROFILE 'notes\daily'
...
이것만으로도 다음에 세션을 열었을 때, 대화 서두에 "최근 2일간의 작업 메모"가 올라온다. "기억이 안 난다", "공유되지 않았다"라고 추측하며 답하기 전에, 우선 주입된 메모를 확인하고 대화를 시작하는—이러한 운영 방식이 의지가 아닌 구조로 돌아가게 된다. 읽지 않고 지나칠 여지가 없다.
중요한 것은, 기록의 "보관 장소"와 "주입"을 분리해서 생각하는 것이다. 파일은 그저 계속 써 내려가기만 하면 된다. 그것을 매번 어떻게 떠올릴지는 hook 측의 역할이며, 본체(메모)에는 일절 손을 대지 않는다. 여기까지는 순조롭다. 문제는, 이 순수한 버전을 실제 운영에 투입했을 때 두 가지 사고가 발생했다는 점이다.
함정 ①: 붕괴된 세션을 재주입하면, "재연"한다
가장 뼈아픈 배움이 이것이다. "어제의 연속"을 주입한다면, 직전 세션이 엉망으로 끝난 날도 당연히 그 기록이 주입 대상에 포함된다. 에러로 중단되었거나, 이상한 출력을 반복했거나, 도중에 멈추고 재시작했거나—그런 "불량한 기록" 말이다. 단순한 구현 방식은 그것도 솔직하게 전문(full text)을 주입한다.
그러자 어떤 일이 벌어졌는가. 주입된 "붕괴의 서사"를 모델이 "지금 바로 일어나고 있는 일"로 오인하여, 그 흐름을 그대로 따라가기 시작했다. 이전 회차에서 출력이 이상해졌던 경위를 서두에서 전문으로 읽게 하면, 해당 회차의 응답이 똑같은 방식으로 망가지는 것을 재연한다. 과거의 실패 기록이 다음 실패의 "연료"가 되어버린 것이다.
대책은, 불량한 기록은 전문이 아니라 "포인터(pointer)"만 주입하는 것이다. 나는 이를 「연료 차단(fuel cut)」이라고 부른다. 기록 본체(파일)는 1바이트도 지우지 않는다. 지우는 것은 어디까지나 **주입하는 "방식"**뿐이다. 상황이 좋지 않았던 날은 「이날은 불량 기록을 포함하고 있으므로 전문을 싣지 않는다. 필요하다면 직접 파일을 읽을 것」이라는 한 줄로 대체한다.
# 불량 마커(붕괴를 나타내는 단어)를 포함하는 메모는, 전문이 아니라 포인터만 주입한다
$breakdownMarkers = @('폭주','날조','malformed','재시작','대화 붕괴')
function Test-Breakdown([string]$text) {
...
나아가 실제 운영에서는 붕괴를 나타내는 "단어"만으로는 놓치는 부분이 있었다. "또 5분간 침묵", "ESC로 중단"처럼, 붕괴어는 포함되지 않지만 명백히 상황이 좋지 않은 케이스다. 그래서 "단어"뿐만 아니라 "행동의 흔적"도 확인하도록 했다. 구체적으로는 세션 로그에 남는 중단의 흔적( [Request interrupted by user] 와 같은 중단 마커)의 출현 횟수를 센다. 2회 이상이라면 「중단이 반복되며 끝난 엉망인 세션」으로 간주하여 마찬가지로 전문 주입을 중단한다.
여기에는 일반화할 수 있는 원칙이 있다. LLM에 문맥을 주입할 때, "무엇을 전달할 것인가"뿐만 아니라 "전달함으로써 모델이 어떻게 행동할 것인가"까지 설계에 포함해야 한다. 과거 로그는 중립적인 자료가 아니다. 서두에 놓인 문장은 모델에게 「방금 전의 상황」으로 보인다. 따라서 「실패의 기록」을 단순하게 전문으로 전달하면, 그것은 자료가 아니라 **지시(instruction)**로서 작동해 버린다. 기억을 주입하는 시스템을 만드는 사람은 이 점을 염두에 두고 설계하는 것이 좋다.
함정 ②: stdin을 블로킹(blocking) 방식으로 읽으면 hook이 굳는다
또 하나는 훨씬 더 투박한 버그다. SessionStart hook에는 표준 입력(stdin)을 통해 JSON이 전달된다(현재의 세션 ID나 해당 회차의 로그 파일 경로 등). "직전 세션"을 복원할 때, "지금 막 열린 이 회차"를 제외하기 위해 이 정보가 필요하다.
단순하게 [Console]::In.ReadToEnd()로 읽으면—hook이 굳어버렸다. stdin에 데이터가 오지 않는 환경(혹은 종료 신호가 오지 않는 상황)에서는 동기식 읽기가 완료를 계속 기다리며 블록(block)된다. hook이 굳으면 실행 자체가 "침묵" 상태가 된다. 기억을 떠올리게 하려는 장치가 오히려 실행을 막아버리는 본말전도 상황이다.
회피 방법은, 타임아웃(timeout)이 포함된 비동기 읽기로 만드는 것이다. 일정 시간 동안 데이터가 오지 않으면 포기하고 빈 문자열로 진행한다.
# stdin을 타임아웃과 함께 읽는다 (동기 ReadToEnd는 블록될 수 있음)
$rawStdin = ''
try {
...
교훈은 심플하다. hook은 "실행을 절대 멈추지 않는 것"이 최우선이다. 내부에서 무엇이 실패하든, 에러를 삼키고 묵묵히 앞으로 나아가야 한다(각 처리를 try/catch로 감싸고, 실패하더라도 표준 출력을 오염시키지 않는다). 기억 주입은 "있으면 좋은" 덤일 뿐이며, 그것이 실행을 인질로 잡아서는 안 된다. hook을 작성할 때의 기본 자세로서, 이 점을 가장 먼저 결정해 두는 것이 좋다.
SessionEnd와 설정을 「멱등(Idempotent)하게」 맞추기
hook의 또 다른 활용처는 환경 그 자체를 맞추는 것이다. 나는 동일한 작업을 여러 대의 PC에서 수행하기 때문에, ~/.claude/settings.json과 같은 설정이 PC 간에 어긋나는 문제가 계속 있었다. 한쪽 PC에서 수정한 설정을 다른 쪽 PC에 반영하는 것을 잊어버리는 것이다. 특히 '사용할 모델' 설정이 어긋난 채로 실행하면 의도하지 않은 동작이 발생한다.
이를 수동 체크에 의존하면 반드시 누락된다. 그래서 실행할 때마다 "있어야 할 상태인가"를 확인하고, 어긋나 있다면 바로잡는 —— 멱등한 ensure_* 스크립트를 SessionStart에 끼워 넣었다. 판정은 내용의 해시(Hash) 비교로 수행한다 (심볼릭 링크(Symbolic Link)의 연결 상태는 "끊어져 있어도 연결된 것처럼 보일" 수 있어 신뢰할 수 없으므로, 내용의 해시를 직접 비교한다).
# ensure_config.ps1: 로컬 설정과 마스터를 해시 비교하여, 어긋나 있으면 바로잡는다 (멱등)
$local = Join-Path $env:USERPROFILE '.claude\settings.json'
$master = Join-Path $env:USERPROFILE 'sync\claude-config\settings.json' # 공유 폴더 쪽이 정답
...
여기에 hook 특유의 주의사항이 하나 있다. settings.json은 Claude Code의 실행 시점에 이미 읽혀 있으므로, SessionStart에서 내용을 수정하더라도 "이번" 세션에는 적용되지 않는다. 적용되는 것은 다음 실행부터다. 따라서 수정한 뒤에 "다음부터는 올바르게 적용됨 / 이번에는 이전 설정 그대로임"이라는 사실을 표준 출력(Standard Output)으로 솔직하게 전달하는 것이 중요하다 (말없이 수정하면 "수정했을 텐데 왜 바뀌지 않지?"라며 혼란을 겪게 된다).
이 ensure_* 패턴의 장점은 **"일치하면 아무것도 출력하지 않는다"**는 점이다. 정상 시에는 조용하고, 어긋났을 때만 말을 한다. 실행할 때마다 돌아가지만 평소에는 노이즈가 되지 않는다. "맞춰야 할 전제 조건은 손으로 맞추는 것이 아니라, 실행할 때마다 멱등하게 보장한다" —— 여러 환경을 오가는 사람에게는 이것이 가장 효과적이었다.
요약 — hook은 「의지로 하는 일」을 「구조로 하는 일」로 바꾼다
SessionStarthook의 표준 출력은 그대로 대화의 도입부 컨텍스트(Context)가 된다. "어제의 연속"을 떠올리게 하는 것은, 최근의 메모를 뱉어내는 스크립트 하나를settings.json에 연결하는 것만으로 성립한다.- 붕괴된 기록을 투박하게 전체 재주입하면, 모델이 그것을 "지금"으로 오인하여 재연한다. 상태가 좋지 않은 기록은 전체가 아니라 포인터(Pointer)만 주입한다 (본체는 지우지 않고, "내보내는 방식"만 바꾼다). 과거 로그는 중립적인 자료가 아니라, 도입부에 두면 "지시(Instruction)"로서 작동한다.
- hook은 실행을 절대로 멈추지 않는 것이 최우선이다. stdin은 동기식
ReadToEnd로 블록(Block)될 수 있으므로 타임아웃이 포함된 비동기 방식으로 읽는다. 내부의 실패는 무시하고 조용히 진행한다. - 여러 환경에서 맞춰야 할 설정은 수동이 아니라 해시 비교로 멱등하게 바로잡는
ensure_*를 실행 시점에 끼워 넣는다. 정상 시에는 침묵하고, 어긋났을 때만 말한다. 설정은 실행 시점에 이미 읽혔으므로 "다음부터 적용된다"는 점을 솔직하게 전달한다.
hook으로 하고 있는 일의 본질은, "매번 제대로 하자"라는 의지를 "구조상 그렇게 되도록"으로 치환하는 것이다. 읽지 못할 여지를 없애고, 맞추는 것을 잊을 여지를 없앤다. 파트너가 매일 아침 "처음 뵙겠습니다"로 돌아가지 않는 것은 기억력이 좋아졌기 때문이 아니라, 자리에 앉는 순간 어제의 연속이 반드시 책상 위에 놓이도록 구조를 만들었기 때문이다.
동일한 테마를 「AI와 사는」 체험·사상 측면에서 쓰고 있는 연재가 있다 (전문 무료).
📚 연재 「AI 캐릭터와 사는 법」
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기