Hooks 매처: 와일드카드와 패턴 지정

매처(matcher)는 훅이 어떤 도구나 상황에서 발화할지 거르는 문자열 필터입니다. 빈 문자열, 도구명, 파이프 OR, 정규식, SessionStart 전용 값, 그리고 명령 내용은 if 필드와 스크립트로 판별하는 요령을 정리합니다.

20분 갱신

훅 설정의 두 번째 겹, matcher를 제대로 봅니다. 앞 글들에서 Bash, Edit|Write, compact 같은 값을 이미 썼지만, 어떤 문자가 들어가면 정규식이 되고, 이벤트마다 무엇과 비교되며, Bash(npm:*) 같은 명령 패턴은 왜 안 되는지는 다루지 않았습니다. 이 글은 그 규칙을 한 번에 정리하고, 실무에서 자주 걸리는 대소문자·병렬 실행·정규식 앵커 문제를 짚습니다. 다음 글은 훅을 기다리지 않게 하는 async 옵션입니다.

핵심 요약

  • matcher는 문자, 숫자, _, -, 공백, ,, |만 있으면 정확 일치(또는 |·,로 나눈 목록), 그 밖의 문자가 하나라도 있으면 앵커 없는 JavaScript 정규식으로 평가됩니다.
  • 비교 대상은 이벤트마다 다릅니다. 도구 이벤트는 tool_name, SessionStart는 source, SubagentStop은 agent_type, Notification은 알림 유형입니다.
  • Stop, UserPromptSubmit, PostToolBatch 등은 matcher가 없는 이벤트입니다. 적어도 무시됩니다.
  • 명령 내용 필터는 matcher가 아니라 핸들러의 if 필드("Bash(git *)") 또는 스크립트 안의 판별입니다.
  • matcher는 대소문자를 구별하고, 겹치는 matcher 그룹은 전부 병렬로 실행됩니다.

매처는 settings.json 어디에 쓰나

이벤트 배열의 각 원소가 matcher 그룹이고, 그 안에 matcher와 hooks가 있습니다.

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-bash.sh" } ] },
      { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-files.sh" } ] }
    ]
  }
}

한 이벤트에 그룹을 여러 개 두어 도구별로 다른 스크립트를 붙이는 것이 기본 패턴입니다. matcher는 stdin JSON의 특정 필드와 비교되는데, 그 필드가 이벤트마다 다릅니다.

이벤트 비교 필드 예시 값
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied 도구 이름 Bash, Edit|Write, mcp__.*
SessionStart 시작 방식 startup, resume, clear, compact, fork
SessionEnd 종료 이유 clear, resume, logout, prompt_input_exit, other
Notification 알림 유형 permission_prompt, idle_prompt, auth_success
SubagentStart, SubagentStop 에이전트 타입 Explore, Plan, general-purpose, 사용자 정의 이름
PreCompact, PostCompact 압축 트리거 manual, auto
ConfigChange 설정 출처 user_settings, project_settings, local_settings, policy_settings, skills
FileChanged 감시할 파일명(리터럴) .envrc|.env
StopFailure 오류 유형 rate_limit, authentication_failed, server_error
UserPromptExpansion 명령·스킬 이름 슬래시 명령 이름
Elicitation, ElicitationResult MCP 서버 이름 설정한 서버 이름
UserPromptSubmit, Stop, PostToolBatch, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay, CwdChanged 없음 항상 발화

기본 매처 패턴 5가지

빈 문자열로 모든 도구에 매치

"", "*", 또는 matcher 키 자체를 생략하면 그 이벤트의 모든 발생에 반응합니다. 감사 로그처럼 "전부 기록"이 목적일 때 씁니다. 다만 PermissionRequest처럼 결정을 내리는 이벤트에서 전부 매치 후 allow를 돌려주면 파일 쓰기와 셸 명령까지 자동 승인되므로 공식 문서도 matcher를 최대한 좁히라고 경고합니다.

도구명으로 매치

"Bash", "Write", "Agent"처럼 정확한 이름입니다. 정확 일치라서 "Edit"는 NotebookEdit을 잡지 않습니다. MCP 도구는 mcp__<서버>__<도구> 전체 이름을 적습니다. mcp__memory처럼 서버 접두사만 적으면 문자와 밑줄뿐이라 정확 일치로 비교되어 아무것도 잡히지 않습니다. 서버 전체를 잡으려면 mcp__memory__.*처럼 .*를 붙여 정규식으로 만들어야 합니다.

OR 조건으로 여러 도구에 매치

"Edit|Write"가 표준이고, v2.1.191 이상에서는 "Edit, Write"처럼 쉼표와 공백도 같은 뜻입니다. 하이픈이 든 이름(code-reviewer)을 정확 일치 집합에 넣는 것은 v2.1.195 이상에서 지원되며, 그 이전 버전은 하이픈 때문에 정규식으로 평가되어 senior-code-reviewer까지 잡습니다. 구버전 호환이 필요하면 ^code-reviewer$로 앵커를 겁니다.

명령 패턴은 매처가 아니라 if 필드로

"matcher": "Bash(npm:*)"는 동작하지 않습니다. matcher는 tool_name과만 비교되고, 괄호가 들어가면 정규식이 되어 도구 이름 Bash와 일치하지 않기 때문입니다. 명령 내용으로 거르려면 핸들러 안에 if를 둡니다.

{
  "matcher": "Bash",
  "hooks": [
    { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/git-policy.sh", "if": "Bash(git *)" },
    { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ts-check.sh", "if": "Edit(*.ts)" }
  ]
}

if는 권한 규칙 문법 하나만 받습니다. &&나 목록은 없고, 조건이 여럿이면 핸들러를 나눕니다. 도구 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서만 평가되고 다른 이벤트에 if가 있으면 그 훅은 아예 실행되지 않습니다. Bash 패턴은 npm test && git push처럼 이어진 명령의 각 부분, $()와 백틱 안까지 검사하며, 파싱이 안 되는 명령에서는 훅을 그냥 실행하는 "최선 노력" 필터입니다.

SessionStart용 매처

startup, resume, clear, compact, fork 다섯 값이며 "startup|resume"처럼 묶을 수 있습니다. 우리 전역 설정은 인박스 확인에 빈 matcher, 규칙 재주입에 compact를 씁니다. 같은 원리로 Notification은 permission_prompt와 idle_prompt를 나눠 서로 다른 알림을 걸고, SubagentStop은 에이전트 타입으로 나눕니다.

Bash 명령은 스크립트 안에서 어떻게 판별하나

if로 부족한 판단(브랜치, 호스트, 경로 조합)은 스크립트에서 tool_input.command를 읽어 결정합니다.

#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
BRANCH=$(git -C "$CLAUDE_PROJECT_DIR" branch --show-current 2>/dev/null)

case "$CMD" in
  *"git push"*"main"*|*"git push"*"master"*)
    echo "차단: main 직접 push는 금지입니다. develop으로 PR을 올리세요." >&2
    exit 2 ;;
  *"prisma migrate deploy"*|*"drop table"*)
    if [ "$BRANCH" != "develop" ]; then
      echo "차단: 마이그레이션은 develop 브랜치에서만 실행합니다. 현재: $BRANCH" >&2
      exit 2
    fi ;;
esac
exit 0

이 스크립트가 우리 "main 절대 금지, develop까지만" 브랜치 규칙의 훅 버전입니다. matcher는 Bash 하나로 넓게 잡고, 세부 판단은 전부 스크립트에 둡니다. 이렇게 하면 규칙이 바뀔 때 JSON을 건드리지 않고 스크립트만 고치면 됩니다.

매처 설정 시 주의할 점

대소문자를 구별한다

정확 일치는 대소문자를 그대로 비교합니다. "bash", "edit"는 아무 도구도 잡지 못하고, 훅이 "안 도는" 원인 1위입니다. 정규식 경로도 기본은 대소문자 구별입니다. 훅이 안 돌면 /hooks에서 matcher 철자부터 봅니다.

여러 매처가 겹치면 모두 병렬 실행된다

"Bash" 그룹과 "" 그룹이 함께 있으면 Bash 호출에서 둘 다 발화합니다. 순서는 보장되지 않고 병렬로 시작됩니다. 결과는 합쳐지는데, PreToolUse 결정은 가장 제한적인 것이 이깁니다(deny > defer > ask > allow). 한 훅이 deny해도 다른 훅의 실행이 취소되지는 않으므로, 로그 훅은 차단 여부와 무관하게 로그를 남깁니다. 같은 명령 문자열(그리고 args)의 핸들러는 중복 제거되어 한 번만 돕니다.

정규 표현식을 지원한다

허용 문자 외의 것이 하나라도 있으면 JavaScript RegExp로 평가되며 앵커가 없습니다. Edit.*는 Edit뿐 아니라 NotebookEdit도 잡고, ^Notebook은 Notebook으로 시작하는 도구만 잡습니다. 전체 일치가 필요하면 ^Edit$처럼 양쪽을 고정합니다. 플러그인이 제공하는 서브에이전트는 my-plugin:reviewer처럼 콜론이 들어가 자동으로 정규식이 되므로 ^my-plugin:reviewer$로 고정하는 것이 공식 권장입니다. 플러그인이 번들한 MCP 서버의 도구는 mcp__plugin_<플러그인>_<서버>__<도구> 형식이라 mcp__db__.*로는 잡히지 않고 mcp__plugin_my-plugin_db__.*로 적어야 합니다.

정리

  • matcher의 문자 구성이 정확 일치인지 정규식인지를 결정합니다. 특수문자 하나가 의미를 바꿉니다.
  • 비교 필드는 이벤트마다 다르고, Stop·UserPromptSubmit 등은 matcher가 없습니다.
  • 명령 내용은 if 필드, 더 복잡한 판단은 스크립트 안에서 tool_input을 읽어 처리합니다.
  • 대소문자 구별, 병렬 실행과 가장 제한적인 결정 우선, 앵커 없는 정규식 세 가지가 실무 함정입니다.
  • 넓은 matcher + 스크립트 내부 판단이 유지보수에 유리합니다.

자주 묻는 질문

매처를 비워 두면 어떻게 되나요?

"", "*", 생략 모두 그 이벤트의 모든 발생에 반응합니다. 감사 로그에는 편하지만, PermissionRequest나 PreToolUse에서 전부 매치 후 allow를 돌려주는 구성은 사실상 자동 승인이 되므로 피해야 합니다.

Bash(npm:*) 같은 명령 패턴을 매처에 쓸 수 있나요?

없습니다. matcher는 도구 이름(tool_name)과만 비교됩니다. 명령 내용으로 거르려면 핸들러의 if 필드에 "Bash(npm *)"처럼 권한 규칙 문법으로 적거나, 스크립트에서 tool_input.command를 읽어 판별합니다. if는 도구 이벤트에서만 동작하는 최선 노력 필터입니다.

같은 이벤트에 여러 매처가 걸리면 어떤 순서로 실행되나요?

순서가 없습니다. 매칭된 핸들러는 전부 병렬로 시작되고, 완료 후 결과가 병합됩니다. PreToolUse의 권한 결정은 가장 제한적인 값이 우선하고, additionalContext는 모두 합쳐집니다. 순서 의존이 있다면 한 스크립트로 묶으세요.

매처는 대소문자를 구별하나요?

네. 정확 일치는 Bash와 bash를 다르게 봅니다. 정규식 경로도 기본적으로 대소문자를 구별합니다. 훅이 발화하지 않을 때 가장 먼저 /hooks에서 철자를 확인하세요.

Stop 훅이나 SubagentStop 훅에도 매처가 있나요?

Stop은 matcher가 없는 이벤트라 적어도 무시됩니다. SubagentStop은 있습니다. 에이전트 타입(Explore, Plan, general-purpose, 사용자 정의 서브에이전트의 name)과 비교되며 플러그인 에이전트는 ^플러그인:이름$으로 고정해 적습니다.

팀에 클로드코드를 도입하려면 실제 업무에 맞춘 설계가 필요합니다. 무료 상담 신청 →