지금까지의 훅 예제는 전부 "$CLAUDE_PROJECT_DIR"/.claude/hooks/... 경로를 썼습니다. 이 변수는 어디서 오고, 그 밖에 무엇이 훅에 넘어올까요. 이 글은 훅이 물려받는 환경을 정리하고 CLAUDE_PROJECT_DIR과 CLAUDE_ENV_FILE을 실습으로 확인합니다. 우리 팀이 두 대의 맥에서 같은 설정 파일을 쓸 수 있는 것도 절대 경로 대신 이 변수로 적었기 때문입니다. 다음 글은 매처입니다.
핵심 요약
- 훅은 Claude Code의 환경을 그대로 물려받고, 거기에
CLAUDE_PROJECT_DIR이 항상 추가됩니다. CLAUDE_ENV_FILE은SessionStart,Setup,CwdChanged,FileChanged네 이벤트에서만 주어지는 파일 경로입니다. 여기 쓴export줄이 세션 내내 Bash 도구에 적용됩니다.- 플러그인 훅에는
CLAUDE_PLUGIN_ROOT·CLAUDE_PLUGIN_DATA, 웹 환경에는CLAUDE_CODE_REMOTE, 도구 컨텍스트에는CLAUDE_EFFORT가 추가됩니다. $CLAUDE_MODEL같은 변수는 없습니다. 모델은SessionStart입력 JSON의model필드로만 알 수 있습니다.
실습용 디렉터리
앞 글의 ~/hook-lab을 씁니다.
mkdir -p ~/hook-lab/.claude/hooks && cd ~/hook-lab && git init -q
여기서 claude를 띄우면 이 디렉터리가 프로젝트 루트입니다(Windows Git Bash도 동일).
Hooks에서 쓸 수 있는 환경 변수는 무엇인가
훅은 Claude Code가 실행된 셸 환경을 상속합니다(OTEL_* 텔레메트리 변수만 제거됩니다). 그 위에 추가되는 변수는 다음과 같습니다.
| 변수 | 값 | 어디서 |
|---|---|---|
CLAUDE_PROJECT_DIR |
프로젝트 루트 절대 경로 | 모든 훅. stdio MCP 서버와 플러그인 LSP 서버 환경에도 설정 |
CLAUDE_ENV_FILE |
export 줄을 추가할 파일 경로 |
SessionStart, Setup, CwdChanged, FileChanged만 |
CLAUDE_PLUGIN_ROOT |
플러그인 설치 디렉터리(업데이트마다 바뀜) | 플러그인 훅 |
CLAUDE_PLUGIN_DATA |
플러그인 영속 데이터 디렉터리(업데이트 후에도 유지) | 플러그인 훅 |
CLAUDE_CODE_REMOTE |
웹 환경이면 "true", 로컬 CLI에서는 미설정 |
모든 훅 |
CLAUDE_CODE_BRIDGE_SESSION_ID |
Remote Control 세션 ID(v2.1.199 이상) | 원격 제어 연결 중 |
CLAUDE_EFFORT |
low·medium·high·xhigh·max |
PreToolUse, PostToolUse, Stop, SubagentStop 같은 도구 컨텍스트 |
없는 것도 알아 두세요. $CLAUDE_MODEL은 없습니다. 셸의 ANTHROPIC_MODEL은 상속되지만 /model로 바꾼 값은 반영되지 않고, 현재 모델은 SessionStart 입력 JSON의 model 필드로만 알 수 있습니다. 세션 ID와 작업 디렉터리도 stdin JSON의 session_id, cwd로 옵니다.
먼저 간단한 예로 확인하기
어떤 변수가 실제로 오는지 파일로 떨궈 봅니다.
cat > .claude/hooks/dump-env.sh <<'EOF'
#!/bin/bash
INPUT=$(cat)
{
echo "=== $(date '+%T') $(echo "$INPUT" | jq -r '.hook_event_name')"
env | grep '^CLAUDE_' | sort
echo "cwd(json)=$(echo "$INPUT" | jq -r '.cwd')"
} >> "$HOME/hook-env.log"
exit 0
EOF
chmod +x .claude/hooks/dump-env.sh
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dump-env.sh" } ] }
],
"PostToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dump-env.sh" } ] }
]
}
}
새 세션에서 "pwd 실행해 줘"라고 한 뒤 cat ~/hook-env.log를 보면 SessionStart 블록에는 CLAUDE_ENV_FILE이 있고 PostToolUse 블록에는 없습니다. 이 차이를 직접 보는 것이 목적입니다.
CLAUDE_PROJECT_DIR: 프로젝트 루트는 어떻게 얻나
CLAUDE_PROJECT_DIR은 Claude Code를 실행한 프로젝트의 루트입니다. 입력 JSON의 cwd는 "훅이 호출된 시점의 작업 디렉터리"라 Claude가 cd를 한 뒤에는 달라질 수 있습니다. 파일을 찾을 때는 CLAUDE_PROJECT_DIR, 실행 위치를 볼 때는 cwd입니다.
설정 파일에서 쓸 때는 두 가지 형식이 있습니다.
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check.sh" }
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check.sh", "args": [] }
첫 번째는 셸 형식이라 공백 경로를 위해 큰따옴표로 감쌉니다. 두 번째는 args가 있어 exec 형식으로 실행되며 ${CLAUDE_PROJECT_DIR}이 문자열로 치환되므로 따옴표가 필요 없습니다. 경로 자리 표시자를 쓰는 훅은 exec 형식이 공식 권장입니다. 스크립트 안에서는 cd "$CLAUDE_PROJECT_DIR"로 시작해 상대 경로 사고를 없앱니다.
CLAUDE_ENV_FILE: 세션 중 환경 변수를 어떻게 영속화하나
훅은 매번 새 프로세스라 export가 남지 않습니다. SessionStart 등 네 이벤트에서 이 변수에 파일 경로가 담겨 오고, 그 파일에 적은 export 줄은 이후 Claude가 실행하는 모든 Bash 명령 앞에 프리앰블로 실행됩니다.
#!/bin/bash
# .claude/hooks/session-env.sh
[ -z "$CLAUDE_ENV_FILE" ] && exit 0
{
echo "export PROJECT_ROOT=\"$CLAUDE_PROJECT_DIR\""
echo 'export NODE_OPTIONS="--max-old-space-size=4096"'
echo 'export PATH="$PATH:./node_modules/.bin"'
} >> "$CLAUDE_ENV_FILE"
exit 0
>>로 추가하는 이유는 다른 훅이 먼저 써 둔 줄을 지우지 않기 위해서입니다. 새 세션에서 "echo $PROJECT_ROOT 실행해 줘"라고 시켜 값이 찍히면 영속화된 것입니다. nvm, mise처럼 변수를 한꺼번에 바꾸는 도구는 실행 전후 export -p를 comm -13으로 비교해 달라진 줄만 붙이는 패턴을 공식 문서가 제시하고, direnv는 direnv export bash > "$CLAUDE_ENV_FILE" 한 줄이면 됩니다. 같은 줄을 CwdChanged에 걸면 디렉터리를 옮길 때마다 다시 로드됩니다.
쓰임새는 도구 경로, 테스트 DB 주소, 로그 레벨처럼 "이 세션의 모든 명령에 공통인" 값입니다. API 키 원문은 적지 마세요. 평문으로 남고 stdout에 찍히면 컨텍스트에까지 들어갑니다. 키는 셸 환경이나 시크릿 관리자에서 상속받게 둡니다.
정리
- 훅은 부모 환경을 상속하고
CLAUDE_PROJECT_DIR이 항상 추가됩니다. cwd는 호출 시점 디렉터리,CLAUDE_PROJECT_DIR은 프로젝트 루트. 파일 참조는 후자로 합니다.CLAUDE_ENV_FILE은 네 이벤트에서만 오고,>>로 추가한export줄이 세션 내내 적용됩니다.- 모델명 환경 변수는 없습니다.
SessionStart입력의model을 씁니다.
자주 묻는 질문
CLAUDE_PROJECT_DIR은 어떤 값이 들어오나요?
Claude Code를 실행한 프로젝트의 루트 절대 경로입니다. 모든 훅과 stdio MCP 서버에 설정됩니다. Claude가 cd로 이동해도 바뀌지 않으므로 이동 후 위치는 입력 JSON의 cwd를 보세요.
CLAUDE_ENV_FILE은 모든 훅에서 쓸 수 있나요?
아닙니다. SessionStart, Setup, CwdChanged, FileChanged 네 이벤트에서만 옵니다. 다른 훅에서는 비어 있으므로 [ -n "$CLAUDE_ENV_FILE" ]로 검사한 뒤 씁니다.
환경 변수가 실제로 넘어오는지 어떻게 확인하나요?
훅 스크립트에서 env | grep '^CLAUDE_' >> ~/hook-env.log처럼 파일로 떨군 뒤 열어 봅니다. 이벤트별 차이를 보려면 hook_event_name도 함께 기록합니다. claude --debug-file의 로그로 확인해도 됩니다.
환경 변수를 영속화하면 어디에 쓸 수 있나요?
그 세션의 모든 Bash 명령에서 그 변수가 보입니다. 도구 경로(PATH), Node 메모리 옵션, 테스트 DB 주소, 로그 레벨, direnv·nvm 환경처럼 "매 명령 앞에 붙이던" 설정을 한 번에 끝냅니다.