Claude Code의 UserPromptSubmit 훅은 배경 작업 완료 알림도 받는다 — 하네스가 넣은 알림은 통과시킨다
사람의 새 지시만 막으려던 훅이 배경 명령의 완료 알림까지 막으면, 기다리던 일이 끝나도 세션이 깨어나지 못합니다. 알림 꼴은 먼저 통과시키고, 실제 하네스 입력으로 시험합니다.
문제
Claude Code의 UserPromptSubmit 훅으로 「조건이 맞으면 새 지시를 받지 않는다」를 걸었습니다. 사람이 친 지시는 의도대로 막혔습니다. 그런데 같은 세션이 CI 같은 긴 명령을 배경으로 돌려 놓고 기다리던 중, 명령이 끝나도 세션이 다시 움직이지 않았습니다. 기록에는 완료 알림이 훅에 막혔다(blocked by hook)는 줄만 남았습니다.
원인
하네스는 배경 작업이 끝났다는 사실을 따로 된 통로가 아니라 사용자 프롬프트 한 건으로 세션에 넣습니다. 그 프롬프트는 <task-notification>으로 시작하고, 사람이 친 지시와 똑같이 UserPromptSubmit을 탑니다. 훅이 프롬프트 내용을 보지 않고 막으면 완료 알림도 버려집니다. 배경 작업을 기다리던 세션은 그 알림으로 깨어나게 되어 있으므로, 알림이 버려지면 아무도 깨우지 않는 상태로 멈춥니다.
해법
프롬프트를 막는 훅은 먼저 하네스가 자동으로 넣은 꼴인지 보고, 그렇다면 그대로 통과시킵니다. 「사람의 새 지시를 막는다」가 목적이면 막을 대상은 사람이 친 프롬프트뿐입니다.
// UserPromptSubmit 훅: 하네스가 넣은 자동 알림은 막지 않는다
const HARNESS_PROMPT = /^\s*<task-notification>/;
let raw = '';
process.stdin.on('data', (c) => (raw += c));
process.stdin.on('end', () => {
const input = JSON.parse(raw);
const prompt = String(input.prompt || '');
if (HARNESS_PROMPT.test(prompt)) process.exit(0); // 배경 작업 완료 알림
if (shouldBlock(input)) {
process.stdout.write(JSON.stringify({ decision: 'block', reason: '새 세션에서 받으세요' }));
}
process.exit(0);
});
시험은 실제 입력으로
손으로 지어낸 입력 JSON으로만 시험하면 이런 꼴은 빠지기 쉽습니다. 훅을 걸기 전에 잠시 입력을 그대로 파일로 남기는 훅을 걸어 두고, 사람 지시·배경 완료 알림·서브에이전트 호출을 한 번씩 일으켜 실제 입력을 모읍니다. 그 파일을 훅에 그대로 흘려 넣는 시험을 두면, 하네스가 나중에 다른 자동 메시지를 더해도 같은 방법으로 잡을 수 있습니다.
# 훅 입력을 그대로 남기기(잠깐만 걸어 두는 훅 명령)
cat > "hook-input-$(date +%s%N).json"
# 모은 입력으로 다시 돌려 보기
node my-hook.js < hook-input-task-notification.json