SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 19:58 UTC

This page contains 146 additions and 119 deletions.

2026
Thu 1 23:59 Fri 2 19:58

mod로 이벤트에 반응하기

mod에서 Claude Code 이벤트를 처리합니다. 도구 호출, 프롬프트, 턴을 관찰하거나 재작성하거나 직접 응답하고, 훅이 처리할 이벤트를 필터링하며, 다른 mod와의 공존을 계획합니다.

훅은 이벤트 핸들러, 즉 이름이 지정된 이벤트가 발생할 때 Claude Code가 실행하는 함수입니다. Claude Code는 도구를 실행하거나, 프롬프트를 제출하거나, 모델에 요청을 보내거나, 세션을 시작하거나 종료하는 등 작업을 수행하려는 각 시점에 이벤트를 발생시킵니다. 훅은 Claude Code가 작업을 수행하기 전에 실행되므로 이벤트를 관찰하거나, 재작성하거나, Claude Code를 대신하여 응답할 수 있습니다. 훅은 on(eventName, handler)로 등록합니다.

여기서 시작하기 전에 첫 번째 mod를 먼저 만드십시오. 모든 이벤트와 정확한 필드는 레퍼런스를 참조하거나 사용 중인 빌드의 타입을 확인하십시오.

훅이 이벤트를 처리하는 방식

훅은 이벤트와 그 이벤트에 대해 Claude Code가 수행할 동작 사이에 위치하므로, 이벤트를 관찰하거나, 다시 작성하거나, 직접 응답할 수 있습니다. 훅은 세 개의 인수를 받습니다. $로 전달되는 mods API, e로 전달되는 이벤트, next로 전달되는 다음 핸들러입니다. 하나의 이벤트에 대한 핸들러들은 미들웨어 체인을 구성합니다. next(e)는 다음 핸들러를 호출하며, 이 핸들러는 다른 mod의 훅이거나 체인의 끝에서는 Claude Code 자체의 동작입니다. next(e)는 그 결과로 resolve됩니다. 훅이 next로 무엇을 하느냐에 따라 세 가지 중 어느 것을 수행할지가 결정됩니다.

이벤트 관찰하기

이벤트를 변경하지 않고 관찰하려면 원하는 작업을 수행한 뒤 next(e)를 반환합니다. 다음 훅은 Claude가 사용하려는 각 도구를 로그에 기록합니다.

on('tool.call', async ($, e, next) => {
  // Runs before the tool does
  $.ui.log('Claude is about to use ' + e.tool)
  // Pass the event on unchanged
  return next(e)
})

각 도구가 실행되기 전에 ● my-mod: Claude is about to use Bash와 같은 흐린 줄이 트랜스크립트에 표시되며, 여기서 my-mod는 플러그인 이름입니다. 도구는 mod가 없을 때와 동일하게 실행됩니다.

이벤트 이후에 동작하려면 await next(e)를 실행하고, 원하는 작업을 수행한 다음 결과를 반환합니다. 다음 훅은 각 도구가 실행된 후에 로그에 기록합니다.

on('tool.call', async ($, e, next) => {
  // Let the tool run, and wait for its result
  const result = await next(e)
  // Runs after the tool does
  $.ui.log(e.tool + ' finished')
  // Give the result back unchanged
  return result
})

이제 각 도구가 완료된 후에 해당 줄이 표시됩니다. 훅이 next(e)가 resolve된 값을 그대로 반환하므로, 어느 경우든 Claude는 동일한 결과를 읽습니다.

이벤트 다시 작성하기

프롬프트의 텍스트처럼 Claude Code가 처리하는 대상을 변경하려면, 이벤트의 수정된 복사본으로 next를 호출합니다. 이벤트 자체는 불변입니다. 깊게 동결(deeply frozen)되어 있어 필드에 값을 할당하면 오류가 발생합니다. 다음 훅은 각 프롬프트가 전송되기 전에 앞뒤 공백을 제거합니다.

on('prompt.submit', async ($, e, next) => {
  // Pass on a copy of the event with its text changed
  return next({ ...e, text: e.text.trim() })
})

이후의 핸들러와 Claude Code는 공백이 제거된 프롬프트를 받으며 원본은 보지 않습니다. 결과를 변경할 수도 있습니다. await next(e)를 실행한 다음, 필드 하나를 교체한 결과의 복사본을 반환하면 됩니다.

이벤트에 응답하기

이벤트를 직접 처리하려면 next를 호출하지 않고 결과를 반환합니다. 이렇게 하면 체인이 단락(short-circuit)되므로 이후의 mod와 Claude Code 자체의 동작이 실행되지 않습니다. 다음 훅은 모든 Bash 명령을 거부합니다.

on('tool.call', { tool: 'Bash' }, async () => {
  // No call to next, so the command never runs
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Claude가 Bash 명령을 시도하면 명령은 실행되지 않으며, Claude는 deny 텍스트를 도구의 결과로 읽습니다. 각 이벤트에는 고유한 결과 형태가 있으며, 이벤트 레퍼런스에 나열되어 있습니다.

훅이 처리할 이벤트 필터링하기

일부 이벤트에 대해서만 훅을 실행하려면 on의 두 번째 인수로 필터를 전달합니다. Claude Code에서는 이 필터를 matcher라고 부릅니다. matcher는 필드를 이벤트의 필드와 비교하는 객체이며, 모든 필드가 일치할 때만 훅이 실행됩니다. 필드 값으로는 단일 값, 허용되는 값의 배열, 또는 정규 표현식을 사용할 수 있습니다.

다음 예시의 각 줄은 동일한 함수 hook을 더 좁은 범위의 도구 호출에 대해 등록합니다.

// A string matches one value: Bash calls only
on('tool.call', { tool: 'Bash' }, hook)
// An array matches any value in it: Edit calls and Write calls
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// A regular expression matches by pattern: every tool of one MCP server
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook은 Bash, Edit, Write 호출에 대해 한 번, 그리고 이름이 mcp__github__로 시작하는 도구의 호출에 대해 한 번 실행됩니다. Read와 같은 다른 도구의 호출은 세 가지 중 어느 것과도 일치하지 않으므로 hook이 실행되지 않습니다.

이벤트 이름에는 와일드카드를 사용할 수 있습니다. 'classic.*'는 모든 설정 훅 이벤트와 일치합니다. '*'는 텔레메트리 이벤트를 제외한 모든 이벤트와 일치하며, 텔레메트리 이벤트는 고유한 이름과 { to: 'collector' } 필터를 사용합니다.

각 이벤트는 matcher당 한 번씩 등록합니다. matcher 없이 session.start에 대해 on을 두 번 호출하면 모듈이 on("session.start") is registered twice without a matcher 오류와 함께 로드에 실패합니다. 세션 시작 시 mod가 수행하는 모든 작업은 하나의 훅에 넣으십시오.

Claude가 하는 작업에 훅 연결하기

이 이벤트들을 처리하면 도구 호출, 프롬프트, 턴이 진행되는 동안 이를 확인하거나 변경할 수 있습니다. 모든 이벤트와 훅이 반환할 수 있는 값은 이벤트 레퍼런스를 참조하세요.

도구 호출 차단 또는 변경하기

tool.call 훅은 Claude가 사용하려는 각 도구를 확인하므로 호출을 거부하거나, 인수를 변경하거나, 그대로 통과시킬 수 있습니다. tool.call은 Claude Code가 도구를 실행하려 할 때 발생하며, 서브에이전트가 수행하는 호출과 MCP 도구 호출도 포함됩니다. e.tool은 도구의 이름이고, 도구의 인수는 Bash의 e.command처럼 e의 필드입니다. next(e)를 호출하면 Claude Code가 권한 검사를 실행한 다음 도구를 실행합니다.

다음 훅은 강제 푸시하는 Bash 명령을 거부하고 Claude에게 그 이유를 알려 줍니다.

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

Claude가 git push --force를 시도하면 훅이 next를 호출하지 않으므로 명령이 실행되지 않고 권한 프롬프트도 표시되지 않습니다. Claude는 deny 텍스트를 도구의 결과로 읽으므로, Claude가 따를 수 있는 지시문 형태로 작성해야 합니다. 그 밖의 모든 Bash 명령은 mod가 없을 때와 동일하게 실행됩니다.

도구가 실행된 후에 작업하려면 await next(e)를 수행하고, 필요한 작업을 한 다음, next가 반환한 값을 반환합니다. 다음 훅은 Claude가 변경하는 각 .mdx 파일을 $.ui.log로 로그에 기록합니다. 이 함수는 Claude가 읽지 않는 흐린 줄을 트랜스크립트에 추가합니다.

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

Claude가 .mdx 파일을 편집하거나 작성하면 트랜스크립트의 흐린 줄에 해당 파일 이름이 표시됩니다. 다른 종류의 파일이나 거부되거나 실패한 호출은 로그에 기록되지 않습니다. 훅이 받은 결과를 그대로 반환하므로 Claude가 보는 호출 내용은 변하지 않습니다.

호출을 변경하려면 변경된 인수를 next에 전달합니다. 호출을 재시도하려면 next(e)를 다시 호출합니다. 첫 번째 결과에서 isError를 확인한 훅은 도구를 한 번 더 실행하고 그 결과를 반환할 수 있습니다. 호출에 직접 응답하려면 next를 호출하지 않고 { result: 'Skipped by my-mod' }처럼 result 필드가 있는 객체를 반환합니다. 이렇게 하면 권한 프롬프트가 표시되지 않고 도구도 실행되지 않으므로, 반환한 결과가 Claude가 해당 상황에 대해 알게 되는 전부입니다.

조직의 관리형 설정에 있는 훅은 모든 mod의 tool.call 훅보다 먼저 실행되며, 이 훅 중 하나의 차단은 최종적입니다.

사용자가 결정할 때까지 도구 호출 보류하기

훅은 도구 호출을 일시 중지하고 진행하기 전에 사용자에게 어떻게 할지 물을 수 있습니다. tool.call 훅은 next를 호출하거나 반환하기 전에 await할 수 있으며, 그때까지 도구 호출은 대기 상태로 유지됩니다. 사용자에게 질문하려면 $.ui.ask를 호출합니다. 이 함수는 Claude가 사용자에게 질문할 때 사용하는 대화 상자에서 질문을 번호가 매겨진 옵션 목록 위에 표시하고, 사용자가 선택한 레이블로 resolve됩니다. 대화 상자는 옵션 뒤에 다른 답변을 입력하는 행과 Chat about this 행을 추가합니다.

이 예제의 RISKY 패턴은 rm -r, rm -rf, git reset --hard, 그리고 --force가 포함된 git push와 일치하며, git push -f 같은 다른 표기는 놓칩니다. 이 모듈은 패턴과 일치하는 Bash 명령을 실행하기 전에 확인을 요청합니다.

const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

Claude가 rm -rf build 같은 명령을 시도하면 해당 명령이 포함된 질문이 표시되고, 명령은 답변을 기다립니다.

  • 사용자가 Run it을 선택하는 경우: 훅이 next(e)를 호출하며, 그 후에도 일반적인 권한 검사가 계속 실행됩니다
  • 사용자가 Refuse를 선택하는 경우: 명령이 실행되지 않으며, Claude는 deny 텍스트를 읽습니다
  • 사용자가 답변을 입력하는 경우: $.ui.ask가 입력된 텍스트로 resolve됩니다. 훅은 이를 Run it과 비교하므로 그 외의 텍스트는 명령을 거부합니다.
  • 아무도 답변하지 않는 경우: 사용자가 질문을 닫거나 Chat about this를 선택하면, 그리고 claude -p 실행에서는 $.ui.ask가 reject되므로 catch 블록이 답변을 Refuse로 유지합니다

대기는 $.ui.ask 같은 mods API 호출 안에서 이루어지도록 해야 합니다. 그 시간은 훅의 시간 제한에 포함되지 않기 때문입니다. 직접 만든 promise를 기다리는 데 소요된 시간은 포함됩니다. Claude Code는 시간 초과된 훅을 건너뛰므로, 보류된 명령이 실행됩니다.

사용자에게 묻기 전에 도구 호출 승인 또는 거부하기

도구 호출의 실행 가능 여부를 결정하려면 Claude Code가 그 결정을 내리는 이벤트인 tool.check를 처리합니다. 이 이벤트는 권한 규칙과 설정 훅이 결정을 내린 후에 발생하며, next(e)는 그 결정인 allow, ask, deny 중 하나로 resolve됩니다. 훅은 그 결정이나 다른 결정을 반환합니다. e.input에는 Bash의 command처럼 도구의 인수가 들어 있습니다.

고정된 명령이나 경로에는 코드가 필요 없는 Bash(npm test) 같은 권한 규칙을 사용합니다. 현재 Git 브랜치나 다른 훅이 기록한 값처럼 그 시점의 상태에 따라 결정이 달라지는 경우에 tool.check를 처리합니다.

다음 훅은 현재 브랜치가 main일 때 git push를 거부합니다.

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

main에서는 규칙이 git push를 허용하더라도 훅이 deny를 반환합니다. 다른 브랜치에서, 그리고 다른 명령에 대해서는 mod가 없을 때와 동일한 결정이 적용됩니다.

이 훅은 명령의 텍스트를 대조하므로 Claude를 위한 알림 정도로 취급해야 합니다. 모든 사람의 main 푸시를 차단하려면 Git 호스트에서 브랜치를 보호하세요.

훅은 allow, ask, deny를 반환할 수 있으므로, 관리형 설정 외부의 PreToolUse 훅이 차단한 호출을 승인할 수도 있습니다. 훅으로 권한 확장하기에서 mod보다 우선하는 결정을 확인할 수 있습니다.

프롬프트 다시 작성하거나 내용 추가하기

prompt.submit 훅은 턴이 시작되기 전에 각 프롬프트를 확인하므로 텍스트를 다시 작성하거나 내용을 추가할 수 있습니다. e.text는 입력된 내용입니다.

수행할 작업 반환할 값
프롬프트를 다시 작성합니다. 트랜스크립트의 메시지에 새 텍스트가 표시됩니다. next({ ...e, text: newText })
프롬프트 뒤에 Claude만 읽는 텍스트를 추가합니다 next({ ...e, context: [...(e.context ?? []), extraText] })
프롬프트가 전송되지 않도록 합니다 { drop: 'the reason' }

다음 훅은 프롬프트에 풀 리퀘스트가 언급될 때마다 Claude를 위해 현재 브랜치 이름을 추가합니다.

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

open a PR for this change 같은 프롬프트를 보내면 트랜스크립트의 메시지는 그대로 보이며, Claude는 그 뒤에 Current branch: feature/auth 같은 줄도 읽습니다. 풀 리퀘스트를 언급하지 않는 프롬프트는 변경 없이 전달되며 git도 실행되지 않습니다.

다른 이벤트는 Claude가 읽는 나머지 내용을 다룹니다. 시스템 프롬프트의 각 섹션에는 prompt.section, 첫 번째 메시지와 함께 전송되는 컨텍스트에는 prompt.context, 스킬의 텍스트에는 skill.prompt를 사용합니다. 이러한 훅에서 나온 텍스트가 요청마다 달라지면 프롬프트 캐시가 무효화됩니다.

턴 추적하기

턴은 하나의 프롬프트에 응답하여 Claude가 수행하는 모든 작업입니다. 턴을 추적하려면 turn.start, turn.step, turn.complete를 처리합니다.

이벤트 발생 시점 훅이 할 수 있는 작업
turn.start 턴이 시작될 때 관찰합니다. e.turnId는 나머지 두 이벤트에서 해당 턴을 식별합니다.
turn.step Claude Code가 모델에 요청 하나를 보내려 할 때입니다. 도구 호출이 있는 턴에는 요청이 여러 개 있습니다. 서브에이전트의 요청에는 e.agentId가 설정됩니다. 각 요청의 토큰 사용량을 읽거나, next({ ...e, model })로 다른 모델에 보내거나, 모델을 호출하지 않고 응답합니다
turn.complete 턴이 종료되었을 때이며, 사용자가 중단한 턴도 포함됩니다. 이 경우 e.isAborted는 true입니다. e.answer는 Claude의 최종 텍스트, e.durationMs는 소요 시간, e.usage는 턴의 토큰 합계입니다. 서브에이전트의 턴에서는 e.agentId가 설정된 상태로 발생합니다. 관찰하거나, { text: 'Done in 12 seconds' }처럼 text 필드가 있는 객체를 반환하여 답변 아래에 한 줄을 표시합니다

이 이벤트는 스트리밍되므로 turn.step 훅은 async generator로 작성합니다. yield* next(e)는 응답을 스트리밍되는 대로 전달하고 완료된 결과로 평가됩니다. 다음 훅은 각 요청 중 Claude API가 프롬프트 캐시에서 제공한 양을 로그에 기록합니다.

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

Claude의 응답은 mod가 없을 때와 동일하게 화면에 스트리밍됩니다. 각 요청이 완료되면 트랜스크립트의 흐린 줄에 캐시에서 읽은 토큰 수와 캐시에 기록된 토큰 수가 표시됩니다. 도구 호출이 있는 턴에는 요청이 여러 개 있으므로 여러 줄이 추가됩니다.

result.usage에는 Claude API가 요청에 대해 보고하는 토큰 수인 input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens와 응답한 model이 들어 있습니다. 이 훅은 서브에이전트의 요청에도 실행되므로, 메인 대화만 원하는 경우 e.agentId를 확인하세요.

설정 훅 이벤트 처리하기

설정 훅은 설정 파일에서 구성하는 command, HTTP, prompt, agent 훅입니다. Stop, SessionEnd, PostToolUse 같은 각 설정 훅 이벤트는 classic. 뒤에 설정 훅 이벤트 이름이 붙은 이벤트(예: classic.Stop)이기도 합니다. e는 설정 훅이 stdin으로 받는 JSON이며, transcript_path를 포함합니다.

다음 훅은 Claude가 응답을 마칠 때 발생하는 Stop을 사용하여 세션의 트랜스크립트가 저장된 위치를 로그에 기록합니다.

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

Claude가 응답을 마칠 때마다 트랜스크립트의 흐린 줄에 트랜스크립트 파일의 경로가 표시됩니다. 훅이 next(e)를 반환하므로 이벤트를 관찰만 하며 턴이 종료되는 방식은 전혀 변경하지 않습니다.

다른 mod와 함께 실행하기

여러 mod가 같은 이벤트를 처리할 수 있으며, 그중 어느 것이든 실패할 수 있습니다. mod가 도구 호출을 차단한다면 체인에서 해당 mod의 위치와 훅이 실패할 때 어떤 일이 일어나는지 확인하십시오.

mod가 실행되는 순서

같은 이벤트에 대한 훅은 하나의 미들웨어 체인을 형성합니다. 각 mod의 next는 다음 mod의 훅을 호출하며, 마지막 next는 Claude Code 자체의 동작에 도달합니다. 첫 번째 mod가 가장 바깥쪽에 있습니다. 이 mod는 다른 mod보다 먼저 이벤트를 보고 다른 mod보다 나중에 결과를 보며, 다른 mod의 실행 여부를 결정합니다. 뒤에 있는 mod는 앞에 있는 mod가 이벤트를 보는 것을 막을 수 없습니다.

Claude Code는 각 mod의 출처에 따라 체인의 순서를 정합니다.

  1. 기본 제공 가드 sec-default@builtin(로드되는 경우, /plugin에서 cc-plugin-sec-default로 표시되는 Claude Code 내장 mod), 조직이 prependPlugins에 나열한 mod, 그리고 조직의 mod로 간주되면서 appendPlugins에 없는 그 밖의 mod
  2. 사용자가 설치한 mod
  3. 조직이 appendPlugins에 나열한 mod
  4. Claude Code에 내장된 그 밖의 mod

사용자가 설치한 mod 중에서는 mod가 매니페스트의 dependencies 아래에 나열한 mod보다 먼저 실행됩니다. 하나의 모듈 안에서는 register가 on을 호출한 순서대로 훅이 실행됩니다.

설정 훅이 실행되는 순서상의 위치

설정 파일에 구성된 PreToolUse 훅도 도구 호출 중에 mod 체인의 고정된 지점에서 실행됩니다.

  • 관리형 설정의 PreToolUse 훅: 첫 번째 mod의 tool.call 훅보다 먼저 실행되며, 이 훅 중 하나가 차단하면 그 결정이 최종이므로 어떤 mod도 해당 호출을 보지 못합니다.
  • 그 밖의 모든 설정 파일과 플러그인의 hooks/hooks.json에 있는 PreToolUse 훅: Claude Code 자체 동작의 일부로서 마지막 mod가 next를 호출한 후에 실행됩니다. next를 호출하지 않고 tool.call에 응답하는 mod는 이 훅의 실행을 막으며, next를 호출하는 mod는 반환되는 결과에서 이 훅의 결정을 확인할 수 있습니다.

tool.check는 이러한 훅과 권한 규칙이 결정을 내린 후에 발생하므로, 여기에 연결된 훅은 두 번째 그룹의 훅이 차단한 호출을 승인할 수 있습니다.

실패한 훅 처리하기

훅이 실패해도 세션이 중단되지는 않으며, 대신 어떤 일이 일어날지 직접 결정할 수 있습니다. .catch 핸들러가 없는 훅이 예외를 던지거나, 시간 초과되거나, 잘못된 형태의 결과를 반환하면, 이후 동작은 해당 훅이 next를 호출했는지에 따라 달라집니다.

  • next를 호출하기 전에 실패한 경우: Claude Code는 해당 훅을 건너뛰고 그 자리에서 다음 핸들러를 실행합니다
  • next가 완료된 후에 실패한 경우: 해당 결과가 그대로 유지되며, 어떤 것도 다시 실행되지 않습니다

mod, 이벤트, 이유를 나타내는 한 줄이 기록됩니다(예: my-mod: tool.call hook skipped: threw Error: boom). 이 내용을 확인하는 위치는 mod가 아무 동작도 하지 않는 이유 찾기에 나와 있듯이 세션에 따라 다릅니다. 그리기 결과가 유효성 검사를 통과하지 못한 ui.render 훅은 요소로 트리 만들기에 설명된 대로 다른 방식으로 보고됩니다.

호출을 차단하는 훅이 실패 시 닫힌 상태(fail closed)가 되도록 하려면, 그 자리에서 대신 응답하는 .catch 오류 핸들러를 추가하십시오. 여기서 guard는 사용자의 훅 함수입니다.

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind is 'throw' or 'timeout', which says how guard failed
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

guard가 정상적으로 작동하는 동안에는 핸들러가 실행되지 않습니다. guard가 Bash 호출에서 예외를 던지거나 시간 초과되면, Claude Code는 같은 이벤트로 핸들러를 호출합니다. 핸들러가 { deny }를 반환하므로 명령은 실행되지 않으며, Claude는 끝에 throw 또는 timeout이 붙은 텍스트를 읽습니다. 핸들러가 없다면 Claude Code는 guard를 건너뛰고 명령을 실행합니다. 핸들러에는 자체적으로 더 짧은 시간 제한이 적용됩니다.

다음 단계