훅은 확정이고 프롬프트는 확률이라는데, 훅도 깨집니다

안녕하세요, 고준서입니다. agent-research는 “내 분야에 전문성이 있는 AI 에이전트를 어떻게 만드나”를 정리하는 개인 연구 레포예요. 9월 5일에 그 레포에서 문서에 적어만 두던 규칙 하나(“생성된 위키는 손으로 고치지 않는다”)를, 파일 편집 직전에 경로를 검사해 거부하는 훅으로 바꿨습니다. 그리고 그 훅이 같은 날 깨졌어요. 설정에 적힌 스크립트 경로가 상대경로라서, 직전 셸 명령이 다른 디렉터리로 옮겨 간 상태에서는 훅이 스크립트를 못 찾았습니다. 훅은 실행됐는데 실행돼서 실패한 겁니다.

계기는 그날 아침 카카오페이 기술 블로그의 AI 에이전트 글을 Claude Code에게 읽힌 일이었습니다. 거기 나온 원칙 하나가 “훅 실행은 보장되고 프롬프트는 확률”이었어요. 훅이라는 건 AI가 어떤 도구를 쓰기 직전이나 직후에 무조건 실행되는 작은 프로그램입니다. 프롬프트에 “이건 하지 마”라고 적어 두면 AI가 그 지시를 지킬지는 매번 확률의 문제지만, 훅은 조건에 걸리면 예외 없이 막습니다.

이 글에는 왜 그 규칙을 훅으로 바꿨는지, 훅이 어떻게 깨졌고 어떻게 고쳤는지, 그리고 고친 방식의 대안을 비교하지 않았다는 것까지 레포의 07 문서와 커밋으로 적습니다. 훅이 실제 작업 중에 편집을 막은 횟수는 세지 않았어요. 기록에 있는 차단은 검증하려고 일부러 시도한 것뿐입니다.

약속 하나를 차단으로 바꾸기

이 레포에는 OpenWiki라는 도구가 소스 문서를 읽어 자동으로 만든 위키가 있습니다. 그 위키를 손으로 고치면 다음 생성 때 덮어써지니까, “생성된 위키는 손으로 고치지 않는다”는 규칙을 문서에 적어 두고 있었어요. 적어만 둔 규칙이었죠. 문서에 적힌 규칙은 다섯 개였는데, 왜 이 규칙을 먼저 훅으로 옮겼는지는 07에 이유가 없습니다. 지금 보면 다섯 중 “경로만 보면 판정이 끝나는” 유일한 규칙이긴 한데, 그건 당시 기록이 아니라 지금의 설명이에요.

Claude가 이 규칙을 PreToolUse 훅으로 옮겼습니다. 파일을 편집하는 도구가 호출되기 직전에 스크립트가 경로를 보고, 생성물 경로면 거부하는 구조예요.

# .claude/hooks/block-openwiki-generated-edits.py
GENERATED_PATTERNS = [
    r"(^|/)openwiki/\.claims/",
    r"(^|/)openwiki/.*\.md$",
]
    if any(re.search(p, file_path) for p in GENERATED_PATTERNS):
        reason = (
            f"Blocked: '{file_path}' is OpenWiki-generated content. "
            "Never hand-edit openwiki/*.md or openwiki/.claims/*.json "
            "(agent-expertise-framework/04-purpose-and-self-improvement-loop.md §5). "
            "Edit the source doc in agent-expertise-framework/ instead and let OpenWiki regenerate."
        )
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": reason,
            }
        }))

문서 07의 실행 기록에 따르면 확인은 세 단계로 했습니다. 가짜 입력을 파이프로 넣어 차단 응답이 나오는지, 실제 편집 도구로 위키 파일을 고치려 했을 때 막히는지(막힌 뒤 파일이 안 바뀐 것을 grep으로 재확인), 그리고 소스 문서 편집은 정상 통과하는지. 처음엔 개인 설정 파일(settings.local.json, 커밋되지 않음)에 넣었다가 “이 레포에서 작업하는 누구에게나 적용돼야 하는 규칙”이라는 이유로 팀 공유 설정(settings.json)으로 옮겼다는 것도 거기 적혀 있어요.

같은 세션 초반에 다른 일로 제가 이런 말을 한 적이 있습니다. AI가 비슷한 계정 이름 둘을 같은 계정으로 넘겨짚었을 때였어요.

문자열이 다르다는걸 잘 인지했으면 문제 업었을 것 같은데, 그 부분을 툴로 만들어

“조심하겠다”가 아니라 도구로 막으라는 요구였고, 훅은 그 방향의 첫 구현이었습니다.

그 훅이 같은 날 깨졌습니다

훅을 넣고 몇 시간 뒤, 같은 문서(07)에 다른 검증 결과를 적으려고 편집 도구를 쓰자 이런 오류가 났습니다.

python3: can't open file '.../.claude/skills/openwiki/.claude/hooks/block-openwiki-generated-edits.py'

설정 파일의 명령이 python3 .claude/hooks/...처럼 상대경로였는데, 직전에 실행한 셸 명령에서 다른 디렉터리로 cd한 상태가 이어져서 훅이 저장소 루트가 아닌 엉뚱한 곳에서 스크립트를 찾은 거예요. 훅은 “무조건 실행”되긴 했습니다. 실행돼서 실패했을 뿐이죠.

고친 건 명령 앞에 저장소 루트로 이동하는 한 줄이었습니다.

"command": "cd \"$(git rev-parse --show-toplevel)\" && python3 .claude/hooks/block-openwiki-generated-edits.py"

일부러 저장소 밖 디렉터리에 서서 다시 편집을 시도해 차단되는 것까지 확인했다고 07에 적혀 있습니다.

이 방식 말고도 길은 있었습니다. 스크립트를 절대경로로 적거나, 훅 실행 환경이 주는 프로젝트 경로 변수를 쓰는 방법이요. 어느 쪽이 나은지는 비교하지 않았고, 07에도 비교 기록이 없습니다. git rev-parse는 저장소 안에서만 답을 주니 저장소 밖에서 훅이 돌면 이 한 줄도 실패하는데, 그 경우가 실제로 생기는지는 확인하지 않았어요.

문서에는 이 사건의 의미를 이렇게 정리해 뒀습니다. “‘구조적으로 불가능’을 다시 ‘프롬프트 수준의 우연’으로 되돌릴 수 있다는 걸 실제로 보여준 사례.” 이번엔 훅이 시끄럽게 실패해서(오류 메시지가 남아서) 바로 잡혔지만, 조용히 통과시키는 방향으로 깨졌다면 규칙은 다시 약속으로 돌아가 있었을 겁니다. 조용히 깨지는 경우를 잡는 장치는 지금도 없어요.

남은 것

이 훅과 문서는 Claude가 쓰고 제가 검토해 9월 5일 커밋(a15f7b9)으로 올렸습니다. 문서에 적어 둔 규칙 다섯 개 중 훅으로 바뀐 건 하나뿐이고, 나머지 넷이 훅으로 바꿀 수 있는 종류인지는 아직 분류하지 않았습니다. 그건 07의 열린 질문 목록에 그대로 남아 있어요.

이 하루에서 지금도 쓰는 기준은 이렇습니다.

  1. 훅은 프롬프트보다 확실하지만, 훅 자체가 안 깨졌을 때만 그렇습니다. 훅을 넣으면 훅이 깨지는 경우도 같이 시험해야 합니다.
  2. 훅이 깨질 때는 시끄럽게 깨져야 합니다. 조용히 통과시키는 실패는 규칙을 없애는 것과 같습니다.
  3. 훅 명령은 실행 시점의 현재 디렉터리를 믿으면 안 됩니다. 이번엔 저장소 루트로 먼저 이동하는 것으로 막았고, 다른 방법과는 비교하지 않았습니다.


함께 읽을 만한 글