Claude Code Hook์ผ๋ก ์์ด ๊ณต๋ถํ๊ธฐ
๋ฐฐ๊ฒฝ ์ค๋ช
ํ์์ ์์ด๋ก ํ๋กฌํํธ๋ฅผ ์์ฑํ๋ฉด ๋ชจ๋ธ์ด ๋ ์ข์ ์ฑ๋ฅ์ ๋ณด์ผ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์๊ณ ๊ฒธ์ฌ๊ฒธ์ฌ ์์ด์ ๋ ์ต์ํด์ง๊ธฐ ์ํด์ ๋๋๋ก Claude Code, Codex ๋ฑ์ ์ฌ์ฉํ ๋๋ ์์ด๋ก prompt๋ฅผ ์์ฑํ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ด ๊ฒฝ์ฐ์ ๋ถ์กฑํ ์์ด ์ค๋ ฅ ๋๋ฌธ์ ์๋ฏธ๋ ๋์์ค๊ฐ ์๋ชป ์ ๋ฌ๋๋ ๊ฒฝ์ฐ๊ฐ ์ข
์ข
์์์ต๋๋ค. ์ฒ์์๋ prompt rewriter๋ฅผ ๋ง๋ค์ด์ ๊ธฐ์กด ํ๋กฌํํธ๋ฅผ ์๋์ผ๋ก ๊ต์ ๋ ๋ฒ์ ์ผ๋ก ๋์ฒดํ๋ hook์ ๋ง๋ค๋ ค๊ณ ํ์ต๋๋ค. ํ์ง๋ง Claude Code์ hook ์์คํ
์ ๊ธฐ์กด ํ๋กฌํํธ๋ฅผ ์์ ํ๋ ๊ฒ์ด ์๋๋ผ ์ถ๊ฐ์ ์ธ ํ๋กฌํํธ๋ง ์ ๊ณตํ ์ ์์์ต๋๋ค. ๋ํ ์ถ๊ฐ์ ์ธ ํ๋กฌํํธ ์ ๊ณต๋ ์๋ฒฝํ์ง ์๋ค๋ ์ด์๋ค์ด ์์ด ์ค์ ๋ก ์ ์ฉํ๋ ๋ฐ ์ด๋ ค์์ด ์์์ต๋๋ค. issue link ๊ทธ๋ฌ๋ค jiun.dev์ ๊ธ์์ ์์ด ๊ณต๋ถ์ฉ์ผ๋ก hook์ ํ์ฉํ๋ ์์ด๋์ด๋ฅผ ์ป์๊ณ , ์ด๋ฅผ ์ฐธ๊ณ ํด์ ์์ด ๊ณต๋ถ์ฉ์ผ๋ก ์์ ํด๋ณด๊ธฐ๋ก ํ์ต๋๋ค. ๊ตฌํ ์ ๋ ๋ชจ๋ธ ์ฑ๋ฅ ์ ํ๋ฅผ ์ต์ํํ๊ณ ์ถ์๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ ํ๋กฌํํธ์ context๊ฐ ์ฃผ์
๋์ง ์๋ ๊ฒ์ด ์ด์์ ์ด์์ต๋๋ค. ์ด๋ฅผ ์ํ์ฌ 2๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์์ต๋๋ค. ๋ฉ์ธ Claude Code ํ๋ก์ธ์ค์์ ์์ด ๊ณต๋ถ ํ๋กฌํํธ๋ฅผ ์ฒ๋ฆฌํ์ง ์๊ณ ๋ณ๋์ Claude Code ์๋ธ ํ๋ก์ธ์ค์์ non-interactive๋ชจ๋์ structured output์ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌํ๋๋ก ํ์์ต๋๋ค. hook์ output์ผ๋ก systemMessage๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ ์๊ฒ๋ง ๋ฉ์์ง๊ฐ ๋ณด์ผ ์ ์๋๋ก ํ์์ต๋๋ค. (๊ด๋ จ claude code ๋ฌธ์) ์ด๋ ๋ณ๋์ Claude Code ํ๋ก์ธ์ค๋ฅผ non-interactive ๋ชจ๋๋ก ์คํํ๋๋ผ๋ hook์ด ์ฃผ์
๋๊ธฐ ๋๋ฌธ์ ํ๊ฒฝ๋ณ์๋ฅผ ํตํด ์์ฃผ ๊ฐ๋จํ LOCK์ ๊ตฌํํ์์ต๋๋ค. disableAllHooks ์ต์
์ ํตํด hook์ ๋นํ์ฑํ ํ ์ ์์ผ๋ ์ด ๊ฒฝ์ฐ์ structured output์ด ํ์๋กํ๋ SDK hook์ด ๋์ํ์ง ์์ structured output์ ๋ฐ์ ์ ์์ต๋๋ค. ์ค์ ๋ฐฉ๋ฒ 1. ์คํฌ๋ฆฝํธ ์ค์น ์๋ ์คํฌ๋ฆฝํธ๋ฅผ ~/.claude/english-lecturer.sh๋ก ์ ์ฅํฉ๋๋ค. ( ์ dotfiles ๋ ํฌ์์๋ ํ์ธ๊ฐ๋ฅํฉ๋๋ค ) #!/bin/bash # acknowledge: https://github.com/crescent-stdio for prompt if [[ -n "$REWRITER_LOCK" ]]; then exit 0 fi INPUT_PROMPT="$(cat | jq '.prompt')" TARGET_LANGUAGE="Korean" JSON_SCHEMA=' { "type": "object", "properties": { "enhanced_prompt": { "type": "string", "description": "The improved prompt preserving original meaning" }, "has_corrections": { "type": "boolean", "description": "Whether the original prompt had any issues to improve" }, "corrections": { "type": "array", "items": { "type": "object", "properties": { "original": { "type": "string" }, "suggestion": { "type": "string" }, "category": { "type": "string", "enum": ["grammar", "vocabulary", "style", "spelling", "word_order"] }, "explanation": { "type": "string" } }, "required": ["original", "suggestion", "category", "explanation"] }, "description": "Gentle improvement suggestions, max 3 items" }, "tip": { "type": "string", "description": "One concise learning tip" } }, "required": ["enhanced_prompt", "has_corrections", "corrections", "tip"] } ' INPUT_PROMPT="\ You are a supportive, encouraging English coach for a $TARGET_LANGUAGE developer. Analyze the prompt below and return structured JSON. Rules: 1. enhanced_prompt: Rewrite to be clear, natural, professional English. Preserve the original intent exactly. If the prompt is code-only or already perfect English, return it unchanged. 2. has_corrections: true if you made any meaningful improvements, false if the prompt was already correct or is pure code/commands. 3. corrections: List up to 3 gentle improvement suggestions. Each must have: - original: the phrase from the original prompt - suggestion: the improved phrase - category: one of grammar, vocabulary, style, spelling, word_order - explanation: brief explanation in $TARGET_LANGUAGE (1 sentence, max 20 words). 4. tip: One memorable tip in $TARGET_LANGUAGE (1 sentence, max 30 words) about the most useful pattern. If no corrections, share a useful English expression tip. Focus on patterns $TARGET_LANGUAGE speakers commonly struggle with: articles (a/the), prepositions, singular/plural, tense consistency, word order. <PROMPT> $INPUT_PROMPT </PROMPT>\ " RESPONSE="$( \ REWRITER_LOCK=1 claude \ --no-session-persistence \ --model sonnet \ --output-format json \ --json-schema "$JSON_SCHEMA" \ -p "$INPUT_PROMPT" )" STRUCTURED_OUTPUT="$(echo "$RESPONSE" | jq -r '.structured_output')" # echo 'This is lesson message for user. Assistant should dismiss it.' if [[ -z "$STRUCTURED_OUTPUT" || "$STRUCTURED_OUTPUT" == "null" ]]; then OUTPUT_PROMPT="Failed to generate lesson." exit 0 fi ENHANCED="$(echo "$STRUCTURED_OUTPUT" | jq -r '.enhanced_prompt')" HAS_CORRECTIONS="$(echo "$STRUCTURED_OUTPUT" | jq -r '.has_corrections')" TIP="$(echo "$STRUCTURED_OUTPUT" | jq -r '.tip')" OUTPUT_PROMPT="$ENHANCED" if [[ "$HAS_CORRECTIONS" == "true" ]]; then CORRECTIONS_DISPLAY="$(echo "$STRUCTURED_OUTPUT" | jq -r ' .corrections[] | "- โ
\(.category): \(.original) โ \(.suggestion)\n - \(.explanation)\n" ')" OUTPUT_PROMPT="$OUTPUT_PROMPT $CORRECTIONS_DISPLAY" fi OUTPUT_PROMPT=" $OUTPUT_PROMPT โจ $TIP" OUTPUT_PROMPT="$(echo -e "$OUTPUT_PROMPT")" # escape newlines OUTPUT_PROMPT="${OUTPUT_PROMPT//$'\n'/\\n}" # escape double quotes OUTPUT_PROMPT="${OUTPUT_PROMPT//\"/\\\"}" echo "{ \"suppressOutput\": false, \"systemMessage\": \"$OUTPUT_PROMPT\" }" exit 0 2. ์ค์ ํ์ผ ์์ ~/.claude/settings.json์ ๋ค์ ๋ด์ฉ์ ์ถ๊ฐํฉ๋๋ค. { "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "~/.claude/english-lecturer.sh" } ] } ] } } ํน์ ์๋ ๋ช
๋ น์ด๋ฅผ ์คํํฉ๋๋ค. jq '.hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) + [{"hooks": [{"type": "command", "command": "~/.claude/english-lecturer.sh"}]}])' ~/.claude/settings.json > /tmp/settings.json && mv /tmp/settings.json ~/.claude/settings.json 3. ์ปค์คํฐ๋ง์ด์ง ์คํฌ๋ฆฝํธ ๋ด์ ๋ค์ ๋ณ์๋ค์ ์์ ํด์ ๋์์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค. TARGET_LANGUAGE: ๋ฌธ๋ฒ ์ค๋ช
์ ์ธ์ด (๊ธฐ๋ณธ๊ฐ: โKoreanโ) JSON_SCHEMA: Claude๋ก๋ถํฐ ๋ฐ์ ์๋ต์ ๊ตฌ์กฐ (ํ๋กฌํํธ๋ฅผ ํฌ๊ฒ ๋ณ๊ฒฝํ์ง ์๋๋ค๋ฉด ์์ ์ด ํ์ํ์ง ์์ต๋๋ค) ์ฌ์ฉ ๋ชจ๋ธ (๊ธฐ๋ณธ๊ฐ: โsonnetโ / haiku์ ๊ฒฝ์ฐ ๋น ๋ฅด๊ธด ํ๋ ์ฑ๋ฅ์ด ๋จ์ด์ก์ต๋๋ค) ์ด์ ํ๋กฌํํธ๋ฅผ ์ ์ถํ ๋๋ง๋ค ์๋์ผ๋ก ๊ต์ ๋ ๋ฒ์ ๊ณผ ์งง์ ๋ฌธ๋ฒ ์ค๋ช
์ ๋ฐ์ ์ ์์ต๋๋ค. Appendix: change log 2026/02/06: Claude Code์ ์ถ๊ฐ๋ systemMessage ๊ธฐ๋ฅ ์ฌ์ฉ-history toggle ์์ด ํ์, --no-session-persistence ์ต์
์ถ๊ฐ 2026/02/02: ํ๋กฌํํธ ๊ฐ์ 2026/01/31: ํฌ์คํธ ์ฒซ ์์ฑ
torch.vision ยท Lab of ryul99