본문 바로가기
C.W.K.
Stream
Lesson 06 of 08 · published

훅은 프롬프트보다 단단한 정책 경계야

~16 min · hooks, PreToolUse, PostToolUse

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

도구 사건에 코드가 끼어들어

훅은 PreToolUse, PostToolUse, UserPromptSubmit, Stop 같은 사건에 호출되는 콜백이야. 도구 실행 전후에 기록을 남기고, 인자에서 비밀을 가리고, 위험한 호출을 거부할 수 있어. 모델의 협조와 무관하게 코드가 정책을 집행하는 자리야.

안내는 프롬프트로, 불변 조건은 훅으로

시스템 프롬프트에 “rm -rf를 실행하지 마”라고 쓰면 모델이 대체로 따르지만 보장은 아니야. 해당 명령 패턴을 본 훅이 거부하면 실행 경로 자체가 닫혀. 바람직한 행동은 프롬프트로 설명하고 절대 넘으면 안 되는 경계는 코드로 막아.

기록·가림·거부 세 종류부터 시작해

모든 도구 호출을 구조화 로그에 남기는 기록 훅, 실행 전에 인자의 비밀을 지우는 가림 훅, 금지 패턴을 차단하는 거부 훅이 기본 세트야. cwkPippa 자체는 지금 SDK 훅을 하나도 등록하지 않아. 그러니 cwkPippa 코드 구경이 아니라 패턴으로 받아 가.

원칙: 훅은 프롬프트가 바뀌어도 정책을 지켜. 모델 행동은 흔들릴 수 있지만 코드 경계는 흔들리지 않아.

Code

위험 명령 veto하는 PreToolUse hook·python
from claude_agent_sdk import ClaudeAgentOptions, HookContext, HookMatcher

DESTRUCTIVE = ("rm -rf", "dd if=")

async def block_destructive_bash(input_data: dict, tool_use_id: str | None, context: HookContext) -> dict:
    """Every hook callback takes (input, tool_use_id, context) and returns a JSON dict."""
    command = input_data["tool_input"].get("command", "")
    if any(pattern in command for pattern in DESTRUCTIVE):
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "refused: matches a destructive command pattern",
            }
        }
    return {}  # no opinion: the normal permission flow decides

options = ClaudeAgentOptions(
    cwd="/srv",
    # matcher is a pattern over tool names; only Bash calls reach this callback
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_destructive_bash])]},
)
Telemetry용 PostToolUse hook·python
import hashlib, json, time
from claude_agent_sdk import ClaudeAgentOptions, HookContext, HookMatcher

async def log_tool_use(input_data: dict, tool_use_id: str | None, context: HookContext) -> dict:
    line = {
        "ts": time.time(),
        "session": input_data["session_id"],
        "tool": input_data["tool_name"],
        "tool_use_id": tool_use_id,
        # a stable digest; Python's hash() changes with every process
        "input_sha": hashlib.sha256(json.dumps(input_data["tool_input"], sort_keys=True).encode()).hexdigest()[:16],
    }
    with open("/var/log/agent.jsonl", "a") as f:
        f.write(json.dumps(line) + "\n")
    return {}

options = ClaudeAgentOptions(
    cwd="/srv",
    # no matcher: every tool. A call that fails fires PostToolUseFailure instead.
    hooks={"PostToolUse": [HookMatcher(hooks=[log_tool_use])]},
)

External links

Exercise

모든 Bash 명령을 기록하는 읽기 전용 PreToolUse 훅을 넣고 로그를 확인해. 반드시 막고 싶은 명령 패턴 하나에는 거부 규칙을 추가해 시험해.
Hint
모델이 실제로 그 패턴을 요청하게 만들어 훅이 거부하고 모델이 다른 길을 고르는지 봐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.