Claude Code UserPromptSubmit 훅의 차단·컨텍스트 주입 실험

UserPromptSubmit는 Enter를 누른 뒤 Claude가 입력 내용을 보기 전에 실행된다. 이런 위치 때문에 이 훅은 사람들이 두 가지 상황에서 자주 찾는 대상이 된다. 모든 프롬프트에 컨텍스트를 추가하는 경우(현재 브랜치, 오늘 날짜, 반복해 둘 만한 규칙)와 프롬프트 자체를 아예 거부하는 경우(붙여넣은 키, 절대 나가면 안 되는 요청)다. 두 작업은 결국 하나의 질문으로 귀결된다. 훅이 출력한 것들 중 무엇이 모델 앞에 가고, 무엇이 Claude Code를 실행한 사람이나 스크립트에 전달되며, 무엇이 디스크에 남는가? hooks 레퍼런스는 대부분을 산문으로 설명하지만, 나는 세션 파일 자체에서 답을 확인하고 싶었다. 그래서 단 하나의 훅으로 실험실을 만들고, 10가지 출력 형태를 claude -p로 각각 2번씩, 총 20번 실행한 뒤, 각 실행이 남긴 흔적을 모두 읽었다.
아래 모든 실험은 2026-09-30에 Claude Code 2.1.285(claude --version)와 claude-opus-5-5(--model opus로 선택)에서 수행했다. 문서 인용은 같은 날 trafilatura로 가져온 https://code.claude.com/docs/en/hooks.md에서 가져왔다.
hooks 페이지가 출력에 대해 말하는 것
아래는 내가 테스트한 문장들로, 가져온 그대로 인용한 것이다.
- 종료 코드 0일 때: "대부분의 이벤트에서 Claude Code는 stdout을 디버그 로그에 쓰며 트랜스크립트에는 표시하지 않습니다. 예외는
UserPromptSubmit,UserPromptExpansion,SessionStart,PostModelSwitch이며, 이 경우 Claude Code는 일반 텍스트 stdout을 Claude가 보고 행동할 수 있는 컨텍스트로 추가합니다."
UserPromptSubmit섹션에서: "일반 stdout과additionalContext값은 각각 훅 이름으로 시작하는 시스템 리마인더로 주입되며, Claude는 둘 다 읽습니다."
- 차단에 대해서는 결정표에서:
"block"은 "프롬프트가 처리되는 것을 막고 컨텍스트에서 지웁니다"이며,reason은"decision이"block"일 때 사용자에게 표시됩니다. 컨텍스트에 추가되지 않습니다". 종료 코드 표에서는 이 이벤트에서 종료 코드 2가 "프롬프트 처리를 차단하고 프롬프트를 지웁니다"라고 되어 있습니다.
- 종료 코드 2에 대해 구체적으로는: "종료 코드 2로 차단하는 훅은
reason과 동일한 방식으로 라우팅됩니다. 즉, 차단 메시지는 stderr 텍스트를 사용자에게 보여 주며, 컨텍스트에 추가되지 않습니다."
suppressOriginalPrompt:"decision이"block"일 때true이면, 사용자에게 표시되는 차단 메시지에서 원래 프롬프트 텍스트를 생략합니다".
- 일반 텍스트 stdout과 함께 다른 종료 코드를 쓰는 경우: "대부분의 훅 이벤트에서는 비차단 오류입니다. 동작은 계속 진행됩니다".
- JSON으로 간주되는 조건: "
{로 시작해서}로 끝나면 Claude Code는 이를 JSON으로 파싱합니다."
- 크기에 대해서는: 일반 stdout과
additionalContext는 "10,000자로 제한되며", 한도를 넘으면 "Claude Code가 출력을 세션 디렉터리의 파일로 저장하고, 이를 파일 경로와 최대 처음 2,000자까지의 미리보기로 대체합니다." 또한: "Claude Code는 Claude에게 그 파일을 읽으라고 요청하지 않으므로, Claude가 항상 봐야 하는 내용은 한도 안에 두세요."
실험실
실험실은 새 디렉터리다. git 저장소도 없고, CLAUDE.md도 없으며, 설정 파일 하나와 스크립트 하나만 있다. .claude/settings.json은 단 하나의 command 훅을 등록한다:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "python3 <lab>/.claude/hooks/ups-probe.py" } ] }
]
}
}(실제 파일에서는 인터프리터와 스크립트 둘 다 절대 경로이며, 이 글 전체에서 는 실험실 디렉터리를 뜻한다.) 스크립트는 mode.txt에서 모드를 읽고, 하나의 출력 형태를 출력한 뒤, 자체 로그에 한 줄을 덧붙인다. 그래서 각 실행마다 정확히 한 번 실행되었는지 확인할 수 있었다. 실제로 그랬다. 로그에는 20줄이 있다. 로깅을 제거한 버전은 다음과 같다:
import json, os, sys
LAB = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdin.read()
mode = open(os.path.join(LAB, "mode.txt")).read().strip()
out, err, code = "", "", 0
def long_text(n):
line = "filler line: the quick brown fox jumps over the lazy dog, again and again.\n"
chars = list((line * (n // len(line) + 2))[:n])
marks = [(0, "UPS-LONG-AT0000\n"), (1900, "\nUPS-LONG-AT1900\n"),
(2100, "\nUPS-LONG-AT2100\n"), (n // 2, "\nUPS-LONG-AT%05d\n" % (n // 2)),
(n - 13, "\nUPS-LONG-END")]
for at, text in marks:
chars[at:at + len(text)] = list(text)
return "".join(chars)
if mode == "plain":
out = "Lab note from the prompt hook: the marker for this prompt is UPS-STDOUT-PLAIN-7K2.\n"
elif mode == "addctx":
out = json.dumps({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
"additionalContext": "Lab note from the prompt hook: the marker for this prompt is UPS-ADDCTX-JSON-4Q9."}})
elif mode in ("block", "suppress"):
specific = {"hookEventName": "UserPromptSubmit",
"additionalContext": "Lab note from the prompt hook: UPS-BLOCK-ADDCTX-6T8."}
reason = "Lab block reason: UPS-BLOCK-REASON-2M5."
if mode == "suppress":
specific = {"hookEventName": "UserPromptSubmit",
"additionalContext": "Lab note from the prompt hook: UPS-SUPPRESS-ADDCTX-7V3.",
"suppressOriginalPrompt": True}
reason = "Lab block reason: UPS-SUPPRESS-REASON-5N1."
out = json.dumps({"decision": "block", "reason": reason, "hookSpecificOutput": specific})
elif mode == "exit2":
out = "Lab note from the prompt hook: UPS-EXIT2-STDOUT-1R6.\n"
err = "UPS-EXIT2-STDERR-L1-8P3 first line of stderr\nUPS-EXIT2-STDERR-L2-5W1 second line of stderr\n"
code = 2
elif mode == "exit1":
out = "Lab note from the prompt hook: UPS-EXIT1-STDOUT-6D4.\n"
err = "UPS-EXIT1-STDERR-L1-3H7 first line of stderr\nUPS-EXIT1-STDERR-L2-9B2 second line of stderr\n"
code = 1
elif mode.startswith("long"):
out = long_text(int(mode[4:]))
elif mode == "rawjson":
out = json.dumps({"branch": "main", "open_issues": 3, "lab_marker": "UPS-RAWJSON-DATA-3C1"})
sys.stdout.write(out); sys.stderr.write(err); sys.exit(code)제어 모드는 아무것도 출력하지 않고 종료 코드 0으로 끝난다. 모든 마커는 UPS-로 시작하고, 프롬프트는 모델에게 자신이 볼 수 있는 마커만 나열하고 그 외에는 아무것도 하지 말라고 요청한다:
Do not use any tools. Look at everything in your context for this turn, including any text
attached alongside this message. List every token that begins with the four characters UPS-
and continues with letters or digits, in the order they appear, separated by single spaces.
If there are none, reply NONE. Reply with that single line only.각 실행은 하나의 호출이었다:
claude -p "$PROMPT" --setting-sources project,local --tools "" --strict-mcp-config \
--max-turns 1 --model opus --output-format stream-json --verbose --include-hook-events \
--session-id "$SID" --debug-file "runs/$NAME/debug.txt" < /dev/null--setting-sources project,local는 내 사용자 설정을 제외하므로, 내 자체 hook은 하나도 실행되지 않았다. 스트림에는 실행마다 정확히 하나의 hook_started 이벤트만 보였고, 그게 바로 이것이었다. --tools ""는 모든 기본 제공 도구를 제거하므로, 모델은 이미 컨텍스트 안에 있는 것만 보고할 수 있고, 예를 들어 hook 출력이 가리키는 파일을 열 수도 없다. 구성 파일이 없는 상태에서 --strict-mcp-config를 사용하면 MCP 서버가 없다. --include-hook-events는 hook_started, hook_progress, hook_response 이벤트를 스트림에 추가하고, --session-id는 ~/.claude/projects/ / .jsonl에서 대본을 쉽게 찾을 수 있게 해준다. 또한 작업하던 Claude Code 세션이 내보내는 환경 변수들(CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_EFFORT 등 12개)을 모두 해제했기 때문에, 하위 실행은 그것들을 상속하지 않았다.
아래의 모든 주장은 실행마다 세 가지 출처에 기반한다. 첫째는 모델의 응답이다. 둘째는 대본이다. Claude Code가 유지하는 각 hook 출력은 attachment 기록이 되며, 그 기록에는 Claude Code가 모델을 위해 렌더링한 텍스트를 담은 rendered 필드가 있거나, 아무것도 렌더링하지 않았을 때는 null이 들어 있다. 셋째는 첫 요청의 크기인데, 어시스턴트 메시지의 usage에 있는 input_tokens + cache_creation_input_tokens + cache_read_input_tokens를 의미하며, 비교 기준은 제어 hook으로서 두 제어 실행 모두에서 2,756 토큰이었다. 응답의 마커는 그것이 컨텍스트 안에 있었음을 뜻하고, rendered 필드는 그것이 어떻게 들어갔는지를 말하며, 토큰 수는 그 비용을 말한다.
한 표에 담은 20회 실행
| Hook 출력(각 2회 실행) | 종료 코드 | 모델 요청 수 | 첫 요청, 토큰 | 제어군 대비 | 모델이 나열한 마커 | 대본에 남은 것 |
|---|---|---|---|---|---|---|
| 없음(제어군) | 0 | 1 | 2,756 | — | NONE | hook 기록 없음 |
| 83자 일반 stdout | 0 | 1 | 2,811 | +55 | stdout 마커, 2/2 | hook_success , sent |
| JSON additionalContext | 0 | 1 | 2,813 | +57 | context 마커, 2/2 | hook_additional_context , sent |
| 10,000자 stdout | 0 | 1 | 6,407 | +3,651 | 5개 중 5개 마커, 2/2 | hook_success , 10,068자 전송됨 |
| 50,000자 stdout | 0 | 1 | 3,708 / 3,711 | +952 / +955 | 5개 중 2개 마커, 2/2 | hook_success , 경로와 1,949자 미리보기 전송됨 |
| stdout와 stderr 2줄 | 1 | 1 | 2,756 | 0 | NONE | hook_non_blocking_error , rendered: null |
| 데이터만 있는 빈 JSON 객체 | 0 | 1 | 2,756 | 0 | NONE | 아무것도 없음 |
| decision: "block"과 reason | 0 | 0 | — | — | (모델 호출 없음) | 프롬프트, 2회 |
| stderr 2줄(및 stdout 1줄) | 2 | 0 | — | — | (모델 호출 없음) | 프롬프트, 2회 |
| decision: "block"과 suppressOriginalPrompt | 0 | 0 | — | — | (모델 호출 없음) | 프롬프트, 1회 |
모델에 프롬프트가 도달하지 않았던 6개를 포함해, 모든 20개의 claude -p 프로세스는 종료 코드 0으로 끝났다.
문서화된 두 채널과 그 비용
종료 코드 0의 일반 stdout은 페이지에 적힌 대로 정확히 동작했다. 대본에는 렌더링된 텍스트가 다음과 같은 hook_success 첨부 기록이 남았다:
<system-reminder>
UserPromptSubmit hook success: Lab note from the prompt hook: the marker for this prompt is UPS-STDOUT-PLAIN-7K2.
</system-reminder>JSON 형식은 UserPromptSubmit hook success: 대신 UserPromptSubmit hook additional context:가 들어간 같은 방식의 hook_additional_context 첨부 기록을 생성했다. 두 경우 모두 시스템 역할로 전송되었고, 각 실행에서 모델은 마커만 나열하고 그 외에는 아무것도 나열하지 않았다. 안내문 시작의 "hook's name"은 이벤트 이름이었다. 일반 stdout 기록에는 hook의 전체 명령줄, 원시 stdout, 종료 코드, 실행 시간도 저장되지만, 이들 중 어느 것도 렌더된 텍스트에는 들어가지 않았다. 비용은 일반 형식이 55토큰, JSON 형식이 57토큰이었는데, stdout 83자(82자에 Claude Code가 잘라낸 개행 1개 포함)와 81자 additionalContext에 대한 값이므로, 어느 쪽이든 래퍼는 토큰 몇 개 수준이다. 이 증거만 놓고 보면 컨텍스트만을 위해 JSON을 선호할 이유는 없다. 차이는 같은 출력에서 다른 필드도 함께 원할 때에만 의미가 있다.
10,000자 상한은 유지되었고, 더 큰 출력은 더 싸게 만들었다
10,000자 stdout은 그대로 통과했다. 그 렌더된 첨부 기록은 10,068자였는데, 출력에 68자짜리 래퍼를 더한 것이며, 모델은 두 실행 모두에서 다섯 마커 전부(오프셋 0, 1,901, 2,101, 5,001, 9,988)를 나열했다. 첫 요청에는 3,651토큰이 추가되었다.
50,000자 stdout은 그대로 통과하지 못했다. 그 첨부 기록은 포인터와 미리보기로 렌더링되었다:
UserPromptSubmit hook success: <persisted-output>
Output too large (48.8KB). Full output saved to: ~/.claude/projects/<project>/<session-id>/tool-results/hook-<id>-stdout.txt
Preview (first 2KB):
UPS-LONG-AT0000
quick brown fox jumps over the lazy dog, again and again.
[25 filler lines omitted here]
UPS-LONG-AT1900
r the lazy dog, again and again.
...
</persisted-output>대괄호로 둘러싼 줄, 즉 제 것이고, 그 경로를 가린 부분과 그 주위의 태그를 제외하면, 그것이 렌더링된 텍스트입니다. 닫는 태그 앞의 ...는 Claude Code의 것입니다. 미리보기에는 처음 1,949자가 들어갔고, 2,000번째 문자 바로 앞의 마지막 줄바꿈에서 잘렸으므로, 오프셋 1,901의 표식은 포함되었고 2,101의 표식은 포함되지 않았습니다. 모델은 두 번의 실행 모두에서 바로 그 두 표식만 나열했고, 중간이나 끝은 전혀 언급하지 않았습니다. 저장된 파일은 전체 50,000바이트였습니다. 요청은 952토큰과 955토큰만큼 늘었는데, 이는 10,000자 출력에 든 비용의 약 4분의 1이었습니다. 텍스트는 다섯 배였지만 비용은 3.8배 더 쌌습니다. 모델이 대신 미리보기를 받았기 때문입니다.

이 모든 것은 문서와 일치합니다. Claude Code가 Claude에게 파일을 읽으라고 요청하지 않는다는 부분까지도 그렇습니다. 렌더링된 블록에는 경로가 있을 뿐 지시는 없습니다. 실질적인 결과는 완만한 경사가 아니라 절벽입니다. 어떤 훅은 출력이 9,000자일 때도 있고 11,000자일 때도 있는데, 이 경우 모델에 전혀 다른 두 가지를 보내게 됩니다. 그리고 긴 날에는 2,000번째 문자 바로 앞의 마지막 줄바꿈 이후의 모든 내용이 사라집니다. 훅의 출력이 늘어날 수 있다면, 직접 10,000자보다 훨씬 아래로 제한하거나 중요한 내용을 처음 1,900자 안에 넣으세요.
종료 코드 1과 빈 JSON 객체: 출력은 됐지만 기록만 되고 전송되지는 않음
종료 코드 1 모드는 stdout에 컨텍스트 한 줄을 출력하고 stderr에 두 줄을 출력한 뒤 1로 종료했습니다. 모델은 두 번의 실행 모두에서 NONE으로 답했고, 첫 번째 요청은 두 번 다 2,756토큰으로, 대조군과 같았습니다. 트랜스크립트에는 기록이 남아 있었는데, stdout을 담은 hook_non_blocking_error 첨부와 Failed with non-blocking status code: 접두사가 붙은 두 줄의 stderr, 종료 코드와 명령이 포함되어 있었습니다. 하지만 rendered 필드는 null이어서 아무것도 전송되지 않았습니다. stream-json 출력에도 이에 대한 경고 이벤트는 없었습니다. 유일한 흔적은 "exit_code": 1과 "outcome": "error"가 들어 있는 hook_response 이벤트뿐이었습니다.
문서에서는 이를 "작업은 계속 진행된다"는 비차단 오류라고 부르고, 종료 코드 0일 때만 stdout을 컨텍스트로 약속하므로, 여기에는 모순이 없습니다. 측정이 더해 주는 것은 그 결과입니다. 훅이 실패하기 전에 출력한 내용은 버려지고, 모델의 입장에서는 아무것도 출력되지 않은 것과 같습니다. set -e가 설정된 bash 훅이 브랜치 이름을 출력한 뒤 상태 1로 실패하는 명령을 실행한다면, 바로 이 경우입니다.
빈 JSON 객체는 더 조용했습니다. {"branch": "main", "open_issues": 3, "lab_marker": "UPS-RAWJSON-DATA-3C1"} 는 {로 시작해 }로 끝나므로 Claude Code는 이를 훅 출력으로 파싱했습니다. 디버그 로그에는 Successfully parsed and validated hook JSON output 다음에 Hook JSON output had unrecognized keys (ignored): branch, open_issues, lab_marker.가 표시됩니다. 트랜스크립트에는 첨부가 없었고 스트림에도 경고가 없었으며, 요청은 두 번 모두 2,756토큰이었습니다. 모델이 데이터로 읽으라고 기대한 채 gh pr view --json title,body의 출력 같은 JSON 객체를 stdout에 그대로 찍는 훅이나, 객체를 내보내는 jq 필터는 아무런 효과도 더하지 않습니다. 그리고 이를 알려 주는 유일한 장소는 Claude Code가 --debug 또는 --debug-file로 실행될 때만 존재하는 디버그 로그입니다. 데이터를 additionalContext 안에 넣거나, 앞에 텍스트 한 줄을 출력하세요. 문서에서는 JSON 배열을 일반 텍스트로 취급하므로, 이것은 객체에 관한 이야기입니다. 배열은 실행하지 않았습니다.
차단: 모델은 프롬프트를 보지 못하지만, 디스크에는 남음
세 가지 모드는 프롬프트를 차단했습니다. reason이 있는 decision: "block"과, stderr 두 줄을 출력한 뒤 종료 코드 2로 끝난 경우, 그리고 suppressOriginalPrompt가 있는 decision: "block"입니다. 여섯 번의 실행 모두 num_turns: 0, total_cost_usd: 0으로 끝났고, assistant 메시지도 없었습니다. 모델은 프롬프트를 받지 못했습니다. 이것이 문서가 말하는 "컨텍스트에서 지운다"는 절반이며, 실제로 매번 그렇게 동작했습니다.
claude -p가 보고한 내용은 또 다른 문제다. 스트림에는 "level": "warning"이고 "prevent_continuation": true인 informational 하위 유형의 system 이벤트가 있었고, 이어서 "subtype": "success"이고 "is_error": false인 결과 이벤트가 왔으며, 프로세스는 0으로 종료됐다. 결과 텍스트는 차단 메시지였다. JSON 블록의 경우에는 다음과 같았다:
UserPromptSubmit operation blocked by hook:
Lab block reason: UPS-BLOCK-REASON-2M5.
Original prompt: Do not use any tools. Look at everything in your context for this turn, ...exit 2의 경우에는 다음과 같았다:
UserPromptSubmit operation blocked by hook:
[/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 <lab>/.claude/hooks/ups-probe.py]: UPS-EXIT2-STDERR-L1-8P3 first line of stderr
UPS-EXIT2-STDERR-L2-5W1 second line of stderr
Original prompt: Do not use any tools. Look at everything in your context for this turn, ...문서에는 없는 세부 사항이 두 가지 있다. exit 2 메시지는 stderr 앞에 대괄호로 둘러싼 hook의 전체 명령줄을 포함하고, 그 뒤에 stderr 두 줄이 이어진다. 그리고 각 블록에 덧붙인 추가 출력은 어디로도 가지 않았다. exit 2와 함께 출력된 stdout 줄과 JSON 블록 안의 additionalContext는 스트림의 원시 hook 이벤트에만 나타나며, transcript나 결과에는 전혀 나타나지 않는다.
transcript에서 "프롬프트를 지운다"는 말은 더 이상 사실이 아니게 된다. 차단된 세션의 파일에 있는 첫 번째 기록은 queue-operation 기록이었고, "operation": "enqueue"와 완전한 프롬프트 텍스트를 담고 있었으며, Claude Code가 hook의 출력을 파싱하기 전에 타임스탬프가 찍혀 있었다(첫 번째 JSON 블록 실행에서 04:14:28.526 대 04:14:29.571). 그 이후에 이를 지운 것은 없었다. 차단 메시지, "Original prompt" 줄을 포함한 메시지, 는 다시 system 기록으로 저장됐다. user 기록은 없었으므로 대화 자체에는 프롬프트가 들어 있지 않지만, 파일에는 suppressOriginalPrompt가 없는 4번의 실행 모두에서 두 번씩 들어 있다.

suppressOriginalPrompt는 메시지는 고치지만 파일은 고치지 못한다. hooks 페이지의 표에는 그것이 어디로 가는지 말하지 않은 채 나와 있다. 나는 그것을 additionalContext 옆의 hookSpecificOutput 안에 넣었다. 그렇게 하자 결과 텍스트는 UserPromptSubmit operation blocked by hook:와 이유만 남고 프롬프트는 사라졌으며, 2번 중 2번 모두 그랬다. 하지만 enqueue 기록에는 여전히 완전한 프롬프트가 2번 중 2번 모두 남아 있었다.
만약 당신의 hook이 붙여넣은 비밀 정보가 밖으로 나가는 것을 막기 위한 것이라면, 이 실행들은 그 비밀이 모델까지는 도달하지 않는다고 말한다. 또한 그것이 어쨌든 ~/.claude/projects/에는 저장된다고 말하고, suppressOriginalPrompt가 없으면 claude -p 결과로 다시 출력되므로, CI에서는 그것이 로그 한 줄이 된다고도 말한다. claude -p의 종료 코드를 확인하는 CI 단계는 차단된 실행도 성공으로 간주하게 된다.
이 실행들 이후 UserPromptSubmit hook에서 내가 바꾸고 싶은 것
컨텍스트 hook의 규칙은 항상 종료 코드 0이다. 실패할 수 있는 것들(git, gh, 네트워크 호출)은 스크립트가 반드시 0으로 끝나도록 감싸라. 종료 코드 1이면 hook이 이미 출력한 모든 것이 버려지기 때문이다. 헤드리스 실행에서는 원시 hook 이벤트만 그 사실을 말해 준다. 텍스트를 출력하거나 additionalContext가 있는 JSON을 출력하되, 데이터 객체만 덩그러니 내보내지는 마라. 출력은 10,000자를 넘기지 말고, 넘길 경우 모델이 실제로 보는 것은 처음 1,900자 정도뿐이라고 가정하라.
차단 hook의 경우, exit 2와 decision: "block"은 둘 다 2번 중 2번 모두 요청을 멈췄으므로, 사용자가 무엇을 봐야 하는지에 따라 선택하라. exit 2는 hook의 명령줄 전체를 앞에 붙인 채 stderr를 보여 주고, JSON 형식은 명령줄 없이 reason을 보여 준다. 프롬프트 자체가 문제일 수 있다면 언제나 suppressOriginalPrompt를 추가하고, transcript에는 어쨌든 그것이 남아 있다고 간주하라. claude -p가 스크립트에서 실행될 때는 종료 코드가 아니라 결과 텍스트, num_turns: 0, 또는 스트림의 informational 경고 이벤트를 통해 차단 여부를 감지하라.
내가 측정하지 않은 것
모든 실행은 헤드리스로 진행되었습니다. 저는 대화형 인터페이스를 보지 않았습니다. 문서에는 훅 오류 알림이 stderr의 첫 줄을 보여준다고 되어 있지만, 그것도 확인하지 않았고, 대화형 세션이 같은 enqueue 레코드를 쓰는지도 확인하지 않았습니다. 저는 10,000자와 50,000자를 측정했을 뿐 10,001자는 측정하지 않았으므로, 상한선의 정확한 경계는 제 말이 아니라 문서의 표현입니다. 저는 additionalContext를 통해 10,000자보다 많은 내용을 보내지 않았습니다. 해당 페이지에 따르면 additionalContext는 별도로 상한이 정해져 있습니다. CLI 참조에는 --no-session-persistence("세션 지속성을 비활성화하여 세션이 디스크에 저장되지 않고 다시 시작할 수 없게 합니다. 인쇄 모드에서만 사용 가능.")가 안내되어 있지만, 차단된 프롬프트가 디스크에 남지 않게 하는지는 테스트하지 않았습니다. --tools ""를 사용했을 때 모델은 저장된 50,000자 파일을 읽을 수 없었으므로, 만약 읽을 수 있었다면 어떻게 되었을지는 알지 못합니다. 또한 종료 코드 2와 JSON의 조합, continue: false, 비동기 훅, 타임아웃, HTTP 및 프롬프트 훅, 종료 코드 0에서의 stderr, 다른 모델들, Windows도 제외했습니다.
참고용 숫자
2026-09-30 UTC 04:10부터 04:22 사이에 진행된 claude -p 20회 실행, macOS의 Claude Code 2.1.285, claude-opus-5-5, Python 3.13 훅 스크립트, 각 10개 모드당 2회 실행. 모델에 도달한 14회 실행은 각각 2.6초에서 15.7초가 걸렸고, 차단된 6회 실행은 API 시간이 전혀 없이 0.5초에서 1.8초에 끝났습니다. 20회 전체에 대한 보고 비용은 $0.116이었습니다. 응답은 요청이 실제로 발생한 14회 실행에서 렌더링된 첨부 파일과 일치했습니다. 렌더링된 모든 마커는 목록에 포함되었고, 렌더링되지 않은 마커는 하나도 포함되지 않았습니다.
Rulestack는 Claude Code용 훅, 스킬, 규칙 파일을 작성해 rulestack.gumroad.com에서 판매합니다. 여기 실험실 환경은 설정 파일 하나, 스크립트 하나, 그리고 mode.txt 하나뿐이며, 20회 실행에 총 $0.116밖에 들지 않았으므로, 프롬프트 훅의 출력 형태가 바뀔 때마다 저렴하게 다시 실행할 수 있습니다.
사용 중인 Claude Code 버전이 차단된 프롬프트를 다른 곳에 저장하거나 아예 대화 기록에 남기지 않는다면, 아래 댓글에 버전을 남겨 주세요. 다음 측정을 위해 Bluesky에서 @ai-shop.bsky.social을 팔로우해 주세요.
출처
- 원문: https://dev.to/rulestack/6-of-6-prompts-a-claude-code-userpromptsubmit-hook-erased-were-still-in-the-transcript-and-15m2 (2026-10-06 · Rulestack)