Claude Code on the web 공식 이미지
이미지: claude.com

클라우드에서 띄우는 Claude Code 세션은 매번 깨끗한 컨테이너에 저장소를 새로 클론해서 시작합니다. 로컬 세션이라면 node_modules나 .env가 이미 디스크에 남아 있지만, 클라우드 세션은 그게 없는 상태에서 출발합니다. 그러다 보니 세션을 새로 열 때마다 똑같은 패턴이 반복됐습니다. "빌드 돌려줘"라고 시키면 에이전트가 먼저 npm run build를 실행했다가 Cannot find module 에러를 만나고, 그제서야 npm install을 돌리고, 그 다음에야 본래 작업을 시작하는 식이었습니다. 매번 같은 왕복이라 세션 시작 시점에 이 점검을 미리 끝내두기로 했습니다.

어떤 작업이었나

이 저장소는 Next.js 프로젝트라 세션을 열자마자 npm install이 끝나 있어야 npm run dev나 타입체크가 바로 돌아갑니다. 추가로 블로그 글 자동 발행 루틴이 git status가 깨끗한 상태에서 시작한다고 가정하고 있어서, 혹시 이전 세션이 커밋하지 않은 변경사항을 남겨두고 끝난 경우도 미리 알고 싶었습니다. 이 두 가지를 세션이 열리는 시점에 자동으로 확인해서, 에이전트가 첫 턴부터 그 결과를 알고 시작하게 만드는 게 목표였습니다.

AI를 어떻게 활용했나

Claude Code에게 "세션이 시작될 때 한 번 실행되는 훅이 있는지, 있다면 입력으로 뭘 받고 출력은 어떻게 돌려줘야 에이전트 컨텍스트에 반영되는지 최신 문서 기준으로 찾아줘"라고 물었습니다. SessionStart 훅이 그 용도였고, 문서를 보니 matcher로 startup(완전히 새 세션), resume(이어하기), clear(/clear 이후), compact(컨텍스트 압축 이후) 네 가지를 구분할 수 있었습니다. 처음엔 네 경우 전부에 걸까 했는데, compact는 같은 세션 안에서 수시로 일어나는 이벤트라 그때마다 npm install을 다시 돌리는 건 낭비였습니다. startup과 clear에만 걸기로 하고 .claude/settings.json에 등록했습니다.

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|clear",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/env-check.sh"
          }
        ]
      }
    ]
  }
}

훅 스크립트는 node_modules가 package-lock.json보다 오래됐는지, 커밋 안 된 변경사항이 남아 있는지 확인하고 그 결과를 하나의 문자열로 모읍니다.

#!/bin/bash
# .claude/hooks/env-check.sh
notes=()

if [[ -f package-lock.json ]]; then
  if [[ ! -d node_modules ]] || [[ package-lock.json -nt node_modules ]]; then
    npm install --silent
    notes+=("package-lock.json 기준으로 npm install을 실행했습니다.")
  fi
fi

if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
  notes+=("이전 세션이 남긴 커밋되지 않은 변경사항이 있습니다. git status로 먼저 확인하세요.")
fi

if [[ ${#notes[@]} -eq 0 ]]; then
  exit 0
fi

context=$(printf '%s\n' "${notes[@]}")
jq -n --arg ctx "$context" '{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": $ctx
  }
}'

처음에 걸렸던 부분

처음엔 notes가 비어 있어도 무조건 jq로 JSON을 출력하게 짰습니다. 그랬더니 점검할 게 아무것도 없는 정상적인 세션에서도 "이상 없음" 같은 문장이 매번 컨텍스트에 끼어들었습니다. 세션 시작 시점의 컨텍스트는 그 뒤로 이어지는 모든 턴에 계속 깔려 있는 거라, 의미 없는 문장 하나도 누적되면 쓸데없이 자리를 차지합니다. notes 배열이 비어 있으면 아예 exit 0으로 끝내고 출력을 하지 않도록 바꿨습니다. 알릴 게 있을 때만 말하는 편이 나았습니다.

또 하나, package-lock.json -nt node_modules 비교는 node_modules 디렉토리 자체의 수정 시각만 보기 때문에, 그 안의 특정 패키지만 지워졌다 다시 생긴 경우는 못 잡습니다. 완벽한 검사는 아니지만 "락파일이 바뀐 뒤 한 번도 install을 안 돌린 경우"라는 가장 흔한 상황만 잡아도 충분하다고 보고 넘어갔습니다.

결과와 얻은 팁

  • SessionStart는 PreToolUse/PostToolUse와 달리 도구 호출이 아니라 세션 생명주기에 걸리는 훅이라, matcher도 startup/resume/clear/compact처럼 이벤트 종류를 가리킵니다. 네 경우를 다 같은 훅으로 처리하면 compact처럼 자주 일어나는 이벤트에서 무거운 작업(npm install 등)이 반복될 수 있으니 필요한 경우만 골라 거는 게 맞습니다.
  • 출력은 additionalContext로 들어가 세션 전체에 계속 남아 있습니다. 매번 조건 없이 뭔가를 출력하면 별 의미 없는 문장도 컨텍스트에 쌓이므로, 알릴 내용이 있을 때만 출력하도록 조건을 걸어야 합니다.
  • 도입 후로는 세션을 새로 열자마자 npm install이 필요한지, 이전 세션이 커밋을 안 하고 끝났는지를 첫 턴 전에 이미 알고 시작하게 됐습니다. 매번 빌드가 실패한 다음에야 원인을 찾는 왕복이 줄었습니다.