Windows에서 "shell": "powershell"을 지정한 hook은, 호출된 node 등이 exit 2로 종료되어도 Claude Code 2.1.303에서는 동작을 멈추지 않았습니다. 이는 Windows PowerShell 5.1의 -Command가 종료 코드 2를 1로 변경하기 때문입니다. 2.1.309 버전에서는 이 방식은 멈췄지만, node 뒤에 Write-Output을 추가한 방식은 2.1.309에서도 무시되었습니다. command 끝에 ; exit $LASTEXITCODE를 추가하면 어떤 버전에서도 동작이 멈추었습니다.
위험한 작업을 막기 위해 PowerShell hook을 작성한 사람은, 실제로 멈추지 않았다는 사실을 인지하기 어려울 수 있습니다. 제 환경에서는 2.1.303에서 멈추지 않았을 때, Claude의 답변에 hook 표시가 아무것도 나타나지 않았습니다. 본 기사에서는 동일한 hook 작성 방식 7가지를 시도한 표(이 중 5가지 방식은 2.1.303과 2.1.309 모두 해당하며, 나머지 2가지 방식은 2.1.303에만 해당)와 각각의 해결 방법을 안내합니다 (Windows 11・Windows PowerShell 5.1・2026-10-23 실측 결과입니다).
7가지 작성 방식 실측표 (5가지는 2.1.303과 2.1.309 모두 해당)
PreToolUse(matcher는 Write)에, exit 2로 종료되는 hook을 배치했습니다. Claude에게 Write를 통해 파일을 하나 생성하도록 시켰고, 파일이 생기면 '성공'으로, 실패하면 '정지'로 판단했습니다.
| 작성 방식 | command 내용 | 2.1.303 | 2.1.309 |
|---|
| A | ` | | |
process.stderr.write("blocked by exit2.js\n");
process.exit(2);
이것을 PreToolUse 훅에 적용하고, "shell": "powershell"로 실행했습니다. 설정은 다음과 같습니다 (경로는 가렸습니다).
{
"hooks": {
"PreToolUse": [
...
}
2.1.303에서는 Write가 그대로 실행되어 파일이 생성되었습니다. 두 번 시도했고, 두 번 모두 그랬습니다. Claude의 응답은 '차단되지 않았습니다'였고, 훅 표시(hook display)는 나타나지 않았습니다.
동일한 명령어에서 "shell": "powershell"을 제거하고 Git Bash로 실행하자 멈췄습니다. Windows에서도 Git Bash가 설치되어 있다면, shell을 지정하지 않은 훅은 Git Bash에서 작동합니다 (공식 문서에 따르면, Git Bash가 없는 Windows에서는 기본값이 PowerShell입니다). 이 내용은 이전 기사에 작성했습니다.
테스트 환경
- Windows 11
- Claude Code 2.1.303 (본체)
- Claude Code 2.1.309 (임시 폴더에
npm install @anthropic-ai/[email protected]로 설치한 다른 실행 파일) - Windows PowerShell 5.1.26100.9711 (PowerShell 7의 pwsh는 설치되어 있지 않음)
- Node v24.14.1
- Git Bash 5.2.37
- 확인일: 2026-10-23
원인: PowerShell 5.1의 -Command는 종료 코드 2를 1로 변경함
Claude Code의 hook에서는 종료 코드 2만 '차단(block)'입니다. 공식 문서는 exit 2를 Blocking error (PreToolUse라면 도구 호출을 막음), 그 외의 종료 코드를 Non-blocking error (작업은 진행됨)로 구분합니다.
반면, Windows PowerShell 5.1의 -Command는 마지막으로 실행된 외부 프로그램의 종료 코드를 그대로 반환하지 않습니다. Microsoft 문서(about_PowerShell_exe)에는 마지막 명령어가 외부 프로그램이고, 종료 코드가 0도 아니고 1도 아닐 경우, 프로세스의 종료 코드를 1로 변경한다고 쓰여 있습니다. 같은 곳에, 종료 코드를 유지하고 싶을 때는 exit $LASTEXITCODE를 추가해야 한다고 명시되어 있습니다.
따라서 node는 2로 종료되었음에도 불구하고, 2.1.303의 Claude Code에는 2가 아닌 1이 전달된 것으로 추정됩니다 (아래 PowerShell 단독 결과와 나중에 작성할 F의 표시를 통해 유추했으며, Claude Code가 실제로 받은 값 자체는 측정하지 않았습니다). 1은 막지 않는 오류(error)이기 때문에 Write는 그대로 진행됩니다.
PowerShell 단독으로 종료 코드를 확인한 결과
Claude Code를 거치지 않고, PowerShell만으로 같은 것을 확인했습니다. exit2.js는 위의 두 줄로 구성된 스크립트입니다. 모두 Git Bash에서 실행했으며, 직후의 echo "rc=$?"로 종료 코드를 확인했습니다 ($LASTEXITCODE는 Git Bash에 미리 전개되지 않도록 \$로 이스케이프 처리했습니다).
| 실행한 명령어 | 종료 코드 |
|---|
bash -c "node exit2.js" | 2 |
powershell -NoProfile -Command "node exit2.js" | 1 |
powershell -NoProfile -Command "node exit2.js; exit $LASTEXITCODE" | 2 |
powershell -NoProfile -Command "node exit2.js; Write-Output after" | 0 (after가 출력됨) |
powershell -NoProfile -Command "exit 2" | 2 |
powershell -NoProfile -Command "[Environment]::Exit(2)" | 2 |
외부 프로그램의 종료 코드 2는 1로 바뀌고, PowerShell 자체의 exit 2는 그대로 반환됩니다. 표의 C가 2.1.303에서도 멈춘 것은 이 때문이라고 생각할 수 있습니다.
-File을 사용하여 node를 호출하는 단 한 줄짜리 .ps1 파일을 실행했을 때는, node가 실패(종료 코드 1)하더라도 0이 반환되었습니다. 마지막에 exit $LASTEXITCODE를 추가하자, node의 종료 코드가 반환되었습니다. 후크에서 .ps1을 호출하는 분들은 이 부분도 확인해 주십시오.
2.1.309에서 수정된 것과 수정되지 않은 것
2.1.309의 CHANGELOG에는 다음 한 줄이 있습니다 (CHANGELOG.md 원문에서 2026-10-23에 확인 가능).
Fixed hooks that run in PowerShell not blocking when the hook's program exits with code 2, and on Windows `PreToolUse` and `PermissionRequest` hooks with "shell": "powershell" failing open when Git for Windows isn't installed
실제로 동일한 설정의 A는 2.1.303에서는 통과했지만, 2.1.309에서는 멈췄습니다. 표시되는 내용은 PreToolUse:Write hook error: [node C:/.../exit2.js]: blocked by exit2.js입니다.
다만 E(node C:/.../exit2.js; Write-Output checked)는 2.1.309에서도 통과했습니다 (단 한 번의 결과입니다). PowerShell 단독으로 구성된 표와 같이, 뒤에 다른 명령어가 있으면 종료 코드는 그 명령어의 성공 여부에 따라 결정됩니다. Write-Output은 성공하므로 0이 되고, node의 2는 남지 않습니다. 종료 코드는 마지막 명령어로 결정된다는 PowerShell의 사양이기 때문에, 버전을 올려도 이 점은 변하지 않을 가능성이 높다고 생각합니다.
onFailure: block 표시로 알 수 있는 것
F는 A에 `
를 추가합니다(표의 F). 중지되기는 하지만, 표시가 "failed"로 나옵니다. onFailure는 공식 문서에서 v2.1.295 이상이 필요하다고 되어 있습니다.
Claude Code를 2.1.309 이상으로 올립니다. 제 환경에서는 A 방식은 2.1.309에서 중지되었지만, E(뒤에
Write-Output
을 이어서 작성하는 방식)는 2.1.309에서도 중지되지 않았으므로, 1~3 중 하나와 병합합니다.
PowerShell 처리를 한 번에 작성하고 싶을 때는 표의 C처럼 PowerShell 자체에서 exit 2를 반환하는 방식으로도 두 버전 모두에서 중지되었습니다.
확인 방법
수정했다면, 실제로 중지되는지 한번 시도해 보세요. 저는 임시 폴더에서 다음 형태의 claude -p를 실행하고, out.txt가 생성되었는지 여부로 판단했습니다(프롬프트는 중간 생략).
claude -p --settings <설정 파일> --permission-mode acceptEdits --max-turns 3 "Use the Write tool exactly once to create out.txt ..."
out.txt가 생성되지 않았다면 중지된 것입니다. 중지되었다면, hook이 표준 오류(stderr)에 기록한 이유(이번 경우 blocked by exit2.js)가 표시되었습니다.
종료 코드만 먼저 보고 싶을 때는 PowerShell 단독의 표처럼, hook과 동일한 명령어를 powershell -NoProfile -Command로 실행합니다. PowerShell 프롬프트에서 입력할 때는 바깥쪽을 작은따옴표(')로 감싸주세요. 큰따옴표(")를 그대로 두면 $LASTEXITCODE가 외부 PowerShell에서 먼저 전개되어 표와 같은 결과가 나오지 않습니다.
powershell -NoProfile -Command 'node exit2.js; exit $LASTEXITCODE'
$LASTEXITCODE
두 번째 줄의 $LASTEXITCODE로 내부 PowerShell의 종료 코드가 나옵니다(cmd에서 입력할 경우 echo %ERRORLEVEL%로 확인합니다). 제 환경에서는 단독으로 2가 반환되는 방식(표의 B, C)은 2.1.303에서도 2.1.309에서도 중지되었습니다. 한편, 2.1.309에서는 단독으로 1이 나오는 A도 중지되었습니다. 단독 실행 결과는 2가 반환되는지 여부를 보는 참고 자료로만 삼고, 최종적으로는 위의 claude -p 형태로 중지되는지를 확인해 주세요.
유사 증상 및 주의사항
"shell": "powershell"은 공식 문서에서 'Windows에서 PowerShell을 사용하여 명령어를 실행'하는 설정입니다. 이 PC에는 pwsh (PowerShell 7)가 없고 Windows PowerShell 5.1로 작동했습니다. 반면, 2.1.247의 Windows 11에서 shell: powershell이 pwsh를 시작하고, pwsh가 없는 PC에서는 hook이 작동하지 않는다는 보고(anthropics/claude-code의 issue #90077)도 있습니다. 버전이나 환경에 따라 다를 수 있으므로 자신의 PC에서 시도해 보세요. PowerShell이 exit 2를 1로 변경하는 건 다른 도구에서도 언급되었습니다. kenn-io/kit의 PR #158(2026-10-06)은 PowerShell 형태에서는 hook의 exit 2가 1이 되기 때문에, Windows용 Claude Code의 hook을 exec form으로 수정한 변경입니다.
- 2.1.309와 같은 줄에는 Git for Windows가 설치되지 않은 Windows에서
"shell": "powershell"
의 PreToolUse 및 PermissionRequest hook이 중지되지 않고 통과했던(failing open) 건에 대한 수정도 기재되어 있습니다. 이 건은 제 환경(Git Bash 있음)에서는 시도해 보지 않았습니다.
자신의 hook이 중지되었는지 여부는 설정만으로는 파악하기 어렵습니다. 일부러 exit 2로 종료되는 hook으로 테스트해보는 것이 좋습니다.
관련 기사 (Windows에서 Claude Code의 hook)
- Claude Code의 훅(hook)이 Windows에서 PowerShell로 작동하지 않는 원인은 Git Bash입니다.
- Claude Code 훅의
onFailure: block에서도 멈추지 않는 출력과, 멈추는 exit 1.
Windows의 개행 문자(newline code) 문제는 Claude Code의 Write 기능에서 CRLF가 LF로 변경되면서 git diff가 모든 줄에 걸쳐 발생하는 원인과 해결 방법을 작성했습니다.
참고
토론 (Discussion)