hooks๋ฅผ ์ฌ์ฉํ์ฌ ์์ ์๋ํ
Claude Code๊ฐ ํ์ผ์ ํธ์งํ๊ฑฐ๋ ์์ ์ ์๋ฃํ๊ฑฐ๋ ์ ๋ ฅ์ด ํ์ํ ๋ ์๋์ผ๋ก ์ ธ ๋ช ๋ น์ ์คํํฉ๋๋ค. ์ฝ๋ ํ์ ์ง์ , ์๋ฆผ ์ ์ก, ๋ช ๋ น ๊ฒ์ฆ ๋ฐ ํ๋ก์ ํธ ๊ท์น ์ ์ฉํฉ๋๋ค.
Hooks๋ Claude Code์ ๋ผ์ดํ์ฌ์ดํด์ ํน์ ์ง์ ์์ ์คํ๋๋ ์ฌ์ฉ์ ์ ์ ์ ธ ๋ช ๋ น์ ๋๋ค. ์ด๋ค์ Claude Code์ ๋์์ ๋ํ ๊ฒฐ์ ๋ก ์ ์ ์ด๋ฅผ ์ ๊ณตํ์ฌ LLM์ด ์คํํ๋๋ก ์ ํํ๋ ๊ฒ์ ์์กดํ๊ธฐ๋ณด๋ค๋ ํน์ ์์ ์ด ํญ์ ๋ฐ์ํ๋๋ก ๋ณด์ฅํฉ๋๋ค. Hooks๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ก์ ํธ ๊ท์น์ ์ ์ฉํ๊ณ , ๋ฐ๋ณต์ ์ธ ์์ ์ ์๋ํํ๋ฉฐ, Claude Code๋ฅผ ๊ธฐ์กด ๋๊ตฌ์ ํตํฉํฉ๋๋ค.
ํ๋จ์ด ํ์ํ ๊ฒฐ์ ์ ๊ฒฝ์ฐ ๊ฒฐ์ ๋ก ์ ๊ท์น์ด ์๋ ๊ฒฝ์ฐ, ํ๋กฌํํธ ๊ธฐ๋ฐ hooks ๋๋ ์์ด์ ํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฌ์ฉํ ์๋ ์์ต๋๋ค. ์ด๋ค์ Claude ๋ชจ๋ธ์ ์ฌ์ฉํ์ฌ ์กฐ๊ฑด์ ํ๊ฐํฉ๋๋ค.
Claude Code๋ฅผ ํ์ฅํ๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ skills๋ฅผ ์ฐธ์กฐํ์ฌ Claude์ ์ถ๊ฐ ์ง์นจ๊ณผ ์คํ ๊ฐ๋ฅํ ๋ช ๋ น์ ์ ๊ณตํ๊ณ , subagents๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒฉ๋ฆฌ๋ ์ปจํ ์คํธ์์ ์์ ์ ์คํํ๋ฉฐ, plugins๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ก์ ํธ ์ ์ฒด์์ ๊ณต์ ํ ํ์ฅ์ ํจํค์งํฉ๋๋ค.
์ด ๊ฐ์ด๋๋ ์ผ๋ฐ์ ์ธ ์ฌ์ฉ ์ฌ๋ก์ ์์ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค. ์ ์ฒด ์ด๋ฒคํธ ์คํค๋ง, JSON ์ ์ถ๋ ฅ ํ์ ๋ฐ ๋น๋๊ธฐ hooks ๋ฐ MCP ๋๊ตฌ hooks์ ๊ฐ์ ๊ณ ๊ธ ๊ธฐ๋ฅ์ Hooks ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
์ฒซ ๋ฒ์งธ hook ์ค์
Hook์ ๋ง๋ค๋ ค๋ฉด ์ค์ ํ์ผ์ hooks ๋ธ๋ก์ ์ถ๊ฐํฉ๋๋ค. ์ด ์ฐ์ต์ ๋ฐ์คํฌํฑ ์๋ฆผ hook์ ๋ง๋ค๋ฏ๋ก Claude๊ฐ ํฐ๋ฏธ๋์ ๋ณด๋ ๋์ ์
๋ ฅ์ ๊ธฐ๋ค๋ฆด ๋๋ง๋ค ์๋ฆผ์ ๋ฐ์ต๋๋ค.
์ค์ ์ hook ์ถ๊ฐ
~/.claude/settings.json์ ์ด๊ณ Notification hook์ ์ถ๊ฐํฉ๋๋ค. ์๋ ์์ ๋ macOS์ฉ osascript๋ฅผ ์ฌ์ฉํฉ๋๋ค. Linux ๋ฐ Windows ๋ช
๋ น์ Claude๊ฐ ์
๋ ฅ์ด ํ์ํ ๋ ์๋ฆผ ๋ฐ๊ธฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
์ค์ ํ์ผ์ ์ด๋ฏธ hooks ํค๊ฐ ์์ผ๋ฉด ์ ์ฒด ๊ฐ์ฒด๋ฅผ ๋ฐ๊พธ๋ ๋์ Notification์ ๊ธฐ์กด ์ด๋ฒคํธ ํค์ ํ์ ๋ก ์ถ๊ฐํฉ๋๋ค. ๊ฐ ์ด๋ฒคํธ ์ด๋ฆ์ ๋จ์ผ hooks ๊ฐ์ฒด ๋ด์ ํค์
๋๋ค:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
}
],
"Notification": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }]
}
]
}
}
CLI์์ ์ํ๋ ๊ฒ์ ์ค๋ช ํ์ฌ Claude์๊ฒ hook์ ์์ฑํ๋๋ก ์์ฒญํ ์๋ ์์ต๋๋ค.
๊ตฌ์ฑ ํ์ธ
/hooks๋ฅผ ์
๋ ฅํ์ฌ hooks ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฝ๋๋ค. ๊ตฌ์ฑ๋ hooks๊ฐ ์๋ ๊ฐ ์ด๋ฒคํธ ์์ ๊ฐ์๊ฐ ์๋ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ hook ์ด๋ฒคํธ ๋ชฉ๋ก์ด ํ์๋ฉ๋๋ค. Notification์ ์ ํํ์ฌ ์ hook์ด ๋ชฉ๋ก์ ๋ํ๋๋์ง ํ์ธํฉ๋๋ค. Hook์ ์ ํํ๋ฉด ์ธ๋ถ ์ ๋ณด๊ฐ ํ์๋ฉ๋๋ค: ์ด๋ฒคํธ, matcher, ์ ํ, ์์ค ํ์ผ ๋ฐ ๋ช
๋ น.
hook ํ ์คํธ
Esc๋ฅผ ๋๋ฌ CLI๋ก ๋์๊ฐ๋๋ค. Claude์๊ฒ ๊ถํ์ด ํ์ํ ์์
์ ์ํํ๋๋ก ์์ฒญํ ๋ค์ ํฐ๋ฏธ๋์์ ์ ํํฉ๋๋ค. ๋ฐ์คํฌํฑ ์๋ฆผ์ ๋ฐ์์ผ ํฉ๋๋ค.
/hooks ๋ฉ๋ด๋ ์ฝ๊ธฐ ์ ์ฉ์
๋๋ค. Hooks๋ฅผ ์ถ๊ฐ, ์์ ๋๋ ์ ๊ฑฐํ๋ ค๋ฉด ์ค์ JSON์ ์ง์ ํธ์งํ๊ฑฐ๋ Claude์๊ฒ ๋ณ๊ฒฝ์ ์์ฒญํฉ๋๋ค.
์๋ํํ ์ ์๋ ๊ฒ
Hooks๋ฅผ ์ฌ์ฉํ๋ฉด Claude Code์ ๋ผ์ดํ์ฌ์ดํด์ ์ฃผ์ ์ง์ ์์ ์ฝ๋๋ฅผ ์คํํ ์ ์์ต๋๋ค: ํธ์ง ํ ํ์ผ ํ์ ์ง์ , ์คํ ์ ๋ช ๋ น ์ฐจ๋จ, Claude๊ฐ ์ ๋ ฅ์ด ํ์ํ ๋ ์๋ฆผ ์ ์ก, ์ธ์ ์์ ์ ์ปจํ ์คํธ ์ฃผ์ ๋ฑ. ์ ์ฒด hook ์ด๋ฒคํธ ๋ชฉ๋ก์ Hooks ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
๊ฐ ์์ ์๋ ์ค์ ํ์ผ์ ์ถ๊ฐํ๋ ์ฆ์ ์ฌ์ฉ ๊ฐ๋ฅํ ๊ตฌ์ฑ ๋ธ๋ก์ด ํฌํจ๋์ด ์์ต๋๋ค.
๋ณ๋์ ๋ชจ๋ธ ๊ฒํ ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ธ์
์ ๋ค์ ํผ๋๋ฐฑํ๋ hooks์ ํ๋ก๋์
์์ ๋ security-guidance ํ๋ฌ๊ทธ์ธ์ด Claude Code์ ํตํฉ๋๋ ๋ฐฉ์์ ์ฐธ์กฐํ์ธ์.
Claude๊ฐ ์ ๋ ฅ์ด ํ์ํ ๋ ์๋ฆผ ๋ฐ๊ธฐ
Claude๊ฐ ์์ ์ ์๋ฃํ๊ณ ์ ๋ ฅ์ด ํ์ํ ๋๋ง๋ค ๋ฐ์คํฌํฑ ์๋ฆผ์ ๋ฐ์ผ๋ฏ๋ก ํฐ๋ฏธ๋์ ํ์ธํ์ง ์๊ณ ๋ค๋ฅธ ์์ ์ผ๋ก ์ ํํ ์ ์์ต๋๋ค.
์ด hook์ Claude๊ฐ ์
๋ ฅ ๋๋ ๊ถํ์ ๊ธฐ๋ค๋ฆด ๋ ๋ฐ์ํ๋ Notification ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๊ฐ ํญ์ ํ๋ซํผ์ ๊ธฐ๋ณธ ์๋ฆผ ๋ช
๋ น์ ์ฌ์ฉํฉ๋๋ค. ~/.claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
์๋ฆผ์ด ๋ํ๋์ง ์์ผ๋ฉด
osascript๋ ๊ธฐ๋ณธ ์ ๊ณต Script Editor ์ฑ์ ํตํด ์๋ฆผ์ ๋ผ์ฐํ
ํฉ๋๋ค. Script Editor์ ์๋ฆผ ๊ถํ์ด ์์ผ๋ฉด ๋ช
๋ น์ด ์๋์ผ๋ก ์คํจํ๊ณ macOS๋ ๊ถํ์ ๋ถ์ฌํ๋๋ก ํ๋กฌํํธํ์ง ์์ต๋๋ค. Terminal์์ ์ด๋ฅผ ํ ๋ฒ ์คํํ์ฌ Script Editor๊ฐ ์๋ฆผ ์ค์ ์ ๋ํ๋๋๋ก ํฉ๋๋ค:
osascript -e 'display notification "test"'
์์ง ์๋ฌด๊ฒ๋ ๋ํ๋์ง ์์ต๋๋ค. ์์คํ ์ค์ > ์๋ฆผ์ ์ด๊ณ ๋ชฉ๋ก์์ Script Editor๋ฅผ ์ฐพ์ ๋ค์ ์๋ฆผ ํ์ฉ์ ์ผญ๋๋ค. ๋ช ๋ น์ ๋ค์ ์คํํ์ฌ ํ ์คํธ ์๋ฆผ์ด ๋ํ๋๋์ง ํ์ธํฉ๋๋ค.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
}
]
}
]
}
}
๋น matcher๋ ๋ชจ๋ ์๋ฆผ ์ ํ์์ ๋ฐ์ํฉ๋๋ค. ํน์ ์ด๋ฒคํธ์์๋ง ๋ฐ์ํ๋๋ก ํ๋ ค๋ฉด ๋ค์ ๊ฐ ์ค ํ๋๋ก ์ค์ ํฉ๋๋ค:
| Matcher | ๋ฐ์ ์์ |
|---|---|
permission_prompt |
Claude๊ฐ ๋๊ตฌ ์ฌ์ฉ์ ์น์ธํ๋๋ก ์์ฒญํ ๋ |
idle_prompt |
Claude๊ฐ ์๋ฃ๋๊ณ ๋ค์ ํ๋กฌํํธ๋ฅผ ๊ธฐ๋ค๋ฆด ๋ |
auth_success |
์ธ์ฆ์ด ์๋ฃ๋ ๋ |
elicitation_dialog |
MCP ์๋ฒ๊ฐ ์ ๋ ์์์ ์ด ๋ |
elicitation_complete |
MCP ์ ๋ ์์์ด ์ ์ถ๋๊ฑฐ๋ ๋ซํ ๋ |
elicitation_response |
MCP ์ ๋ ์๋ต์ด ์๋ฒ๋ก ๋ค์ ์ ์ก๋ ๋ |
agent_needs_input |
๋ฐฑ๊ทธ๋ผ์ด๋ ์ธ์ ์ด ์ ๋ ฅ์ ๊ธฐ๋ค๋ฆฌ๊ธฐ ์์ํฉ๋๋ค. agent view๊ฐ ์ด๋ ค ์์ ๋๋ง ๋ฐ์ํฉ๋๋ค |
agent_completed |
๋ฐฑ๊ทธ๋ผ์ด๋ ์ธ์ ์ด ์๋ฃ๋๊ฑฐ๋ ์คํจํฉ๋๋ค. agent view๊ฐ ์ด๋ ค ์์ ๋๋ง ๋ฐ์ํฉ๋๋ค |
agent_needs_input ๋ฐ agent_completed matcher๋ Claude Code v2.1.198 ์ด์์ด ํ์ํฉ๋๋ค.
/hooks๋ฅผ ์
๋ ฅํ๊ณ Notification์ ์ ํํ์ฌ hook์ด ๋ฑ๋ก๋์๋์ง ํ์ธํฉ๋๋ค. ์ ์ฒด ์ด๋ฒคํธ ์คํค๋ง๋ Notification ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
ํธ์ง ํ ์ฝ๋ ์๋ ํ์ ์ง์
Claude๊ฐ ํธ์งํ๋ ๋ชจ๋ ํ์ผ์์ Prettier๋ฅผ ์๋์ผ๋ก ์คํํ์ฌ ์๋ ๊ฐ์ ์์ด ํ์์ด ์ผ๊ด๋๊ฒ ์ ์ง๋๋๋ก ํฉ๋๋ค.
์ด hook์ PostToolUse ์ด๋ฒคํธ๋ฅผ Edit|Write matcher์ ํจ๊ป ์ฌ์ฉํ๋ฏ๋ก ํ์ผ ํธ์ง ๋๊ตฌ ํ์๋ง ์คํ๋ฉ๋๋ค. ๋ช
๋ น์ jq๋ฅผ ์ฌ์ฉํ์ฌ ํธ์ง๋ ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ถ์ถํ๊ณ Prettier์ ์ ๋ฌํฉ๋๋ค. ํ๋ก์ ํธ ๋ฃจํธ์ .claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Claude Code v2.1.191 ์ด์์์๋ matcher๋ฅผ Edit,Write๋ก๋ ์์ฑํ ์ ์์ต๋๋ค. ์ด๋ฌํ ๋ฒ์ ์์๋ |์ ,์ด ๋๊ตฌ ์ด๋ฆ matcher์ ์ํธ ๊ตํ ๊ฐ๋ฅํ ๋ชฉ๋ก ๊ตฌ๋ถ์์ด๊ธฐ ๋๋ฌธ์
๋๋ค.
์ด ํ์ด์ง์ Bash ์์ ๋ JSON ๊ตฌ๋ฌธ ๋ถ์์ ์ํด jq๋ฅผ ์ฌ์ฉํฉ๋๋ค. macOS์์ brew install jq๋ก, Debian ๋ฐ Ubuntu์์ apt-get install jq๋ก ์ค์นํ๊ฑฐ๋ jq ๋ค์ด๋ก๋๋ฅผ ์ฐธ์กฐํ์ธ์.
๋ณดํธ๋ ํ์ผ์ ๋ํ ํธ์ง ์ฐจ๋จ
Claude๊ฐ .env, package-lock.json ๋๋ .git/์ ๋ชจ๋ ํญ๋ชฉ๊ณผ ๊ฐ์ ๋ฏผ๊ฐํ ํ์ผ์ ์์ ํ์ง ๋ชปํ๋๋ก ๋ฐฉ์งํฉ๋๋ค. Claude๋ ํธ์ง์ด ์ฐจ๋จ๋ ์ด์ ๋ฅผ ์ค๋ช
ํ๋ ํผ๋๋ฐฑ์ ๋ฐ์ผ๋ฏ๋ก ์ ๊ทผ ๋ฐฉ์์ ์กฐ์ ํ ์ ์์ต๋๋ค.
์ด ์์ ๋ hook์ด ํธ์ถํ๋ ๋ณ๋์ ์คํฌ๋ฆฝํธ ํ์ผ์ ์ฌ์ฉํฉ๋๋ค. ์คํฌ๋ฆฝํธ๋ ๋์ ํ์ผ ๊ฒฝ๋ก๋ฅผ ๋ณดํธ๋ ํจํด ๋ชฉ๋ก๊ณผ ๋น๊ตํ๊ณ ์ข ๋ฃ ์ฝ๋ 2๋ก ์ข ๋ฃํ์ฌ ํธ์ง์ ์ฐจ๋จํฉ๋๋ค.
hook ์คํฌ๋ฆฝํธ ๋ง๋ค๊ธฐ
์ด๋ฅผ .claude/hooks/protect-files.sh์ ์ ์ฅํฉ๋๋ค:
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
macOS ๋ฐ Linux์์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค๊ธฐ
Hook ์คํฌ๋ฆฝํธ๋ Claude Code๊ฐ ์คํํ๋ ค๋ฉด ์คํ ๊ฐ๋ฅํด์ผ ํฉ๋๋ค:
chmod +x .claude/hooks/protect-files.sh
hook ๋ฑ๋ก
๋ชจ๋ Edit ๋๋ Write ๋๊ตฌ ํธ์ถ ์ ์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ PreToolUse hook์ .claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
์์ถ ํ ์ปจํ ์คํธ ๋ค์ ์ฃผ์
Claude์ ์ปจํ
์คํธ ์๋์ฐ๊ฐ ๊ฐ๋ ์ฐจ๋ฉด ์์ถ์ ๋ํ๋ฅผ ์์ฝํ์ฌ ๊ณต๊ฐ์ ํ๋ณดํฉ๋๋ค. ์ด๋ ์ค์ํ ์ธ๋ถ ์ ๋ณด๋ฅผ ์์ ์ ์์ต๋๋ค. compact matcher์ ํจ๊ป SessionStart hook์ ์ฌ์ฉํ์ฌ ๋ชจ๋ ์์ถ ํ ์ค์ํ ์ปจํ
์คํธ๋ฅผ ๋ค์ ์ฃผ์
ํฉ๋๋ค.
๋ช
๋ น์ด stdout์ ์ฐ๋ ๋ชจ๋ ํ
์คํธ๋ Claude์ ์ปจํ
์คํธ์ ์ถ๊ฐ๋ฉ๋๋ค. ์ด ์์ ๋ Claude์๊ฒ ํ๋ก์ ํธ ๊ท์น๊ณผ ์ต๊ทผ ์์
์ ์๊ธฐ์ํต๋๋ค. ํ๋ก์ ํธ ๋ฃจํธ์ .claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
echo๋ฅผ git log --oneline -5์ ๊ฐ์ด ๋์ ์ถ๋ ฅ์ ์์ฑํ๋ ๋ชจ๋ ๋ช
๋ น์ผ๋ก ๋ฐ๊ฟ ์ ์์ต๋๋ค. ๋ชจ๋ ์ธ์
์์ ์ ์ปจํ
์คํธ๋ฅผ ์ฃผ์
ํ๋ ค๋ฉด CLAUDE.md ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์. ํ๊ฒฝ ๋ณ์๋ CLAUDE_ENV_FILE์ ์ฐธ์กฐํ์ธ์.
๊ตฌ์ฑ ๋ณ๊ฒฝ ๊ฐ์ฌ
์ธ์
์ค์ ์ค์ ๋๋ skills ํ์ผ์ด ๋ณ๊ฒฝ๋ ๋๋ฅผ ์ถ์ ํฉ๋๋ค. ConfigChange ์ด๋ฒคํธ๋ ์ธ๋ถ ํ๋ก์ธ์ค ๋๋ ํธ์ง๊ธฐ๊ฐ ๊ตฌ์ฑ ํ์ผ์ ์์ ํ ๋ ๋ฐ์ํ๋ฏ๋ก ๊ท์ ์ค์๋ฅผ ์ํด ๋ณ๊ฒฝ ์ฌํญ์ ๊ธฐ๋กํ๊ฑฐ๋ ๋ฌด๋จ ์์ ์ ์ฐจ๋จํ ์ ์์ต๋๋ค.
์ด ์์ ๋ ๊ฐ ๋ณ๊ฒฝ์ ๊ฐ์ฌ ๋ก๊ทธ์ ์ถ๊ฐํฉ๋๋ค. ~/.claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}
Matcher๋ ๊ตฌ์ฑ ์ ํ์ผ๋ก ํํฐ๋งํฉ๋๋ค: user_settings, project_settings, local_settings, policy_settings ๋๋ skills. ๋ณ๊ฒฝ์ด ์ ์ฉ๋์ง ์๋๋ก ์ฐจ๋จํ๋ ค๋ฉด ์ข
๋ฃ ์ฝ๋ 2๋ก ์ข
๋ฃํ๊ฑฐ๋ {"decision": "block"}์ ๋ฐํํฉ๋๋ค. ์ ์ฒด ์
๋ ฅ ์คํค๋ง๋ ConfigChange ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
๋๋ ํ ๋ฆฌ ๋๋ ํ์ผ์ด ๋ณ๊ฒฝ๋ ๋ ํ๊ฒฝ ๋ค์ ๋ก๋
์ผ๋ถ ํ๋ก์ ํธ๋ ์ด๋ ๋๋ ํ ๋ฆฌ์ ์๋์ง์ ๋ฐ๋ผ ๋ค๋ฅธ ํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํฉ๋๋ค. direnv์ ๊ฐ์ ๋๊ตฌ๋ ์ ธ์์ ์๋์ผ๋ก ์ด๋ฅผ ์ํํ์ง๋ง Claude์ Bash ๋๊ตฌ๋ ์์ฒด์ ์ผ๋ก ํด๋น ๋ณ๊ฒฝ ์ฌํญ์ ์ ํํ์ง ์์ต๋๋ค.
SessionStart hook์ CwdChanged hook๊ณผ ์์ผ๋ก ์ฌ์ฉํ๋ฉด ์ด๋ฅผ ํด๊ฒฐํฉ๋๋ค. SessionStart๋ ์์ํ ๋๋ ํ ๋ฆฌ์ ๋ํ ๋ณ์๋ฅผ ๋ก๋ํ๊ณ CwdChanged๋ Claude๊ฐ ๋๋ ํ ๋ฆฌ๋ฅผ ๋ณ๊ฒฝํ ๋๋ง๋ค ์ด๋ฅผ ๋ค์ ๋ก๋ํฉ๋๋ค. ๋ ๋ค CLAUDE_ENV_FILE์ ์ฐ๋ฉฐ, Claude Code๋ ๊ฐ Bash ๋ช
๋ น ์ ์ ์ด๋ฅผ ์คํฌ๋ฆฝํธ ํ๋ฆฌ์ฐ๋ธ๋ก ์คํํฉ๋๋ค. ~/.claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
],
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
direnv allow๋ฅผ .envrc๊ฐ ์๋ ๊ฐ ๋๋ ํ ๋ฆฌ์์ ํ ๋ฒ ์คํํ์ฌ direnv๊ฐ ์ด๋ฅผ ๋ก๋ํ ์ ์๋๋ก ํ์ฉํฉ๋๋ค. direnv ๋์ devbox ๋๋ nix๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ direnv export bash ๋์ devbox shellenv ๋๋ devbox global shellenv๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ผํ ํจํด์ด ์๋ํฉ๋๋ค.
๋ชจ๋ ๋๋ ํ ๋ฆฌ ๋ณ๊ฒฝ์ด ์๋ ํน์ ํ์ผ์ ๋ฐ์ํ๋ ค๋ฉด FileChanged๋ฅผ matcher์ ํจ๊ป ์ฌ์ฉํ์ฌ ๊ฐ์ํ ํ์ผ ์ด๋ฆ์ ๋์ดํฉ๋๋ค (ํ์ดํ๋ก ๊ตฌ๋ถ). ๊ฐ์ ๋ชฉ๋ก์ ๊ตฌ์ฑํ๊ธฐ ์ํด ์ด ๊ฐ์ ์ ๊ท์์ผ๋ก ํ๊ฐ๋์ง ์๊ณ ๋ฆฌํฐ๋ด ํ์ผ ์ด๋ฆ์ผ๋ก ๋ถํ ๋ฉ๋๋ค. FileChanged๋ฅผ ์ฐธ์กฐํ์ฌ ํ์ผ์ด ๋ณ๊ฒฝ๋ ๋ ์ด๋ค hook ๊ทธ๋ฃน์ด ์คํ๋๋์ง ํํฐ๋งํ๋ ๋ฐฉ๋ฒ๋ ํ์ธํ์ธ์. ์ด ์์ ๋ ์์
๋๋ ํ ๋ฆฌ์์ .envrc ๋ฐ .env๋ฅผ ๊ฐ์ํฉ๋๋ค:
{
"hooks": {
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
์
๋ ฅ ์คํค๋ง, watchPaths ์ถ๋ ฅ ๋ฐ CLAUDE_ENV_FILE ์ธ๋ถ ์ ๋ณด๋ CwdChanged ๋ฐ FileChanged ์ฐธ์กฐ ํญ๋ชฉ์ ์ฐธ์กฐํ์ธ์.
ํน์ ๊ถํ ํ๋กฌํํธ ์๋ ์น์ธ
ํญ์ ํ์ฉํ๋ ๋๊ตฌ ํธ์ถ์ ๋ํ ์น์ธ ๋ํ๋ฅผ ๊ฑด๋๋๋๋ค. ์ด ์์ ๋ ExitPlanMode๋ฅผ ์๋ ์น์ธํฉ๋๋ค. ์ด๋ Claude๊ฐ ๊ณํ ์ ์๋ฅผ ์๋ฃํ๊ณ ์งํ์ ์์ฒญํ ๋ ํธ์ถํ๋ ๋๊ตฌ์ด๋ฏ๋ก ๊ณํ์ด ์ค๋น๋ ๋๋ง๋ค ํ๋กฌํํธ๊ฐ ํ์๋์ง ์์ต๋๋ค.
์์ ์ข
๋ฃ ์ฝ๋ ์์ ์ ๋ฌ๋ฆฌ ์๋ ์น์ธ์ ์ํด์๋ hook์ด JSON ๊ฒฐ์ ์ stdout์ ์์ฑํด์ผ ํฉ๋๋ค. PermissionRequest hook์ Claude Code๊ฐ ๊ถํ ๋ํ๋ฅผ ํ์ํ๋ ค๊ณ ํ ๋ ๋ฐ์ํ๋ฉฐ "behavior": "allow"๋ฅผ ๋ฐํํ๋ฉด ์ฌ์ฉ์ ๋์ ๋ต๋ณํฉ๋๋ค.
Matcher๋ hook์ ExitPlanMode๋ก๋ง ๋ฒ์๋ฅผ ์ง์ ํ๋ฏ๋ก ๋ค๋ฅธ ํ๋กฌํํธ๋ ์ํฅ์ ๋ฐ์ง ์์ต๋๋ค. ~/.claude/settings.json์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
Hook์ด ์น์ธํ๋ฉด Claude Code๋ ๊ณํ ๋ชจ๋๋ฅผ ์ข ๋ฃํ๊ณ ๊ณํ ๋ชจ๋์ ๋ค์ด๊ฐ๊ธฐ ์ ์ ํ์ฑํ๋์๋ ๊ถํ ๋ชจ๋๋ฅผ ๋ณต์ํฉ๋๋ค. ํธ๋์คํฌ๋ฆฝํธ๋ ๋ํ๊ฐ ๋ํ๋ฌ์ ์์น์ "Allowed by PermissionRequest hook"์ ํ์ํฉ๋๋ค. Hook ๊ฒฝ๋ก๋ ํญ์ ํ์ฌ ๋ํ๋ฅผ ์ ์งํฉ๋๋ค: ๋ํ๊ฐ ํ ์ ์๋ ๋ฐฉ์์ผ๋ก ์ปจํ ์คํธ๋ฅผ ์ง์ฐ๊ณ ์๋ก์ด ๊ตฌํ ์ธ์ ์ ์์ํ ์ ์์ต๋๋ค.
๋์ ํน์ ๊ถํ ๋ชจ๋๋ฅผ ์ค์ ํ๋ ค๋ฉด hook์ ์ถ๋ ฅ์ setMode ํญ๋ชฉ์ด ์๋ updatedPermissions ๋ฐฐ์ด์ด ํฌํจ๋ ์ ์์ต๋๋ค. mode ๊ฐ์ default, acceptEdits ๋๋ bypassPermissions๊ณผ ๊ฐ์ ๋ชจ๋ ๊ถํ ๋ชจ๋์ด๋ฉฐ destination: "session"์ ํ์ฌ ์ธ์
์๋ง ์ ์ฉํฉ๋๋ค.
bypassPermissions์ ์ธ์
์ด ์ด๋ฏธ bypass ๋ชจ๋๋ก ์์๋ ๊ฒฝ์ฐ์๋ง ์ ์ฉ๋ฉ๋๋ค: --dangerously-skip-permissions, --permission-mode bypassPermissions, --allow-dangerously-skip-permissions ๋๋ ์ค์ ์ permissions.defaultMode: "bypassPermissions"์ด๋ฉฐ permissions.disableBypassPermissionsMode๋ก ๋นํ์ฑํ๋์ง ์์ต๋๋ค. ์ด๋ defaultMode๋ก ์ ๋ ์ง์๋์ง ์์ต๋๋ค.
์ธ์
์ acceptEdits๋ก ์ ํํ๋ ค๋ฉด hook์ด ์ด JSON์ stdout์ ์์ฑํฉ๋๋ค:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": [
{ "type": "setMode", "mode": "acceptEdits", "destination": "session" }
]
}
}
}
Matcher๋ฅผ ๊ฐ๋ฅํ ํ ์ข๊ฒ ์ ์งํฉ๋๋ค. .*์ ์ผ์นํ๊ฑฐ๋ matcher๋ฅผ ๋น์๋๋ฉด ํ์ผ ์ฐ๊ธฐ ๋ฐ ์
ธ ๋ช
๋ น์ ํฌํจํ ๋ชจ๋ ๊ถํ ํ๋กฌํํธ๋ฅผ ์๋ ์น์ธํฉ๋๋ค. ์ ์ฒด ๊ฒฐ์ ํ๋ ์งํฉ์ PermissionRequest ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
Hooks ์๋ ๋ฐฉ์
Hook ์ด๋ฒคํธ๋ Claude Code์ ๋ผ์ดํ์ฌ์ดํด์ ํน์ ์ง์ ์์ ๋ฐ์ํฉ๋๋ค. ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ์ผ์นํ๋ ๋ชจ๋ hooks๊ฐ ๋ณ๋ ฌ๋ก ์คํ๋๊ณ ๋์ผํ hook ๋ช ๋ น์ ์๋์ผ๋ก ์ค๋ณต ์ ๊ฑฐ๋ฉ๋๋ค. ์๋ ํ๋ ๊ฐ ์ด๋ฒคํธ์ ๋ฐ์ ์๊ธฐ๋ฅผ ๋ณด์ฌ์ค๋๋ค:
| Event | When it fires |
|---|---|
SessionStart |
When a session begins or resumes |
Setup |
When you start Claude Code with --init-only, or with --init or --maintenance in -p mode. For one-time preparation in CI or scripts |
UserPromptSubmit |
When you submit a prompt, before Claude processes it |
UserPromptExpansion |
When a user-typed command expands into a prompt, before it reaches Claude. Can block the expansion |
PreToolUse |
Before a tool call executes. Can block it |
PermissionRequest |
When a permission dialog appears |
PermissionDenied |
When a tool call is denied by the auto mode classifier. Return {retry: true} to tell the model it may retry the denied tool call |
PostToolUse |
After a tool call succeeds |
PostToolUseFailure |
After a tool call fails |
PostToolBatch |
After a full batch of parallel tool calls resolves, before the next model call |
Notification |
When Claude Code sends a notification |
MessageDisplay |
While assistant message text is displayed |
SubagentStart |
When a subagent is spawned |
SubagentStop |
When a subagent finishes |
TaskCreated |
When a task is being created via TaskCreate |
TaskCompleted |
When a task is being marked as completed |
Stop |
When Claude finishes responding |
StopFailure |
When the turn ends due to an API error. Output and exit code are ignored |
TeammateIdle |
When an agent team teammate is about to go idle |
InstructionsLoaded |
When a CLAUDE.md or .claude/rules/*.md file is loaded into context. Fires at session start and when files are lazily loaded during a session |
ConfigChange |
When a configuration file changes during a session |
CwdChanged |
When the working directory changes, for example when Claude executes a cd command. Useful for reactive environment management with tools like direnv |
FileChanged |
When a watched file changes on disk. The matcher field specifies which filenames to watch |
WorktreeCreate |
When a worktree is being created via --worktree, isolation: "worktree", or for a background session. Replaces default git behavior |
WorktreeRemove |
When a worktree is being removed at session exit, when a subagent finishes, or when you delete a background session |
PreCompact |
Before context compaction |
PostCompact |
After context compaction completes |
Elicitation |
When an MCP server requests user input during a tool call |
ElicitationResult |
After a user responds to an MCP elicitation, before the response is sent back to the server |
SessionEnd |
When a session terminates |
๊ฐ hook์๋ ์คํ ๋ฐฉ์์ ๊ฒฐ์ ํ๋ type์ด ์์ต๋๋ค. ๋๋ถ๋ถ์ hooks๋ "type": "command"๋ฅผ ์ฌ์ฉํ์ฌ ์
ธ ๋ช
๋ น์ ์คํํฉ๋๋ค. ๋ค ๊ฐ์ง ๋ค๋ฅธ ์ ํ์ ์ฌ์ฉํ ์ ์์ต๋๋ค:
"type": "http": ์ด๋ฒคํธ ๋ฐ์ดํฐ๋ฅผ URL์ POSTํฉ๋๋ค. HTTP hooks๋ฅผ ์ฐธ์กฐํ์ธ์."type": "mcp_tool": ์ด๋ฏธ ์ฐ๊ฒฐ๋ MCP ์๋ฒ์์ ๋๊ตฌ๋ฅผ ํธ์ถํฉ๋๋ค. MCP tool hooks๋ฅผ ์ฐธ์กฐํ์ธ์."type": "prompt": ๋จ์ผ ํด LLM ํ๊ฐ์ ๋๋ค. ํ๋กฌํํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฐธ์กฐํ์ธ์."type": "agent": ๋๊ตฌ ์ก์ธ์ค๋ฅผ ํตํ ๋ค์ค ํด ๊ฒ์ฆ์ ๋๋ค. ์์ด์ ํธ hooks๋ ์คํ์ ์ด๋ฉฐ ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค. ์์ด์ ํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฐธ์กฐํ์ธ์.
์ฌ๋ฌ hooks์ ๊ฒฐ๊ณผ ๊ฒฐํฉ
์ฌ๋ฌ hooks๊ฐ ๋์ผํ ์ด๋ฒคํธ์ ์ผ์นํ๋ฉด ๋ชจ๋ hook์ ๋ช
๋ น์ด ์๋ฃ๋ ๋๊น์ง ์คํ๋ ํ Claude Code๊ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ณํฉํฉ๋๋ค. ํ๋์ hook์ด deny๋ฅผ ๋ฐํํ๋ ๊ฒ์ด ํ์ hooks์ ์คํ์ ์ค์งํ์ง ์์ต๋๋ค. ํ hook์ deny๊ฐ ๋ค๋ฅธ hook์ ๋ถ์์ฉ์ ์ต์ ํ๋ ๊ฒ์ ์์กดํ์ง ๋ง์ธ์.
๋ชจ๋ ์ผ์นํ๋ hooks๊ฐ ์๋ฃ๋ ํ Claude Code๋ ์ถ๋ ฅ์ ๊ฒฐํฉํฉ๋๋ค. PreToolUse ๊ถํ ๊ฒฐ์ ์ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ ํ์ ์ธ ๋ต๋ณ์ด ์ฐ์ ํฉ๋๋ค: deny, defer, ask, allow ์์์
๋๋ค. additionalContext์ ํ
์คํธ๋ ๋ชจ๋ hook์์ ์ ์ง๋๊ณ Claude์ ํจ๊ป ์ ๋ฌ๋ฉ๋๋ค.
์๋ ์์ ๋ Bash์ ๋ ๊ฐ์ PreToolUse hooks๋ฅผ ๋ฑ๋กํฉ๋๋ค. ์ฒซ ๋ฒ์งธ๋ ๋ชจ๋ ๋ช
๋ น์ ๋ก๊ทธ ํ์ผ์ ์ถ๊ฐํ๊ณ 0์ผ๋ก ์ข
๋ฃํฉ๋๋ค. ๋ ๋ฒ์งธ๋ ๋ช
๋ น์ rm -rf๊ฐ ํฌํจ๋์ด ์์ ๋ ๊ฑฐ๋ถํ๊ธฐ ์ํด 2๋ก ์ข
๋ฃํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํฉ๋๋ค:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}
Claude๊ฐ rm -rf /tmp/build๋ฅผ ์คํํ๋ ค๊ณ ํ ๋ ๋ hooks ๋ชจ๋ ๋ณ๋ ฌ๋ก ์คํ๋ฉ๋๋ค. ๋ก๊น
hook์ ๋ช
๋ น์ ~/.claude/bash.log์ ์ฐ๊ณ 0์ผ๋ก ์ข
๋ฃํ์ฌ ๊ฒฐ์ ์ด ์์์ ๋ณด๊ณ ํฉ๋๋ค. ๋ณด์ ๊ท์น hook์ 2๋ก ์ข
๋ฃํ์ฌ ๋๊ตฌ ํธ์ถ์ ๊ฑฐ๋ถํฉ๋๋ค. ๊ฑฐ๋ถ๊ฐ ์ฐ์ ํ๋ฏ๋ก Claude Code๋ ๋ช
๋ น์ ์ฐจ๋จํ๊ณ Claude์๊ฒ ๋ณด์ ๊ท์น์ stderr๋ฅผ ํ์ํฉ๋๋ค. ๋ก๊น
hook์ด ์ด๋ฏธ ์คํ๋์๊ธฐ ๋๋ฌธ์ ๋ก๊ทธ ํญ๋ชฉ์ ์ฌ์ ํ ๊ธฐ๋ก๋ฉ๋๋ค.
์ ๋ ฅ ์ฝ๊ธฐ ๋ฐ ์ถ๋ ฅ ๋ฐํ
Hooks๋ stdin, stdout, stderr ๋ฐ ์ข ๋ฃ ์ฝ๋๋ฅผ ํตํด Claude Code์ ํต์ ํฉ๋๋ค. ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด Claude Code๋ ์ด๋ฒคํธ๋ณ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ์คํฌ๋ฆฝํธ์ stdin์ ์ ๋ฌํฉ๋๋ค. ์คํฌ๋ฆฝํธ๋ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ ์์ ์ ์ํํ ๋ค์ ์ข ๋ฃ ์ฝ๋๋ฅผ ํตํด Claude Code์ ๋ค์ ์์ ์ ์๋ฆฝ๋๋ค.
Hook ์ ๋ ฅ
๋ชจ๋ ์ด๋ฒคํธ์๋ session_id ๋ฐ cwd์ ๊ฐ์ ๊ณตํต ํ๋๊ฐ ํฌํจ๋์ง๋ง ๊ฐ ์ด๋ฒคํธ ์ ํ์ ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํฉ๋๋ค. ์๋ฅผ ๋ค์ด Claude๊ฐ Bash ๋ช
๋ น์ ์คํํ ๋ PreToolUse hook์ stdin์์ ๋ค์๊ณผ ๊ฐ์ ๊ฒ์ ๋ฐ์ต๋๋ค:
{
"session_id": "abc123", // ์ด ์ธ์
์ ๊ณ ์ ID
"cwd": "/Users/sarah/myproject", // ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋์ ์์
๋๋ ํ ๋ฆฌ
"hook_event_name": "PreToolUse", // ์ด hook์ ํธ๋ฆฌ๊ฑฐํ ์ด๋ฒคํธ
"tool_name": "Bash", // Claude๊ฐ ์ฌ์ฉํ๋ ค๋ ๋๊ตฌ
"tool_input": { // Claude๊ฐ ๋๊ตฌ์ ์ ๋ฌํ ์ธ์
"command": "npm test" // Bash์ ๊ฒฝ์ฐ ์ด๊ฒ์ ์
ธ ๋ช
๋ น์
๋๋ค
}
}
์คํฌ๋ฆฝํธ๋ ํด๋น JSON์ ๊ตฌ๋ฌธ ๋ถ์ํ๊ณ ํด๋น ํ๋์ ๋ํด ์๋ํ ์ ์์ต๋๋ค. UserPromptSubmit hooks๋ prompt ํ
์คํธ๋ฅผ ๋์ ๋ฐ๊ณ , SessionStart hooks๋ source (startup, resume, clear, compact)๋ฅผ ๋ฐ์ผ๋ฉฐ, ๋ฑ๋ฑ์
๋๋ค. ๊ณต์ ํ๋๋ ์ฐธ์กฐ์ ๊ณตํต ์
๋ ฅ ํ๋๋ฅผ ์ฐธ์กฐํ๊ณ ๊ฐ ์ด๋ฒคํธ๋ณ ์น์
์์ ์ด๋ฒคํธ๋ณ ์คํค๋ง๋ฅผ ์ฐธ์กฐํ์ธ์.
Hook ์ถ๋ ฅ
์คํฌ๋ฆฝํธ๋ stdout ๋๋ stderr์ ์ฐ๊ณ ํน์ ์ฝ๋๋ก ์ข
๋ฃํ์ฌ Claude Code์ ๋ค์ ์์
์ ์๋ฆฝ๋๋ค. ์๋ฅผ ๋ค์ด ๋ช
๋ น์ ์ฐจ๋จํ๋ ค๋ PreToolUse hook:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr๋ Claude์ ํผ๋๋ฐฑ์ด ๋ฉ๋๋ค
exit 2 # exit 2 = ์์
์ฐจ๋จ
fi
exit 0 # exit 0 = ์งํ ํ์ฉ
์ข ๋ฃ ์ฝ๋๋ ๋ค์์ ์ผ์ด๋ ์ผ์ ๊ฒฐ์ ํฉ๋๋ค:
- Exit 0: hook์ ์ด์๋ฅผ ์ ๊ธฐํ์ง ์์ผ๋ฉฐ ์์
์ด ์ ์์ ์ผ๋ก ์งํ๋ฉ๋๋ค.
PreToolUsehook์ ๊ฒฝ์ฐ ์ด๊ฒ์ ๋๊ตฌ ํธ์ถ์ ์น์ธํ์ง ์์ต๋๋ค: ์ ์์ ์ธ ๊ถํ ํ๋ฆ์ด ์ฌ์ ํ ์ ์ฉ๋ฉ๋๋ค.UserPromptSubmit,UserPromptExpansion๋ฐSessionStarthooks์ ๊ฒฝ์ฐ stdout์ ์ฐ๋ ๋ชจ๋ ๊ฒ์ด Claude์ ์ปจํ ์คํธ์ ์ถ๊ฐ๋ฉ๋๋ค. - Exit 2: ์์
์ด ์ฐจ๋จ๋ฉ๋๋ค. stderr์ ์ด์ ๋ฅผ ์ฐ๋ฉด Claude๊ฐ ํผ๋๋ฐฑ์ผ๋ก ๋ฐ์ ์กฐ์ ํ ์ ์์ต๋๋ค. ์ผ๋ถ ์ด๋ฒคํธ๋ ์ฐจ๋จ๋ ์ ์์ต๋๋ค:
SessionStart,Setup,Notification๋ฐ ๊ธฐํ์ ๊ฒฝ์ฐ exit 2๋ stderr๋ฅผ ์ฌ์ฉ์์๊ฒ ํ์ํ๊ณ ์คํ์ด ๊ณ์๋ฉ๋๋ค. ์ด๋ฒคํธ๋ณ ์ ์ฒด ๋ชฉ๋ก์ ์ด๋ฒคํธ๋ณ exit ์ฝ๋ 2 ๋์์ ์ฐธ์กฐํ์ธ์. - ๋ค๋ฅธ ์ข
๋ฃ ์ฝ๋: ์์
์ด ์งํ๋ฉ๋๋ค. ํธ๋์คํฌ๋ฆฝํธ๋
<hook name> hook error๊ณต์ง๋ฅผ ํ์ํ ํ stderr์ ์ฒซ ๋ฒ์งธ ์ค์ ํ์ํฉ๋๋ค. ์ ์ฒด stderr๋ ๋๋ฒ๊ทธ ๋ก๊ทธ๋ก ์ด๋ํฉ๋๋ค.
๊ตฌ์กฐํ๋ JSON ์ถ๋ ฅ
์ข ๋ฃ ์ฝ๋๋ ์ฐจ๋จํ๊ฑฐ๋ ์นจ๋ฌตํ๋ ๊ฒ๋ง ํ์ฉํฉ๋๋ค. ๋ ๋ง์ ์ ์ด๋ฅผ ์ํด exit 0์ ํ๊ณ stdout์ JSON ๊ฐ์ฒด๋ฅผ ์ธ์ํฉ๋๋ค.
Exit 2๋ฅผ ์ฌ์ฉํ์ฌ stderr ๋ฉ์์ง๋ก ์ฐจ๋จํ๊ฑฐ๋ exit 0์ ์ฌ์ฉํ์ฌ ๊ตฌ์กฐํ๋ ์ ์ด๋ฅผ ์ํด JSON์ ์ฌ์ฉํฉ๋๋ค. ํผํฉํ์ง ๋ง์ธ์: Claude Code๋ exit 2์ผ ๋ JSON์ ๋ฌด์ํฉ๋๋ค.
์๋ฅผ ๋ค์ด PreToolUse hook์ ๋๊ตฌ ํธ์ถ์ ๊ฑฐ๋ถํ๊ณ ์ด์ ๋ฅผ ์๋ฆฌ๊ฑฐ๋ ์ฌ์ฉ์ ์น์ธ์ ์ํด ์์ค์ปฌ๋ ์ด์
ํ ์ ์์ต๋๋ค:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
"deny"๋ฅผ ์ฌ์ฉํ๋ฉด Claude Code๋ ๋๊ตฌ ํธ์ถ์ ์ทจ์ํ๊ณ permissionDecisionReason์ Claude์๊ฒ ํผ๋๋ฐฑ์ผ๋ก ์ ๋ฌํฉ๋๋ค. ์ด permissionDecision ๊ฐ์ PreToolUse์๋ง ํด๋นํฉ๋๋ค:
"allow": ๋ํํ ๊ถํ ํ๋กฌํํธ๋ฅผ ๊ฑด๋๋๋๋ค. ๊ฑฐ๋ถ ๋ฐ ์์ฒญ ๊ท์น์ ํฌํจํ ์ํฐํ๋ผ์ด์ฆ ๊ด๋ฆฌํ ๊ฑฐ๋ถ ๋ชฉ๋ก์ ์ฌ์ ํ ์ ์ฉ๋ฉ๋๋ค. ์กฐ์ง์ดask๋ก ์ค์ ํ ์ปค๋ฅํฐ ๋๊ตฌ ๋ฐrequiresUserInteraction์ผ๋ก ํ์๋ MCP ๋๊ตฌ์ ๋ํ ํ๋กฌํํธ๋ ๋ง์ฐฌ๊ฐ์ง์ ๋๋ค."deny": ๋๊ตฌ ํธ์ถ์ ์ทจ์ํ๊ณ ์ด์ ๋ฅผ Claude์ ์ ์กํฉ๋๋ค"ask": ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ์์๊ฒ ๊ถํ ํ๋กฌํํธ๋ฅผ ํ์ํฉ๋๋ค
๋ค ๋ฒ์งธ ๊ฐ์ธ "defer"๋ -p ํ๋๊ทธ๊ฐ ์๋ ๋น๋ํํ ๋ชจ๋์์ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค. ๋๊ตฌ ํธ์ถ์ ๋ณด์กดํ์ฌ ํ๋ก์ธ์ค๋ฅผ ์ข
๋ฃํ๋ฏ๋ก Agent SDK ๋ํผ๊ฐ ์
๋ ฅ์ ์์งํ๊ณ ์ฌ๊ฐํ ์ ์์ต๋๋ค. ์ฐธ์กฐ์ ๋์ค์ ๋๊ตฌ ํธ์ถ ์ฐ๊ธฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
"allow"๋ฅผ ๋ฐํํ๋ฉด ๋ํํ ํ๋กฌํํธ๋ฅผ ๊ฑด๋๋๋๋ค. ํ์ง๋ง ๊ถํ ๊ท์น์ ์ฌ์ ์ํ์ง ์์ต๋๋ค. ๊ฑฐ๋ถ ๊ท์น์ด ๋๊ตฌ ํธ์ถ๊ณผ ์ผ์นํ๋ฉด hook์ด "allow"๋ฅผ ๋ฐํํ๋๋ผ๋ ํธ์ถ์ด ์ฐจ๋จ๋ฉ๋๋ค. ์์ฒญ ๊ท์น์ด ์ผ์นํ๋ฉด ์ฌ์ฉ์๊ฐ ์ฌ์ ํ ํ๋กฌํํธ๋ฉ๋๋ค. ์กฐ์ง์ด ask๋ก ์ค์ ํ ์ปค๋ฅํฐ ๋๊ตฌ ๋ฐ requiresUserInteraction์ผ๋ก ํ์๋ MCP ๋๊ตฌ๋ ๋ง์ฐฌ๊ฐ์ง์
๋๋ค. ์ด๋ ๊ด๋ฆฌํ ์ค์ ์ ํฌํจํ ๋ชจ๋ ์ค์ ๋ฒ์์ ๊ฑฐ๋ถ ๊ท์น์ด hook ์น์ธ๋ณด๋ค ํญ์ ์ฐ์ ํ๋ค๋ ์๋ฏธ์
๋๋ค.
๋ค๋ฅธ ์ด๋ฒคํธ๋ ๋ค๋ฅธ ๊ฒฐ์ ํจํด์ ์ฌ์ฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด PostToolUse ๋ฐ Stop hooks๋ ์ต์์ decision: "block" ํ๋๋ฅผ ์ฌ์ฉํ๊ณ PermissionRequest๋ hookSpecificOutput.decision.behavior๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ด๋ฒคํธ๋ณ ์ ์ฒด ๋ถ์์ ์ฐธ์กฐ์ ์์ฝ ํ๋ฅผ ์ฐธ์กฐํ์ธ์.
UserPromptSubmit hooks์ ๊ฒฝ์ฐ hookSpecificOutput.additionalContext๋ฅผ ๋์ ์ฌ์ฉํ์ฌ Claude์ ์ปจํ
์คํธ์ ํ
์คํธ๋ฅผ ์ฃผ์
ํฉ๋๋ค. additionalContext๋ฅผ hookSpecificOutput ๋ด์ ์ค์ฒฉํ์ธ์. JSON์ ์ต์์ ์์ค์ ๋ฐฐ์นํ๋ฉด Claude Code๋ ์ด๋ฅผ ์๋์ผ๋ก ๋ฌด์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด ์ด ์ถ๋ ฅ์ ๋ชจ๋ ํ๋กฌํํธ์ ํ์ฌ ๋ธ๋์น ์ํ๋ฅผ ์ถ๊ฐํฉ๋๋ค:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current branch: release-42. Deploy freeze until Friday."
}
}
ํ๋กฌํํธ ์ฐจ๋จ ๋ฐ ์ธ์ ์ ๋ชฉ ์ค์ ์ ํฌํจํ ์ ์ฒด ์ถ๋ ฅ ํํ๋ UserPromptSubmit ๊ฒฐ์ ์ ์ด๋ฅผ ์ฐธ์กฐํ์ธ์.
type: "prompt"๋ฅผ ์ฌ์ฉํ๋ Hooks๋ ์ถ๋ ฅ์ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํฉ๋๋ค: ํ๋กฌํํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฐธ์กฐํ์ธ์.
Matchers๋ก hooks ํํฐ๋ง
Matcher๊ฐ ์์ผ๋ฉด hook์ ์ด๋ฒคํธ์ ๋ชจ๋ ๋ฐ์์์ ๋ฐ์ํฉ๋๋ค. Matchers๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฒ์๋ฅผ ์ขํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด ๋ชจ๋ ๋๊ตฌ ํธ์ถ ํ๊ฐ ์๋ ํ์ผ ํธ์ง ํ์๋ง ํฌ๋งคํฐ๋ฅผ ์คํํ๋ ค๋ฉด PostToolUse hook์ matcher๋ฅผ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write ..." }
]
}
]
}
}
"Edit|Write" matcher๋ Claude๊ฐ Edit ๋๋ Write ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ๋๋ง ๋ฐ์ํ๊ณ Bash, Read ๋๋ ๋ค๋ฅธ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฐ์ํ์ง ์์ต๋๋ค. Claude Code v2.1.191 ์ด์์์๋ ์ผํ๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋์์ ๊ตฌ๋ถํ๋ฏ๋ก "Edit, Write"๋ ๋๋ฑํฉ๋๋ค. Matcher ํจํด์ ์ฐธ์กฐํ์ฌ ์ผ๋ฐ ์ด๋ฆ๊ณผ ์ ๊ท์์ด ํ๊ฐ๋๋ ๋ฐฉ์์ ํ์ธํ์ธ์.
Claude๋ ๋ํ Bash ๋๊ตฌ๋ฅผ ํตํด ์
ธ ๋ช
๋ น์ ์คํํ์ฌ ํ์ผ์ ์์ฑํ๊ฑฐ๋ ์์ ํ ์ ์์ต๋๋ค. Hook์ด ๊ท์ ์ค์ ์ค์บ ๋๋ ๊ฐ์ฌ ๋ก๊น
๊ณผ ๊ฐ์ด ๋ชจ๋ ํ์ผ ๋ณ๊ฒฝ์ ํ์ธํด์ผ ํ๋ ๊ฒฝ์ฐ ํด๋น ํ ๋ฒ ์์
ํธ๋ฆฌ๋ฅผ ์ค์บํ๋ Stop hook์ ์ถ๊ฐํฉ๋๋ค. ํธ์ถ๋น ๋ฒ์๋ฅผ ๋์ ์ํ๋ฉด Bash๋ ์ผ์น์ํค๊ณ ์คํฌ๋ฆฝํธ๊ฐ git status --porcelain์ผ๋ก ์์ ๋๊ณ ์ถ์ ๋์ง ์์ ํ์ผ์ ๋์ดํ๋๋ก ํฉ๋๋ค.
๊ฐ ์ด๋ฒคํธ ์ ํ์ ํน์ ํ๋์์ ์ผ์นํฉ๋๋ค:
| ์ด๋ฒคํธ | Matcher๊ฐ ํํฐ๋งํ๋ ๊ฒ | ์์ matcher ๊ฐ |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied |
๋๊ตฌ ์ด๋ฆ | Bash, Edit|Write, mcp__.* |
SessionStart |
์ธ์ ์ด ์์๋ ๋ฐฉ์ | startup, resume, clear, compact |
Setup |
์ด๋ค CLI ํ๋๊ทธ๊ฐ ์ค์ ์ ํธ๋ฆฌ๊ฑฐํ๋์ง | init, maintenance |
SessionEnd |
์ธ์ ์ด ์ข ๋ฃ๋ ์ด์ | clear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other |
Notification |
์๋ฆผ ์ ํ | permission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed |
SubagentStart |
์์ด์ ํธ ์ ํ | general-purpose, Explore, Plan ๋๋ ์ฌ์ฉ์ ์ ์ ์์ด์ ํธ ์ด๋ฆ |
PreCompact, PostCompact |
์์ถ์ ํธ๋ฆฌ๊ฑฐํ ๊ฒ | manual, auto |
SubagentStop |
์์ด์ ํธ ์ ํ | SubagentStart์ ๋์ผํ ๊ฐ |
ConfigChange |
๊ตฌ์ฑ ์์ค | user_settings, project_settings, local_settings, policy_settings, skills |
StopFailure |
์ค๋ฅ ์ ํ | rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, unknown |
InstructionsLoaded |
๋ก๋ ์ด์ | session_start, nested_traversal, path_glob_match, include, compact |
Elicitation |
MCP ์๋ฒ ์ด๋ฆ | ๊ตฌ์ฑ๋ MCP ์๋ฒ ์ด๋ฆ |
ElicitationResult |
MCP ์๋ฒ ์ด๋ฆ | Elicitation๊ณผ ๋์ผํ ๊ฐ |
FileChanged |
๊ฐ์ํ ๋ฆฌํฐ๋ด ํ์ผ ์ด๋ฆ (FileChanged ์ฐธ์กฐ) | .envrc|.env |
UserPromptExpansion |
๋ช ๋ น ์ด๋ฆ | skill ๋๋ ๋ช ๋ น ์ด๋ฆ |
UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, CwdChanged, MessageDisplay |
matcher ์ง์ ์์ | ๋ชจ๋ ๋ฐ์์์ ํญ์ ๋ฐ์ |
๋ค์ํ ์ด๋ฒคํธ ์ ํ์์ matchers๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ช ๊ฐ์ง ์ถ๊ฐ ์์ :
Bash ๋๊ตฌ ํธ์ถ๋ง ์ผ์น์ํค๊ณ ๊ฐ ๋ช
๋ น์ ํ์ผ์ ๊ธฐ๋กํฉ๋๋ค. PostToolUse ์ด๋ฒคํธ๋ ๋ช
๋ น์ด ์๋ฃ๋ ํ ๋ฐ์ํ๋ฏ๋ก tool_input.command๋ ์คํ๋ ๋ด์ฉ์ ํฌํจํฉ๋๋ค. Hook์ stdin์์ ์ด๋ฒคํธ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋ฐ๊ณ jq -r '.tool_input.command'๋ ๋ช
๋ น ๋ฌธ์์ด๋ง ์ถ์ถํ๋ฉฐ >>๋ ๋ก๊ทธ ํ์ผ์ ์ถ๊ฐํฉ๋๋ค:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}
MCP ๋๊ตฌ๋ ๊ธฐ๋ณธ ์ ๊ณต ๋๊ตฌ์ ๋ค๋ฅธ ๋ช
๋ช
๊ท์น์ ์ฌ์ฉํฉ๋๋ค: mcp__<server>__<tool>. ์ฌ๊ธฐ์ <server>๋ MCP ์๋ฒ ์ด๋ฆ์ด๊ณ <tool>์ ์ ๊ณตํ๋ ๋๊ตฌ์
๋๋ค. ์๋ฅผ ๋ค์ด mcp__github__search_repositories ๋๋ mcp__filesystem__read_file. ํ๋ฌ๊ทธ์ธ ๋ฒ๋ค ์๋ฒ์ ๋๊ตฌ๋ ๋์ mcp__plugin_my-plugin_db__query์ ๊ฐ์ ๋ฒ์๊ฐ ์ง์ ๋ ์๋ฒ ์ธ๊ทธ๋จผํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ ๊ท์ matcher๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ์๋ฒ์ ๋ชจ๋ ๋๊ตฌ๋ฅผ ๋์์ผ๋ก ํ๊ฑฐ๋ mcp__.*__write.*์ ๊ฐ์ ํจํด์ผ๋ก ์๋ฒ ์ ์ฒด์์ ์ผ์นํฉ๋๋ค. ์ฐธ์กฐ์ MCP ๋๊ตฌ ์ผ์น๋ฅผ ์ฐธ์กฐํ์ฌ ์ ์ฒด ์์ ๋ชฉ๋ก์ ํ์ธํ์ธ์.
์๋ ๋ช
๋ น์ hook์ JSON ์
๋ ฅ์์ jq๋ฅผ ์ฌ์ฉํ์ฌ ๋๊ตฌ ์ด๋ฆ์ ์ถ์ถํ๊ณ stderr์ ์๋๋ค. stderr๋ stdout์ ๊นจ๋ํ๊ฒ ์ ์งํ๊ณ ๋ฉ์์ง๋ฅผ ๋๋ฒ๊ทธ ๋ก๊ทธ๋ก ๋ณด๋
๋๋ค:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2"
}
]
}
]
}
}
SessionEnd ์ด๋ฒคํธ๋ ์ธ์
์ด ์ข
๋ฃ๋ ์ด์ ์ ๋ํ matchers๋ฅผ ์ง์ํฉ๋๋ค. ์ด hook์ ์ผ๋ฐ ์ข
๋ฃ๊ฐ ์๋ /clear๋ฅผ ์คํํ ๋๋ง ๋ฐ์ํฉ๋๋ค:
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "rm -f /tmp/claude-scratch-*.txt"
}
]
}
]
}
}
์ ์ฒด matcher ๊ตฌ๋ฌธ์ Hooks ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.
๋๊ตฌ ์ด๋ฆ ๋ฐ ์ธ์๋ก `if` ํ๋๋ฅผ ์ฌ์ฉํ์ฌ ํํฐ๋ง
if ํ๋๋ ๊ถํ ๊ท์น ๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ฌ ๋๊ตฌ ์ด๋ฆ๊ณผ ์ธ์๋ฅผ ํจ๊ป ์ฌ์ฉํ์ฌ hooks๋ฅผ ํํฐ๋งํ๋ฏ๋ก hook ํ๋ก์ธ์ค๋ ๋๊ตฌ ํธ์ถ์ด ์ผ์นํ ๋๋ง ์์ฑ๋ฉ๋๋ค. ์ด๋ ๋๊ตฌ ์ด๋ฆ๋ง์ผ๋ก ๊ทธ๋ฃน ์์ค์์ ํํฐ๋งํ๋ matcher๋ฅผ ์ด๊ณผํฉ๋๋ค.
์๋ฅผ ๋ค์ด ๋ชจ๋ Bash ๋ช
๋ น์ด ์๋ git ๋ช
๋ น์ ์ฌ์ฉํ ๋๋ง hook์ ์คํํ๋ ค๋ฉด:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
Hook ๋ช
๋ น์ด ์คํ๋๋์ง ์ฌ๋ถ๋ if ํจํด์ ํํ์ Claude๊ฐ ํธ์ถํ๋ Bash ๋ช
๋ น์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋๋ค:
if ํจํด |
Bash ๋ช ๋ น | Hook์ด ์คํ๋๋์? | ์ด์ |
|---|---|---|---|
Bash(git *) |
git push |
์ | ๋ช ๋ น ์ด๋ฆ์ด ์ผ์นํฉ๋๋ค |
Bash(git *) |
npm test && git push |
์ | ๊ฐ ์๋ธ๋ช
๋ น์ด ํ์ธ๋ฉ๋๋ค; git push๊ฐ ์ผ์นํฉ๋๋ค |
Bash(git *) |
echo $(git log) |
์ | $() ๋ฐ ๋ฐฑํฑ ๋ด์ ๋ช
๋ น์ด ํ์ธ๋ฉ๋๋ค; git log๊ฐ ์ผ์นํฉ๋๋ค |
Bash(git *) |
echo $(date) |
์๋์ค | ์๋ธ๋ช
๋ น์ด git *๊ณผ ์ผ์นํ์ง ์์ต๋๋ค |
Bash(git push *) |
echo $(date) |
์ | ๋ช
๋ น ์ด๋ฆ๋ณด๋ค ๋ ๋ง์ด ์ง์ ํ๋ ํจํด์ $(), ๋ฐฑํฑ ๋๋ $VAR์์ ์ด์จ๋ hook์ ์คํํฉ๋๋ค |
ํํฐ๋ ๋ํ ์คํจ ์ ์ด๋ ค ์์ผ๋ฏ๋ก Bash ๋ช ๋ น์ ๊ตฌ๋ฌธ ๋ถ์ํ ์ ์์ ๋ ํจํด์ ๊ด๊ณ์์ด hook์ ์คํํฉ๋๋ค. ํํฐ๋ ์ต์ ์ ๋ ธ๋ ฅ์ด๋ฏ๋ก ํ๋ ํ์ฉ ๋๋ ๊ฑฐ๋ถ๋ฅผ ์ ์ฉํ๋ ค๋ฉด hook ๋์ ๊ถํ ์์คํ ์ ์ฌ์ฉํฉ๋๋ค.
if ํ๋๋ ๊ถํ ๊ท์น๊ณผ ๋์ผํ ํจํด์ ํ์ฉํฉ๋๋ค: "Bash(git *)", "Edit(*.ts)" ๋ฑ. ์ฌ๋ฌ ๋๊ตฌ ์ด๋ฆ์ ์ผ์น์ํค๋ ค๋ฉด ๊ฐ๊ฐ ์์ ์ if ๊ฐ์ ๊ฐ์ง ๋ณ๋์ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ํ์ดํ ๊ต๋๊ฐ ์ง์๋๋ matcher ์์ค์์ ์ผ์นํฉ๋๋ค.
if๋ ๋๊ตฌ ์ด๋ฒคํธ์์๋ง ์๋ํฉ๋๋ค: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest ๋ฐ PermissionDenied. ๋ค๋ฅธ ์ด๋ฒคํธ์ ์ถ๊ฐํ๋ฉด hook์ด ์คํ๋์ง ์์ต๋๋ค.
Hook ์์น ๊ตฌ์ฑ
Hook์ ์ถ๊ฐํ๋ ์์น๋ ๋ฒ์๋ฅผ ๊ฒฐ์ ํฉ๋๋ค:
| ์์น | ๋ฒ์ | ๊ณต์ ๊ฐ๋ฅ |
|---|---|---|
~/.claude/settings.json |
๋ชจ๋ ํ๋ก์ ํธ | ์๋์ค, ์ปดํจํฐ์ ๋ก์ปฌ |
.claude/settings.json |
๋จ์ผ ํ๋ก์ ํธ | ์, ๋ฆฌํฌ์งํ ๋ฆฌ์ ์ปค๋ฐ ๊ฐ๋ฅ |
.claude/settings.local.json |
๋จ์ผ ํ๋ก์ ํธ | ์๋์ค, gitignored |
| ๊ด๋ฆฌํ ์ ์ฑ ์ค์ | ์กฐ์ง ์ ์ฒด | ์, ๊ด๋ฆฌ์ ์ ์ด |
Plugin hooks/hooks.json |
ํ๋ฌ๊ทธ์ธ์ด ํ์ฑํ๋์์ ๋ | ์, ํ๋ฌ๊ทธ์ธ๊ณผ ํจ๊ป ๋ฒ๋ค๋จ |
| Skill ๋๋ agent frontmatter | Skill ๋๋ ์์ด์ ํธ๊ฐ ํ์ฑํ๋์ด ์๋ ๋์ | ์, ์ปดํฌ๋ํธ ํ์ผ์ ์ ์๋จ |
Claude Code์์ /hooks๋ฅผ ์คํํ์ฌ ์ด๋ฒคํธ๋ณ๋ก ๊ทธ๋ฃนํ๋ ๋ชจ๋ ๊ตฌ์ฑ๋ hooks๋ฅผ ์ฐพ์๋ด
๋๋ค.
๋ชจ๋ hooks๋ฅผ ๋นํ์ฑํํ๋ ค๋ฉด ์ค์ ํ์ผ์์ "disableAllHooks": true๋ฅผ ์ค์ ํฉ๋๋ค. ๊ด๋ฆฌํ ์ค์ ์์ ๊ตฌ์ฑ๋ Hooks๋ disableAllHooks๋ ์ค์ ๋์ง ์๋ ํ ์คํ๋ฉ๋๋ค.
Claude Code๊ฐ ์คํ ์ค์ธ ๋์ ์ค์ ํ์ผ์ ์ง์ ํธ์งํ๋ฉด ํ์ผ ๊ฐ์์๊ฐ ์ผ๋ฐ์ ์ผ๋ก hook ๋ณ๊ฒฝ์ ์๋์ผ๋ก ์ ํํฉ๋๋ค.
ํ๋กฌํํธ ๊ธฐ๋ฐ hooks
๊ฒฐ์ ๋ก ์ ๊ท์น์ด ์๋ ํ๋จ์ด ํ์ํ ๊ฒฐ์ ์ ๊ฒฝ์ฐ type: "prompt" hooks๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์
ธ ๋ช
๋ น์ ์คํํ๋ ๋์ Claude Code๋ ํ๋กฌํํธ์ hook์ ์
๋ ฅ ๋ฐ์ดํฐ๋ฅผ Claude ๋ชจ๋ธ(๊ธฐ๋ณธ์ ์ผ๋ก Haiku)์ ์ ์กํ์ฌ ๊ฒฐ์ ์ ๋ด๋ฆฝ๋๋ค. ๋ ๋ง์ ๊ธฐ๋ฅ์ด ํ์ํ ๊ฒฝ์ฐ model ํ๋๋ก ๋ค๋ฅธ ๋ชจ๋ธ์ ์ง์ ํ ์ ์์ต๋๋ค.
๋ชจ๋ธ์ ์ ์ผํ ์์ ์ yes/no ๊ฒฐ์ ์ JSON์ผ๋ก ๋ฐํํ๋ ๊ฒ์ ๋๋ค:
"ok": true: ์์ ์ด ์งํ๋ฉ๋๋ค"ok": false: ์์ ์ด ์ฐจ๋จ๋ฉ๋๋ค. ์ด๋ฒคํธ์ ๋ฐ๋ผ ๋ค์๊ณผ ๊ฐ์ด ์๋ํฉ๋๋ค:Stop๋ฐSubagentStop:reason์ด Claude์๊ฒ ํผ๋๋ฐฑ์ผ๋ก ์ ๋ฌ๋์ด ๊ณ์ ์์ ํฉ๋๋คPreToolUse: ๋๊ตฌ ํธ์ถ์ด ๊ฑฐ๋ถ๋๊ณreason์ด ๋๊ตฌ ์ค๋ฅ๋ก Claude์๊ฒ ๋ฐํ๋์ด ์กฐ์ ํ๊ณ ๊ณ์ํ ์ ์์ต๋๋คPostToolUse,PostToolBatch,UserPromptSubmit, ๋ฐUserPromptExpansion: ํด์ด ์ข ๋ฃ๋๊ณreason์ด ๊ฒฝ๊ณ ์ค๋ก ์ฑํ ์ ๋ํ๋ฉ๋๋ค
์ด ์์ ๋ Stop hook์ ์ฌ์ฉํ์ฌ ๋ชจ๋ ์์ฒญ๋ ์์
์ด ์๋ฃ๋์๋์ง ๋ชจ๋ธ์ ๋ฌป์ต๋๋ค. ๋ชจ๋ธ์ด "ok": false๋ฅผ ๋ฐํํ๋ฉด Claude๋ ๊ณ์ ์์
ํ๊ณ reason์ ๋ค์ ์ง์นจ์ผ๋ก ์ฌ์ฉํฉ๋๋ค:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
์ ์ฒด ๊ตฌ์ฑ ์ต์ ์ ์ฐธ์กฐ์ ํ๋กฌํํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฐธ์กฐํ์ธ์.
์์ด์ ํธ ๊ธฐ๋ฐ hooks
์์ด์ ํธ hooks๋ ์คํ์ ์ ๋๋ค. ๋์ ๋ฐ ๊ตฌ์ฑ์ ํฅํ ๋ฆด๋ฆฌ์ค์์ ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค. ํ๋ก๋์ ์ํฌํ๋ก์ฐ์ ๊ฒฝ์ฐ ๋ช ๋ น hooks๋ฅผ ์ ํธํฉ๋๋ค.
๊ฒ์ฆ์ ํ์ผ ๊ฒ์ฌ ๋๋ ๋ช
๋ น ์คํ์ด ํ์ํ ๊ฒฝ์ฐ type: "agent" hooks๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๋จ์ผ LLM ํธ์ถ์ ์ํํ๋ ํ๋กฌํํธ hooks์ ๋ฌ๋ฆฌ ์์ด์ ํธ hooks๋ ํ์ผ์ ์ฝ๊ณ ์ฝ๋๋ฅผ ๊ฒ์ํ๋ฉฐ ๊ฒฐ์ ์ ๋ฐํํ๊ธฐ ์ ์ ๋ค๋ฅธ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ์ ์๋ subagent๋ฅผ ์์ฑํฉ๋๋ค.
์์ด์ ํธ hooks๋ ํ๋กฌํํธ hooks์ ๋์ผํ "ok" / "reason" ์๋ต ํ์์ ์ฌ์ฉํ์ง๋ง ๊ธฐ๋ณธ ํ์์์์ด 60์ด์ด๊ณ ์ต๋ 50๊ฐ์ ๋๊ตฌ ์ฌ์ฉ ํด์
๋๋ค.
์ด ์์ ๋ Claude๊ฐ ์ค์ง๋๊ธฐ ์ ์ ํ ์คํธ๊ฐ ํต๊ณผํ๋์ง ํ์ธํฉ๋๋ค:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
Hook ์ ๋ ฅ ๋ฐ์ดํฐ๋ง์ผ๋ก ๊ฒฐ์ ์ ๋ด๋ฆด ์ ์์ ๋ ํ๋กฌํํธ hooks๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ฝ๋๋ฒ ์ด์ค์ ์ค์ ์ํ์ ๋ํด ๋ฌด์ธ๊ฐ๋ฅผ ํ์ธํด์ผ ํ ๋ ์์ด์ ํธ hooks๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์ ์ฒด ๊ตฌ์ฑ ์ต์ ์ ์ฐธ์กฐ์ ์์ด์ ํธ ๊ธฐ๋ฐ hooks๋ฅผ ์ฐธ์กฐํ์ธ์.
HTTP hooks
type: "http" hooks๋ฅผ ์ฌ์ฉํ์ฌ ์
ธ ๋ช
๋ น์ ์คํํ๋ ๋์ ์ด๋ฒคํธ ๋ฐ์ดํฐ๋ฅผ HTTP ์๋ํฌ์ธํธ์ POSTํฉ๋๋ค. ์๋ํฌ์ธํธ๋ ๋ช
๋ น hook์ด stdin์์ ๋ฐ์ ๊ฒ๊ณผ ๋์ผํ JSON์ ๋ฐ๊ณ ๋์ผํ JSON ํ์์ ์ฌ์ฉํ์ฌ HTTP ์๋ต ๋ณธ๋ฌธ์ ํตํด ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
HTTP hooks๋ ์น ์๋ฒ, ํด๋ผ์ฐ๋ ํจ์ ๋๋ ์ธ๋ถ ์๋น์ค๊ฐ hook ๋ก์ง์ ์ฒ๋ฆฌํ๊ธฐ๋ฅผ ์ํ ๋ ์ ์ฉํฉ๋๋ค: ์๋ฅผ ๋ค์ด ํ ์ ์ฒด์์ ๋๊ตฌ ์ฌ์ฉ ์ด๋ฒคํธ๋ฅผ ๊ธฐ๋กํ๋ ๊ณต์ ๊ฐ์ฌ ์๋น์ค์ ๋๋ค.
์ด ์์ ๋ ๋ชจ๋ ๋๊ตฌ ์ฌ์ฉ์ ๋ก์ปฌ ๋ก๊น ์๋น์ค์ ๊ฒ์ํฉ๋๋ค:
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}
์๋ํฌ์ธํธ๋ ๋ช
๋ น hooks์ ๋์ผํ ์ถ๋ ฅ ํ์์ ์ฌ์ฉํ์ฌ JSON ์๋ต ๋ณธ๋ฌธ์ ๋ฐํํด์ผ ํฉ๋๋ค. ๋๊ตฌ ํธ์ถ์ ์ฐจ๋จํ๋ ค๋ฉด ์ ์ ํ hookSpecificOutput ํ๋์ ํจ๊ป 2xx ์๋ต์ ๋ฐํํฉ๋๋ค. HTTP ์ํ ์ฝ๋๋ง์ผ๋ก๋ ์์
์ ์ฐจ๋จํ ์ ์์ต๋๋ค.
ํค๋ ๊ฐ์ $VAR_NAME ๋๋ ${VAR_NAME} ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ํ๊ฒฝ ๋ณ์ ๋ณด๊ฐ์ ์ง์ํฉ๋๋ค. allowedEnvVars ๋ฐฐ์ด์ ๋์ด๋ ๋ณ์๋ง ํด๊ฒฐ๋ฉ๋๋ค. ๋ค๋ฅธ ๋ชจ๋ $VAR ์ฐธ์กฐ๋ ๋น์ด ์์ต๋๋ค.
์ ์ฒด ๊ตฌ์ฑ ์ต์ ๋ฐ ์๋ต ์ฒ๋ฆฌ๋ ์ฐธ์กฐ์ HTTP hooks๋ฅผ ์ฐธ์กฐํ์ธ์.
์ ํ ์ฌํญ ๋ฐ ๋ฌธ์ ํด๊ฒฐ
์ ํ ์ฌํญ
Hooks๋ฅผ ์ค๊ณํ ๋ ๋ค์ ์ ์ฝ ์ฌํญ์ ์ผ๋์ ๋์ญ์์ค:
- ๋ช
๋ น hooks๋ stdout, stderr ๋ฐ ์ข
๋ฃ ์ฝ๋๋ฅผ ํตํด์๋ง ํต์ ํฉ๋๋ค.
/๋ช ๋ น์ด๋ ๋๊ตฌ ํธ์ถ์ ์ง์ ํธ๋ฆฌ๊ฑฐํ ์ ์์ต๋๋ค.additionalContext๋ฅผ ํตํด ๋ฐํ๋ ํ ์คํธ๋ Claude๊ฐ ์ผ๋ฐ ํ ์คํธ๋ก ์ฝ๋ ์์คํ ์๋ฆผ์ผ๋ก ์ฃผ์ ๋ฉ๋๋ค. HTTP hooks๋ ์๋ต ๋ณธ๋ฌธ์ ํตํด ํต์ ํฉ๋๋ค. - Hook ํ์์์์ ์ ํ์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค.
timeoutํ๋(์ด ๋จ์)๋ก hook๋น ์ฌ์ ์ํ ์ ์์ต๋๋ค.command,http,mcp_tool: 10๋ถ.UserPromptSubmit์ ์ด๋ฅผ 30์ด๋ก ๋ฎ์ถ๊ณ ,MessageDisplay๋ ์ด๋ฅผ 10์ด๋ก ๋ฎ์ถฅ๋๋ค.prompt: 30์ด.agent: 60์ด.
PostToolUsehooks๋ ๋๊ตฌ๊ฐ ์ด๋ฏธ ์คํ๋์์ผ๋ฏ๋ก ์์ ์ ์ทจ์ํ ์ ์์ต๋๋ค.PermissionRequesthooks๋ ๋น๋ํํ ๋ชจ๋์์-pํ๋๊ทธ์ ํจ๊ป ๋ฐ์ํ์ง ์์ต๋๋ค. ์๋ํ๋ ๊ถํ ๊ฒฐ์ ์ ์ํดPreToolUsehooks๋ฅผ ์ฌ์ฉํฉ๋๋ค.Stophooks๋ ์์ ์๋ฃ ์์๋ง์ด ์๋๋ผ Claude๊ฐ ์๋ต์ ์๋ฃํ ๋๋ง๋ค ๋ฐ์ํฉ๋๋ค. ์ฌ์ฉ์ ์ค๋จ ์์๋ ๋ฐ์ํ์ง ์์ต๋๋ค. API ์ค๋ฅ๋ ๋์ StopFailure๋ฅผ ๋ฐ์์ํต๋๋ค.- ์ฌ๋ฌ
PreToolUsehooks๊ฐupdatedInput์ ๋ฐํํ์ฌ ๋๊ตฌ์ ์ธ์๋ฅผ ๋ค์ ์ธ ๋ ๋ง์ง๋ง์ผ๋ก ์๋ฃ๋ ๊ฒ์ด ์ฐ์ ํฉ๋๋ค. Hooks๋ ๋ณ๋ ฌ๋ก ์คํ๋๋ฏ๋ก ์์๋ ๋น๊ฒฐ์ ์ ์ ๋๋ค. ๋์ผํ ๋๊ตฌ์ ์ ๋ ฅ์ ์์ ํ๋ hook์ด ๋ ๊ฐ ์ด์ ์๋ ๊ฒ์ ํผํฉ๋๋ค.
Hooks ๋ฐ ๊ถํ ๋ชจ๋
PreToolUse hooks๋ ๋ชจ๋ ๊ถํ ๋ชจ๋ ํ์ธ ์ ์ ๋ฐ์ํฉ๋๋ค. permissionDecision: "deny"๋ฅผ ๋ฐํํ๋ hook์ bypassPermissions ๋ชจ๋ ๋๋ --dangerously-skip-permissions์์๋ ๋๊ตฌ๋ฅผ ์ฐจ๋จํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ๊ถํ ๋ชจ๋๋ฅผ ๋ณ๊ฒฝํ์ฌ ์ฐํํ ์ ์๋ ์ ์ฑ
์ ์ ์ฉํ ์ ์์ต๋๋ค.
๋ฐ๋๋ ์ฌ์ค์ด ์๋๋๋ค: "allow"๋ฅผ ๋ฐํํ๋ hook์ ์ค์ ์ ๊ฑฐ๋ถ ๊ท์น์ ์ฐํํ์ง ์์ผ๋ฉฐ, ์กฐ์ง์ด ask๋ก ์ค์ ํ ์ปค๋ฅํฐ ๋๊ตฌ ๋๋ requiresUserInteraction์ผ๋ก ํ์๋ MCP ๋๊ตฌ์ ํ๋กฌํํธ๋ฅผ ์ต์ ํ ์ ์์ต๋๋ค. Hooks๋ ์ ํ์ ๊ฐํํ ์ ์์ง๋ง ๊ถํ ๊ท์น์ด ํ์ฉํ๋ ๊ฒ์ ์ด๊ณผํ์ฌ ์ํํ ์ ์์ต๋๋ค.
Hook์ด ๋ฐ์ํ์ง ์์
Hook์ด ๊ตฌ์ฑ๋์์ง๋ง ์คํ๋์ง ์์ต๋๋ค.
/hooks๋ฅผ ์คํํ๊ณ hook์ด ์ฌ๋ฐ๋ฅธ ์ด๋ฒคํธ ์๋์ ๋ํ๋๋์ง ํ์ธํฉ๋๋ค- Matcher ํจํด์ด ๋๊ตฌ ์ด๋ฆ๊ณผ ์ ํํ ์ผ์นํ๋์ง ํ์ธํฉ๋๋ค. Matchers๋ ๋์๋ฌธ์ ๊ตฌ๋ถ์ ๋๋ค
- ์ฌ๋ฐ๋ฅธ ์ด๋ฒคํธ ์ ํ์ ํธ๋ฆฌ๊ฑฐํ๋์ง ํ์ธํฉ๋๋ค:
PreToolUse๋ ๋๊ตฌ ์คํ ์ ์ ๋ฐ์ํ๊ณPostToolUse๋ ํ์ ๋ฐ์ํฉ๋๋ค - ๋น๋ํํ ๋ชจ๋์์
-pํ๋๊ทธ์ ํจ๊ปPermissionRequesthooks๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋์PreToolUse๋ก ์ ํํฉ๋๋ค
์ถ๋ ฅ์ Hook ์ค๋ฅ
ํธ๋์คํฌ๋ฆฝํธ์ "PreToolUse hook error: ..." ๊ฐ์ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค.
- ์คํฌ๋ฆฝํธ๊ฐ ์๊ธฐ์น ์๊ฒ 0์ด ์๋ ์ฝ๋๋ก ์ข
๋ฃ๋์์ต๋๋ค. ์ํ JSON์ ํ์ดํํ์ฌ ์๋์ผ๋ก ํ
์คํธํฉ๋๋ค:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh echo $? # ์ข ๋ฃ ์ฝ๋ ํ์ธ - "command not found"๊ฐ ํ์๋๋ฉด ์ ๋ ๊ฒฝ๋ก๋ฅผ ์ฌ์ฉํ๊ฑฐ๋
${CLAUDE_PROJECT_DIR}์ ์ฌ์ฉํ์ฌ ์คํฌ๋ฆฝํธ๋ฅผ ์ฐธ์กฐํฉ๋๋ค. ์ ธ ์ธ์ฉ์ ์์ ํ ํผํ๋ ค๋ฉด"args": []๋ฅผ ์ถ๊ฐํ์ฌ exec form์ผ๋ก ์ ํํ๋ฉด ์ ธ ์์ด ์คํฌ๋ฆฝํธ๋ฅผ ์ง์ ์์ฑํฉ๋๋ค - "jq: command not found"๊ฐ ํ์๋๋ฉด
jq๋ฅผ ์ค์นํ๊ฑฐ๋ JSON ๊ตฌ๋ฌธ ๋ถ์์ ์ํด Python/Node.js๋ฅผ ์ฌ์ฉํฉ๋๋ค - ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋์ง ์์ผ๋ฉด ์คํ ๊ฐ๋ฅํ๊ฒ ๋ง๋ญ๋๋ค:
chmod +x ./my-hook.sh
`/hooks`์ ๊ตฌ์ฑ๋ hooks๊ฐ ์์
์ค์ ํ์ผ์ ํธ์งํ์ง๋ง hooks๊ฐ ๋ฉ๋ด์ ๋ํ๋์ง ์์ต๋๋ค.
- ํ์ผ ํธ์ง์ ์ผ๋ฐ์ ์ผ๋ก ์๋์ผ๋ก ์ ํ๋ฉ๋๋ค. ๋ช ์ด ํ์ ๋ํ๋์ง ์์ผ๋ฉด ํ์ผ ๊ฐ์์๊ฐ ๋ณ๊ฒฝ์ ๋์ณค์ ์ ์์ต๋๋ค: ์ธ์ ์ ๋ค์ ์์ํ์ฌ ๊ฐ์ ๋ก ๋ค์ ๋ก๋ํฉ๋๋ค.
- JSON์ด ์ ํจํ์ง ํ์ธํฉ๋๋ค: ํํ ์ผํ ๋ฐ ์ฃผ์์ ํ์ฉ๋์ง ์์ต๋๋ค
- ์ค์ ํ์ผ์ด ์ฌ๋ฐ๋ฅธ ์์น์ ์๋์ง ํ์ธํฉ๋๋ค: ํ๋ก์ ํธ hooks์ ๊ฒฝ์ฐ
.claude/settings.json, ์ ์ญ hooks์ ๊ฒฝ์ฐ~/.claude/settings.json
Stop hook์ด ๋ธ๋ก ์ํ์ ๋๋ฌํจ
Claude๊ฐ ๊ณ์ ์์ ํ๋ ๋์ ์ค์งํ๊ณ Stop hook์ด ๋๋ฌด ๋ง์ ํ์๋ฅผ ์ฐ์์ผ๋ก ์ฐจ๋จํ๋ค๋ ๊ฒฝ๊ณ ๋ก ํด์ ์ข ๋ฃํฉ๋๋ค.
Claude Code๋ Stop hook์ด ์งํ ์์ด 8๋ฒ ์ฐ์์ผ๋ก ์ฐจ๋จํ ํ ์ฌ์ ์ํฉ๋๋ค. Hook ์คํฌ๋ฆฝํธ๋ ์ด๋ฏธ ํธ๋ฆฌ๊ฑฐ๋์๋์ง ํ์ธํด์ผ ํฉ๋๋ค. JSON ์
๋ ฅ์์ stop_hook_active ํ๋๋ฅผ ๊ตฌ๋ฌธ ๋ถ์ํ๊ณ true์ธ ๊ฒฝ์ฐ ์กฐ๊ธฐ์ ์ข
๋ฃํฉ๋๋ค:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Claude๊ฐ ์ค์ง๋๋๋ก ํ์ฉ
fi
# ... hook ๋ก์ง์ ๋๋จธ์ง
Hook์ด ์๋ ดํ๊ธฐ ์ํด 8๋ฒ ์ด์์ ๋ฐ๋ณต์ด ํ์ํ ๊ฒฝ์ฐ CLAUDE_CODE_STOP_HOOK_BLOCK_CAP์ผ๋ก ์ํ์ ์ฌ๋ฆฝ๋๋ค.
JSON ๊ฒ์ฆ ์คํจ
Hook ์คํฌ๋ฆฝํธ๊ฐ ์ ํจํ JSON์ ์ถ๋ ฅํ๋๋ผ๋ Claude Code์ JSON ๊ตฌ๋ฌธ ๋ถ์ ์ค๋ฅ๊ฐ ํ์๋ฉ๋๋ค.
Claude Code๊ฐ ์
ธ ํ์ ๋ช
๋ น hook(args ์๋ hook)์ ์คํํ ๋ macOS ๋ฐ Linux์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก sh -c๋ฅผ ์์ฑํ๊ฑฐ๋ Windows์์๋ Git Bash๋ฅผ ์์ฑํฉ๋๋ค. ์ด ์
ธ์ ๋น๋ํํ์ด์ง๋ง Git Bash ๋ฐ ์ผ๋ถ ๊ตฌ์ฑ(์: BASH_ENV๊ฐ ~/.bashrc๋ฅผ ๊ฐ๋ฆฌํด)์ ์ฌ์ ํ ํ๋กํ์ ์์ฑํฉ๋๋ค. ํด๋น ํ๋กํ์ ๋ฌด์กฐ๊ฑด์ ์ธ echo ๋ฌธ์ด ํฌํจ๋์ด ์์ผ๋ฉด ์ถ๋ ฅ์ด hook์ JSON์ ์์ ๋ถ์ต๋๋ค:
Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}
Claude Code๋ ์ด๋ฅผ JSON์ผ๋ก ๊ตฌ๋ฌธ ๋ถ์ํ๋ ค๊ณ ํ๊ณ ์คํจํฉ๋๋ค. ์ด๋ฅผ ์์ ํ๋ ค๋ฉด ์ ธ ํ๋กํ์ echo ๋ฌธ์ ๋ํํ์ฌ ๋ํํ ์ ธ์์๋ง ์คํ๋๋๋ก ํฉ๋๋ค:
# ~/.zshrc ๋๋ ~/.bashrc์์
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
$- ๋ณ์๋ ์
ธ ํ๋๊ทธ๋ฅผ ํฌํจํ๊ณ i๋ ๋ํํ์ ์๋ฏธํฉ๋๋ค. Hooks๋ ๋น๋ํํ ์
ธ์์ ์คํ๋๋ฏ๋ก echo๋ ๊ฑด๋๋๋๋ค.
๋๋ฒ๊ทธ ๊ธฐ๋ฒ
ํธ๋์คํฌ๋ฆฝํธ ๋ณด๊ธฐ๋ Ctrl+O๋ก ์ ํ๋๋ฉฐ ๋ฐ์ํ ๊ฐ hook์ ๋ํด ํ ์ค ์์ฝ์ ํ์ํฉ๋๋ค: ์ฑ๊ณต์ ์๋์ผ๋ก ํ์๋๊ณ , ์ฐจ๋จ ์ค๋ฅ๋ stderr๋ฅผ ํ์ํ๋ฉฐ, ์ฐจ๋จํ์ง ์๋ ์ค๋ฅ๋ <hook name> hook error ๊ณต์ง๋ฅผ ํ์ํ ํ stderr์ ์ฒซ ๋ฒ์งธ ์ค์ ํ์ํฉ๋๋ค.
์ ์ฒด ์คํ ์ธ๋ถ ์ ๋ณด(์ผ์นํ hooks, ์ข
๋ฃ ์ฝ๋, stdout ๋ฐ stderr ํฌํจ)๋ ๋๋ฒ๊ทธ ๋ก๊ทธ๋ฅผ ์ฝ์ต๋๋ค. claude --debug-file /tmp/claude.log๋ก Claude Code๋ฅผ ์์ํ์ฌ ์๋ ค์ง ๊ฒฝ๋ก์ ์ฐ๊ฑฐ๋ ๋ค๋ฅธ ํฐ๋ฏธ๋์์ tail -f /tmp/claude.log๋ฅผ ์คํํฉ๋๋ค. ํด๋น ํ๋๊ทธ ์์ด ์์ํ ๊ฒฝ์ฐ ์ธ์
์ค์ /debug๋ฅผ ์คํํ์ฌ ๋ก๊น
์ ํ์ฑํํ๊ณ ๋ก๊ทธ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ต๋๋ค.
์์ธํ ์์๋ณด๊ธฐ
- Hooks ์ฐธ์กฐ: ์ ์ฒด ์ด๋ฒคํธ ์คํค๋ง, JSON ์ถ๋ ฅ ํ์, ๋น๋๊ธฐ hooks ๋ฐ MCP ๋๊ตฌ hooks
- ๋ณด์ ๊ณ ๋ ค ์ฌํญ: ๊ณต์ ๋๋ ํ๋ก๋์ ํ๊ฒฝ์์ hooks๋ฅผ ๋ฐฐํฌํ๊ธฐ ์ ์ ๊ฒํ ํฉ๋๋ค
- Bash ๋ช ๋ น ๊ฒ์ฆ๊ธฐ ์์ : ์์ ํ ์ฐธ์กฐ ๊ตฌํ