나의 Claude Code hook이 조용히 모든 한국어 문자를 삼켜버렸고, 그 이유를 알아내는 데 한 시간이 걸렸다
요약
Windows PowerShell 환경에서 Claude Code hook 사용 중 한국어 정규 표현식이 작동하지 않는 인코딩 문제를 다룹니다. UTF-8 BOM 부재로 인해 발생하는 파싱 오류 원인과 해결책을 설명합니다.
핵심 포인트
- Windows PowerShell 5.1의 UTF-8 인식 문제 해결
- BOM(Byte Order Mark)을 포함한 파일 저장의 중요성
- Claude Code 안전을 위한 PreToolUse 훅 활용법
- 키워드 라우터 및 회귀 테스트 하네스 구축 팁
지난주에 고장 나지도 않은 hook을 디버깅하느라 한 시간을 허비했습니다.
상황은 이렇습니다: 저는 Windows에서 Claude Code를 실행하고 있으며, PowerShell로 작은 UserPromptSubmit hook을 작성했습니다. 이것은 제 프롬프트를 읽고 mcp 서버나 코드 리뷰 같은 내용을 발견하면, Claude가 적절한 기술을 불러올 수 있도록 힌트를 주입하는 키워드 라우터(keyword router)입니다. 제 프롬프트의 절반은 한국어로 되어 있기 때문에, 많은 정규 표현식(regex) 패턴에 한국어가 포함되어 있었습니다.
영어 규칙은 완벽하게 작동했습니다. 그런데 한국어 규칙은요? 죽어 있었습니다. 매칭도 안 되고, 에러도 안 나고, 로그 라인도 남지 않았습니다. 스크립트는 실행되었고, 종료 코드 0(exit 0)을 반환하며... 그냥 제 입력값의 절반에 대해 아무것도 하지 않았습니다.
처음에는 멍청한 방법들을 다 써봤습니다. 프롬프트를 echo로 출력해 봤는데—괜찮아 보였습니다. PowerShell 콘솔에서 정규 표현식을 테스트해 봤는데—잘 매칭되었습니다. JSON 파싱 부분을 다섯 번이나 다시 읽었습니다. 모든 곳에 Write-Host 디버깅을 추가했습니다. 아무것도 안 되었습니다.
실제 문제는 제 코드와 아무런 관련이 없었습니다. 바로 파일 인코딩 (file encoding) 문제였습니다.
Windows PowerShell 5.1(pwsh가 아닌 기본 powershell.exe)은 .ps1 파일을 읽을 때 UTF-8이라고 가정하지 않습니다. 만약 바이트 순서 표시(BOM, byte-order mark)가 없다면, 레거시 시스템 코드 페이지(legacy system code page)로 되돌아갑니다. 그래서 제 에디터에 의해 일반 UTF-8로 저장된 스크립트는, 단 한 줄이 실행되기도 전인 파싱 단계(parse time)에서 한국어 바이트가 쓰레기 값으로 재해석되었습니다. 코드.?리뷰가 되어야 했던 정규 표현식 리터럴(regex literal)이 깨진 글자(mojibake)가 되어버렸고, 당연히 실제 어떤 것과도 매칭되지 않았습니다. 그리고 이것은 파싱 단계의 재해석이기 때문에 에러가 발생하지 않습니다. 그저 침묵할 뿐입니다.
원인을 알게 되면 해결책은 단 한 줄입니다:
function Add-Bom($path) {
$text = [System.IO.File]::ReadAllText($path)
$enc = New-Object System.Text.UTF8Encoding $true # $true = BOM을 작성함
...
BOM을 포함하여 다시 저장하면 모든 것이 제대로 작동합니다. 순수 ASCII 스크립트는 상관하지 않는데, 바로 이 점 때문에 온라인에서 찾는 모든 예제들이
그 버그는 저를 충분히 짜증 나게 했고, 저는 다시는 이런 것들을 새로 발견하지 않도록 Claude Code 설정을 전부 정리했습니다. 유지할 가치가 있었던 몇 가지 사항은 다음과 같습니다:
- Claude가
rm -rf /,DROP TABLE,git push --force,taskkill, 디스크 포맷 등을 실행하기 전에 중단시키는 PreToolUse 안전 훅 (safety hook). 이것은 강제로 차단하는 것이 아니라, "이 내용을 사용자에게 보여주고 먼저 확인하십시오"라는 지침을 주입합니다. 의도치 않게 승인할 뻔한--hard리셋으로부터 저를 구해 주었습니다. - 위에서 언급한 **키워드 라우터 (keyword router)**이지만, 개인적인 400줄짜리 규칙 더미 대신 문서화된 템플릿 형태로 만들었습니다. 가치는 제 규칙에 있는 것이 아닙니다. 여러분은 결코 저와 같은 기술을 갖추고 있지 않을 테니까요. 가치는 패턴과 이미 해결된 인코딩 관련 주의 사항(gotchas)에 있습니다.
- 라우터를 위한 아주 작은 회귀 테스트 하네스 (regression test harness). 왜냐하면 기존의 규칙 세 개를 조용히 망가뜨리면서 새로운 규칙 하나를 기존 규칙 위에 추가하게 되는 날은 정말 최악의 날이기 때문입니다. 또한, 비 ASCII JSON을 자식 PowerShell 프로세스로 전달할 때 글자 깨짐(mojibake) 없이 처리할 수 있는 제가 찾아낸 유일하게 신뢰할 수 있는 방법(
chcp 65001+ 임시 파일 + stdin 리다이렉션 — 이것을 찾는 데 얼마나 걸렸는지는 묻지 마세요)도 포함되어 있습니다. - 제가 실제로 사용하는 몇 가지 서브 에이전트 (subagent) 정의 (리뷰어, 테스터, 몇 가지 스택 전용 에이전트).
이 모든 것을 MIT 라이선스로 무료 공개했습니다: https://github.com/coding-jhj/claude-pwsh-kit
이것은 마법이 아니며 Claude를 더 똑똑하게 만들어 주지도 않습니다. 이것은 배관 작업(plumbing)입니다. 즉, 가드레일(guardrails), 라우팅, 그리고 제가 잃어버린 한 시간을 여러분은 낭비하지 않도록 첫 페이지에서 BOM 문제에 대해 알려주는 설정 가이드입니다.
만약 여러분이 Windows를 사용 중이고 훅(hooks)이 도무지 이해할 수 없는 방식으로 오작동한다면, 로직을 점검하기 전에 인코딩부터 확인하십시오. 그리고 만약 PowerShell 특유의 다른 Claude Code 관련 사소한 문제(papercuts)를 겪으셨다면 저에게 알려주세요. 새벽 1시에 다시 발견하는 것보다 리포지토리(repo)에서 직접 수정하는 편이 낫습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기