SpyBara
Go Premium

plugins/mods/api.md 2026-11-06 23:01 UTC to 2026-11-07 23:58 UTC

No changes for this document in this history range.

2026
Mon 2 23:57

mods API 사용하기

Claude Code mod에서 mods API를 호출하여 명령과 도구를 추가하고, 모델을 호출하고, 타이머로 작업을 실행하고, 다른 세션에 메시지를 보내고, 파일과 네트워크에 접근합니다.

mods API는 mod가 동작을 수행하기 위해 호출하는 메서드 집합으로, 명령과 도구를 추가하고, 모델을 호출하고, 이벤트 사이에 작업을 실행하고, 파일 시스템, 프로세스, 네트워크에 접근하는 데 사용됩니다. 모든 훅은 이 API를 첫 번째 인수인 $로 받으며, 메서드는 $.ui 및 $.fs와 같은 네임스페이스로 그룹화되어 있습니다. 이벤트는 훅이 실행되는 시점을 결정하고, mods API는 훅이 실행된 후 호출하는 대상입니다.

여기서 시작하기 전에 첫 번째 mod를 먼저 만들어 보십시오. 모든 메서드에 대해서는 mods API 메서드를 참조하거나 사용 중인 빌드의 타입을 확인하십시오.

명령 또는 도구 추가하기

mod는 사용자가 실행할 명령과 Claude가 호출할 도구를 추가할 수 있습니다. 둘 다 session.start 훅에서 등록합니다. Claude Code는 첫 번째 프롬프트 전에 이 훅이 완료되기를 기다리므로, 등록한 항목은 첫 번째 턴부터 사용할 수 있습니다.

명령 추가하기

명령은 사용자를 위한 것입니다. 명령을 등록한 다음, 해당 이름에 대한 command.run을 처리합니다. 다음 예제는 선택적으로 일수를 받는 /standup 명령을 추가합니다.

on('session.start', async ($, e, next) => {
  // Add /standup to the command list, with the description the user sees there
  await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
  return next(e)
})

// The matcher limits the hook to /standup, so other commands don't reach it
on('command.run', { command: 'standup' }, async ($, e) => {
  // e.args is the text typed after the command name, or an empty string
  return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})

세션이 시작되면 /를 입력했을 때 표시되는 목록에 /standup이 설명과 함께 나타납니다. argumentHint는 명령과 공백을 입력한 후 프롬프트에 /standup [days]처럼 표시됩니다. /standup 3을 실행하면 두 번째 훅이 Summary for the last 3 day(s): ...를 반환하고, 트랜스크립트에는 플러그인 이름 뒤에 이 텍스트가 표시됩니다. 이 명령에는 직접 정의한 동작 외에 다른 동작이 없으므로 훅은 next를 호출하지 않습니다.

반환한 text는 트랜스크립트에 출력되며 Claude가 이를 읽습니다. pane만 여는 명령처럼 아무것도 출력하지 않으려면 {}를 반환합니다. Claude가 작업 중일 때도 명령을 실행할 수 있게 하려면 등록 시 immediate: true를 추가합니다.

기본 제공 명령이 사용하지 않는 이름을 선택하십시오. 세션에서 /를 입력하면 기본 제공 명령을 확인할 수 있습니다. 이미 사용 중인 이름이면 $.command.register는 "/focus" refused: it is the built-in /focus와 같은 메시지와 함께 예외를 발생시킵니다. 예외를 발생시킨 훅은 건너뛰어지므로 session.start 훅의 나머지 부분도 실행되지 않습니다. 명령은 해당 훅의 마지막에 등록하거나, 호출을 try와 catch로 감싸십시오.

도구 추가하기

도구는 Claude를 위한 것입니다. 이름, Claude가 읽는 설명, 입력에 대한 JSON Schema와 함께 도구를 등록합니다. Claude에게는 mcp__, 플러그인 이름, 밑줄 두 개, 등록한 이름으로 구성된 더 긴 이름으로 표시됩니다. 도구 호출은 이 전체 이름으로 필터링한 tool.call 훅에서 처리합니다. my-mod라는 플러그인의 다음 예제는 ticket을 등록하므로 전체 이름은 mcp__my-mod__ticket이 됩니다. 이 예제는 이슈 트래커에서 티켓을 조회하는 도구를 Claude에게 제공합니다.

on('session.start', async ($, e, next) => {
  await $.tool.register({
    name: 'ticket',
    // Claude decides when to call the tool from this description
    description: 'Look up a ticket by its id and return its title and status',
    // The arguments Claude has to send: one required string named id
    inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  })
  return next(e)
})

// The full tool name is mcp__, the plugin's name, and the registered name
on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
  // The tool's arguments are fields of e, so the id is e.id
  const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
  // Return a result either way, so Claude learns when the lookup failed
  return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})

티켓에 대해 질문하면 Claude는 티켓 id와 함께 mcp__my-mod__ticket을 호출할 수 있습니다. 두 번째 훅은 티켓을 가져와 응답 본문을 반환하며, Claude는 이를 도구의 결과로 읽습니다. 서버가 오류 상태로 응답하면 Claude는 Lookup failed with status와 상태 번호를 읽습니다.

[!TIP]

MCP 도구 검색이 등록된 도구를 지연 로드하면, Claude는 해당 도구를 검색하기 전까지 이름만 볼 수 있고 설명은 볼 수 없습니다. Claude가 매 턴마다 이 도구를 고려해야 한다면 등록 시 isDeferred: false를 추가하여 전체 도구를 미리 로드하십시오. 이 필드는 Claude Code v2.1.293 이상이 필요하며, 이전 버전에서는 무시됩니다.

모델 호출하기

mod는 텍스트를 분류하거나 요약하는 등의 작은 작업을 위해 모델에 자체적인 요청을 보낼 수 있습니다. $.model.complete는 프롬프트만 단독으로 보내고, $.model.fork({ prompt })는 현재 대화의 끝에 프롬프트를 붙여 보냅니다.

다음 표는 각 요청에 포함되는 내용을 비교합니다.

요청에 포함되는 항목 $.model.complete $.model.fork
모델 전달한 model 세션의 모델
시스템 프롬프트 짧은 attribution 블록, 그리고 system을 전달한 경우 그 내용 세션의 시스템 프롬프트
메시지 전달한 prompt로 된 사용자 메시지 하나 지금까지의 대화, 그리고 사용자 메시지로 전달한 prompt
CLAUDE.md 및 기타 프로젝트 컨텍스트 포함되지 않음 대화의 마지막 요청과 동일하게 포함됨
도구 없음 Claude의 도구(모델이 호출할 수는 없음)

fork는 대화의 마지막 요청을 반복하므로, 대화가 아직 캐시되어 있는 동안에는 Claude API가 대부분을 프롬프트 캐시에서 처리합니다.

두 호출 모두 세션의 자격 증명을 사용하므로 사용자의 플랜, API 키 또는 클라우드 제공업체로 청구됩니다. 사용 중인 빌드의 타입에는 모든 $.model 메서드가 문서화되어 있습니다.

프롬프트 하나 보내기

$.model.complete에 model과 prompt를 전달합니다. prompt는 사용자 메시지가 됩니다. 역할이나 출력 형식과 같은 지침을 모델에 제공하려면 system도 함께 전달합니다. 이 값은 시스템 프롬프트가 됩니다.

다음 훅은 명령으로 등록된 /triage 명령에 응답하며, 작은 모델에 명령 뒤에 입력된 텍스트의 레이블을 지정하도록 요청합니다.

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    // The system prompt sets the job, and the prompt carries the text to label
    system: 'Reply with one word: bug, feature, or question.',
    prompt: e.args,
    // One word needs few tokens, and the call gives up after 15 seconds
    maxTokens: 20,
    timeoutMs: 15000,
  })
  // r.text exists only when the model answered, so check r.isAnswered first
  const label = r.isAnswered ? r.text.trim() : 'unknown'
  return { text: 'Label: ' + label }
})

/triage the export button does nothing를 실행하면 mod가 해당 텍스트를 모델에 보내고 Label: bug와 같은 응답을 출력합니다. 모델이 응답하지 않으면 레이블은 unknown이 됩니다.

Claude API 오류가 발생해도 호출이 reject되지는 않으므로 r.isAnswered를 확인하고, 값이 false이면 r.reason을 읽어야 합니다. 조직에서 차단한 모델처럼 Claude Code가 보내지 않는 요청의 경우에는 호출이 reject됩니다.

사용 중인 빌드의 타입에는 effort와 같은 다른 옵션이 나열되어 있으며, 제한에서 maxTokens 기본값을 확인할 수 있습니다.

프롬프트 캐싱 사용하기

$.model.complete는 Claude API의 프롬프트 캐싱을 지원합니다. API는 요청의 시작 부분, 즉 프리픽스를 사용자가 설정한 캐시 브레이크포인트까지 캐시합니다. 모든 호출이 지침이나 참고 자료처럼 동일한 길고 정적인 콘텐츠로 시작한다면, 해당 콘텐츠의 끝에 브레이크포인트를 설정합니다. 그러면 이후 호출은 해당 콘텐츠에 대해 전체 입력 가격을 지불하는 대신 캐시에서 읽어 옵니다.

브레이크포인트를 설정하려면 prompt를 문자열 대신 { text } 블록의 배열로 전달하고, 정적 콘텐츠의 마지막 블록에 cache: true를 추가합니다. Claude Code는 해당 블록을 API의 cache_control 필드와 함께 보냅니다. system도 동일한 배열 형식을 사용할 수 있습니다. 둘 중 어느 것을 사용할지 결정하려면 prompt와 system 중에서 선택하기를 참조하세요.

[!NOTE]

블록 배열을 사용하려면 Claude Code v2.1.292 이상이 필요합니다. 이전 버전에서는 prompt의 배열을 takes { model, prompt } (host check)로 끝나는 오류와 함께 거부하며, system의 배열은 요청에서 제외합니다.

다음은 /triage 훅의 다른 버전으로, 레이블을 지정할 텍스트 앞에 긴 레이블링 규칙을 보내고 규칙 뒤에 브레이크포인트를 둡니다. RULES는 사용자가 직접 작성한 문자열입니다.

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    prompt: [
      // Identical on every call, so it forms the cached prefix
      { text: RULES, cache: true },
      // Changes on every call, so it goes after the breakpoint
      { text: e.args },
    ],
  })
  return { text: 'Label: ' + (r.isAnswered ? r.text.trim() : 'unknown') }
})

TTL과 브레이크포인트 수에는 다음과 같은 제한이 있습니다.

  • TTL: 캐시 항목은 마지막으로 사용된 후 5분 동안 유지됩니다. TTL은 호출이 아니라 사용자의 Claude Code 설정에 따라 결정됩니다. 1시간으로 설정하려면 subagentPromptCacheTtl을 1h로 설정합니다.
  • 요청당 브레이크포인트: API는 최대 4개까지 허용하며, 하나를 더 추가하면 r.reason에 api-error가 반환됩니다

prompt와 system 중에서 선택하기

요청이 Claude API로 직접 전송된다는 것을 알고 있는 경우가 아니라면, 호출 간에 공유되는 정적 콘텐츠를 prompt의 시작 부분에 배치합니다.

  • API 키 또는 Claude 구독을 사용해 Claude API로 직접 전송하는 경우: 어느 필드든 사용할 수 있습니다
  • Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, Microsoft Foundry 또는 LLM 게이트웨이를 거치는 경우: prompt를 사용합니다. Claude Code는 시스템 프롬프트를 attribution 블록으로 시작하며, 이 블록의 fingerprint는 사용자 메시지의 시작 부분에서 생성됩니다. api.anthropic.com 엔드포인트는 캐싱 전에 이 블록을 제거합니다. 다른 엔드포인트는 이 블록을 프롬프트의 일부로 받으므로, prompt의 시작 부분이 달라지면 system의 브레이크포인트가 캐시 미스를 일으킬 수 있습니다.
  • 다른 사람이 실행하는 mod인 경우: 그들의 제공업체를 선택할 수 없으므로 prompt를 사용합니다

프리픽스에서 system은 prompt보다 앞에 오므로, prompt의 브레이크포인트는 system까지 포함하며, system이 다른 호출은 캐시 미스가 발생합니다.

캐시 적중 확인하기

$.model.complete의 결과에는 API의 캐시 필드가 담긴 usage 객체가 있습니다. usage.cache_creation_input_tokens는 호출이 캐시에 기록한 토큰 수를, usage.cache_read_input_tokens는 캐시에서 읽은 토큰 수를 나타냅니다. 첫 번째 호출에서는 기록이, TTL 내의 이후 호출에서는 읽기가 발생해야 합니다.

모든 호출이 기록만 하고 읽기가 전혀 없다면, 호출 간에 프리픽스가 다르거나 호출 간격이 TTL보다 깁니다. 프리픽스가 다른 경우에 대해서는 prompt와 system 중에서 선택하기를 참조하세요.

모델이 응답한 호출에서 두 필드가 모두 0으로 유지된다면 아무것도 캐시되지 않은 것입니다. 다음 각 원인을 확인하세요.

model.complete 훅이 받는 내용

다른 mod의 요청을 검사하거나 변경하기 위해 model.complete 이벤트에 훅을 연결하는 경우, 다음 필드에서 텍스트를 읽습니다.

  • e.prompt: 항상 문자열입니다. 호출자가 배열을 전달한 경우 블록의 텍스트를 순서대로 연결한 값입니다.
  • e.system: 같은 방식으로 만들어진 문자열이며, 호출자가 system을 전달하지 않은 경우에는 없습니다
  • e.promptBlocks 및 e.systemBlocks: 호출자의 배열이며, 각각 호출자가 해당 필드에 배열을 전달한 경우에만 존재합니다

Claude Code는 훅이 next에 전달한 문자열을 보내고, 함께 전달한 배열을 사용해 캐시 브레이크포인트를 배치합니다. 문자열의 시작 부분과 여전히 일치하는 앞쪽 블록은 브레이크포인트와 함께 유지하고, 문자열의 나머지 부분은 브레이크포인트 없이 보냅니다. 예를 들어 next({ ...e, prompt: e.prompt + NOTE })는 호출자의 브레이크포인트를 유지하지만, prompt의 시작 부분을 변경하는 훅은 브레이크포인트를 제거합니다.

백그라운드에서 작업 실행하기

1분마다 무언가를 확인하는 것처럼 하나의 이벤트보다 오래 지속되는 작업은 session.start에서 시작하는 타이머로 실행됩니다. 훅 자체는 하나의 이벤트에 대해 실행되며, 자체 실행 시간에 시간 제한이 있습니다. next 또는 mod API 호출을 기다리는 데 소요된 시간은 $.clock.sleep을 제외하고 포함되지 않습니다. $.clock.every와 $.clock.after는 setInterval과 setTimeout을 대신하며, 지연 시간(밀리초)을 첫 번째 인수로 받습니다. $.clock.after(5000, fn)은 지금부터 5초 후에 fn을 한 번 호출합니다. 각각 cancel() 메서드가 있는 타이머를 반환하며, await $.clock.now()는 현재 시간을 밀리초 단위로 제공합니다.

다음 훅은 1분마다 풀 리퀘스트의 검사 결과를 조회하여 프롬프트 아래에 표시합니다. summarize는 명령의 JSON 출력을 몇 단어로 요약하는 사용자 정의 함수입니다.

on('session.start', async ($, e, next) => {
  // Call the function every 60,000 milliseconds, starting one minute from now
  $.clock.every(60_000, async () => {
    const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state'])
    // Replace the line under the prompt with the latest summary
    $.ui.status('checks: ' + summarize(status.stdout))
  })
  // Return without waiting for the timer, so the session starts right away
  return next(e)
})

세션은 평소처럼 시작됩니다. 1분 후 프롬프트 아래에 ⚠, mod 이름, 그리고 checks:와 요약이 담긴 줄이 나타납니다. 이후 이 줄은 1분마다 교체됩니다. 타이머의 콜백은 어떤 이벤트에도 속하지 않고 실행되므로, 턴 사이에도 계속 실행되며 턴을 시작하지 않습니다. 콜백에서 오류가 발생하면 오류는 디버그 로그에 기록되고, 타이머는 다음 간격에 다시 실행됩니다.

턴을 시작하지 않고 내용 표시하기

백그라운드 작업은 턴을 시작하지 않고도 사용자에게 내용을 표시할 수 있습니다. 다음 각 호출은 텍스트를 서로 다른 위치에 표시합니다.

호출 사용자에게 표시되는 내용
$.ui.status(text) 변경할 때까지 유지되는 프롬프트 아래의 한 줄입니다. ⚠ my-mod: checks: 3 passing처럼 ⚠와 mod 이름으로 시작합니다.
$.ui.toast(text) mod 이름과 함께 표시되며 몇 초 후 사라지는 토스트 알림입니다. 전체 화면 렌더링에서는 오른쪽 상단의 상자로, 클래식 렌더러에서는 프롬프트 아래 오른쪽의 한 줄로 표시됩니다.
$.ui.log(text) Claude가 읽지 않는 트랜스크립트의 흐린 줄입니다. ● my-mod: build finished처럼 ●와 mod 이름으로 시작합니다.
$.ui.notify(text) Claude Code 외부에서 사용자의 터미널이 보내는 알림으로, 사용자의 preferredNotifChannel 설정에 따른 방식으로 전송됩니다. 비어 있지 않은 { title }을 전달하지 않으면 mod 이름이 제목이 되며, 전달하면 지정한 그대로 표시됩니다.

$.ui.notify는 이 중 응답을 반환하는 유일한 호출입니다. 사용자의 채널이 터미널에 알림을 기록하면 { isSent: true, channel }로 resolve됩니다. channel은 알림을 보낸 설정 값으로, kitty나 terminal_bell 등이 있습니다. terminal_bell 채널은 벨을 울리며 텍스트와 제목은 모두 표시하지 않습니다.

그 외의 경우 호출은 { isSent: false, reason }으로 resolve됩니다.

reason 의미
disabled 사용자가 preferredNotifChannel을 notifications_disabled로 설정했습니다
no-channel 설정이 auto이며 사용자의 터미널에 Claude Code가 보낼 수 있는 알림이 없습니다
no-surface 세션에 터미널이 없습니다. -p 실행, Agent SDK, 데스크톱 앱, VS Code 확장 프로그램, 클라우드 세션이 이에 해당합니다. mod가 로드되는 중, 즉 세션이 시작되기 전에 수행한 호출과 Claude Code가 종료되는 중에 수행한 호출에 대한 응답이기도 합니다
refused ui.notify에 대한 다른 mod의 훅이 Claude Code 대신 응답하고 알림을 보내지 않았습니다

text가 문자열이 아니거나, ui.notify에 대한 다른 mod의 훅이 { deny }를 반환하거나, 터미널에 쓰기가 실패하면 호출은 resolve되지 않고 reject됩니다.

mod는 채널을 선택할 수 없으며, Claude Code는 mod가 알림을 보내는 빈도를 제한하지 않습니다. 사용자의 설정이 유일한 스위치입니다.

호출이 실행 중인 세션에 도달하면 사용자의 Notification 훅이 plugin_notification 유형으로 실행됩니다. 이 훅은 응답이 disabled인 경우에도, 그리고 응답이 no-surface인 터미널 없는 세션에서도 실행됩니다. 세션이 시작되기 전이나 Claude Code가 종료되는 중에 수행한 호출에 대해서는 훅이 실행되지 않습니다. mod가 $.ui.notify를 호출한 훅은 해당 실행에서 제외되므로, 알림을 보내는 classic.Notification 훅이 자신의 알림에 대해 다시 호출되지 않습니다.

터미널은 알림을 표시했는지 보고하지 않으므로, isSent: true는 알림이 전송되었다는 의미일 뿐 사용자가 보았다는 의미는 아닙니다.

다음 예제는 메인 대화의 턴이 중단 없이 끝나면 알림을 보내고, 텍스트가 포함된 알림이 전송되지 않은 경우 토스트로 폴백합니다. 사용자의 Notification 훅이 먼저 실행되어 시간이 걸릴 수 있으므로, 이벤트를 전달하기 전에 알림을 기다리지 않습니다.

on('turn.complete', ($, e, next) => {
  const isMainTurn = e.agentId === undefined && !e.isAborted
  const fallBack = () => $.ui.toast('Turn finished')
  if (isMainTurn) {
    $.ui.notify('Turn finished').then(sent => {
      const hasText = sent.isSent && sent.channel !== 'terminal_bell'
      if (!hasText) fallBack()
    }, fallBack)
  }
  return next(e)
})

VS Code 확장 프로그램에서는 상태줄과 토스트가 대신 프롬프트 상자 위에 표시되고, 로그 줄은 대화에 표시됩니다.

백그라운드 작업에서 턴 시작하기

백그라운드 작업이 Claude의 주의가 필요한 사항을 발견하면 $.prompt.submit({ text })로 프롬프트를 제출하여 턴을 시작할 수 있습니다. Claude는 해당 mod를 발신자로 명시하는 문장 뒤에 이어지는 텍스트를 읽습니다. 그 문장 없이 사용자 자신의 말로 보내려면 asUser: true를 추가합니다. 이 호출은 세션이 유휴 상태가 될 때까지 기다린 후 새 턴을 시작합니다. 해당 턴이 시작될 때 resolve되므로, Claude가 작업하는 동안 실행되는 핸들러에서는 이를 await하지 마십시오.

백그라운드 작업 중지하기

타이머는 모듈이 다시 로드되면 중지됩니다. 훅 내부에서 장시간 실행되는 작업의 경우, next.signal은 훅이 처리 중인 이벤트가 중단될 때(예: 사용자가 작업을 중단할 때) 중단되는 AbortSignal입니다. 장시간 실행되는 모든 작업에 이를 전달하십시오.

사용자가 턴을 중단한 후의 호출과 백그라운드 작업

결과나 오류에 the session was stopped and nobody has sent input since라는 사유가 있으면 사용자가 Esc나 Stop 등으로 실행 중인 턴을 중지했다는 의미입니다. 이렇게 중단된 후에는 프롬프트에 입력한 메시지처럼 사용자의 다음 입력이 있을 때까지 세션에서 새 작업을 시작하는 호출이 거부됩니다.

$.prompt.submit과 $.command.run을 제외하면, 중단 후의 호출과 백그라운드 작업에 대한 이러한 처리에는 Claude Code v2.1.304 이상이 필요합니다.

거부되거나 삭제된 호출 재시도하기

거부되거나 삭제된 호출은 turn.complete 훅에서 다시 수행합니다. 호출은 사용자의 다음 입력이 있을 때까지 계속 거부되며, 이를 대신 대기열에 넣거나 반복해 주는 기능은 없습니다.

호출의 결과나 오류에 다음 문자열 중 하나가 포함되어 있으면 나중에 재시도할 수 있도록 인수를 보관해 둡니다.

  • the session was stopped and nobody has sent input since: 호출이 거부되었습니다
  • was removed from the queue before it ran: 프롬프트나 명령이 실행되지 않은 채 대기열에서 빠져 삭제되었습니다. 예를 들어 사용자가 턴을 중지한 경우입니다

이 예제에서는 백그라운드 작업에서 $.prompt.submit 대신 submitPrompt를 호출합니다. pendingText 변수와 submitPrompt 함수는 register 위에 두고, turn.complete 훅은 그 안에 두십시오. 검증에서 register 내부에 선언된 함수에 전달된 $를 거부하기 때문입니다. 결과나 오류에 해당 문자열 중 하나가 포함되어 있으면 submitPrompt는 turn.complete 훅이 다시 제출할 수 있도록 텍스트를 pendingText에 보관합니다.

// The text of the last refused or dropped prompt, or null
let pendingText = null

async function submitPrompt($, text) {
  let reason
  try {
    const result = await $.prompt.submit({ text })
    reason = result?.drop ?? ''
  } catch (error) {
    reason = String(error?.message ?? error)
  }
  if (reason.includes('the session was stopped and nobody has sent input since') ||
      reason.includes('was removed from the queue before it ran')) {
    pendingText = text
  }
  // Any other result or error isn't retried
}

on('turn.complete', async ($, e, next) => {
  if (pendingText !== null) {
    const text = pendingText
    // Empty pendingText before retrying, so that a second run of this hook
    // doesn't submit the same text.
    pendingText = null
    // Not awaited: the hook would stay open until the new turn starts
    submitPrompt($, text)
  }
  return next(e)
})

사용자의 다음 입력 전에 중단된 턴 자체나 서브에이전트의 턴에 대해 turn.complete가 발생할 수 있으며, 이때 수행한 호출은 다시 거부됩니다. 이 경우 submitPrompt는 이후의 turn.complete를 위해 텍스트를 보관합니다.

$.command.run처럼 reject되는 호출의 경우, 오류를 catch하고 submitPrompt가 reason을 검사하는 방식으로 오류 메시지를 검사합니다.

거부된 호출이 반환하는 값

결과에서 <reason>은 호출이 거부된 사유를 나타냅니다. 중단 후에는 the session was stopped and nobody has sent input since입니다. 다른 사유는 claude -p 실행 및 Agent SDK 세션에서 거부되는 호출을 참조하십시오.

거부되는 일부 호출 결과
$.prompt.submit { drop: '<reason>' }으로 resolve되며 프롬프트를 대기열에 넣지 않습니다
$.command.run 메시지가 <reason>으로 끝나는 오류와 함께 reject됩니다
$.agent.spawn { deny: '<reason>' }으로 resolve되며 서브에이전트를 시작하지 않습니다
완료된 서브에이전트에 대한 $.session.send { isDelivered: false, reason: 'Agent "<id>" was not resumed: <reason>.' }으로 resolve되며, 여기서 <id>는 서브에이전트의 id입니다. 아직 실행 중인 서브에이전트는 메시지를 받습니다.
에이전트 팀 팀원에 대한 $.session.send { isDelivered: false, reason: 'Teammate "<name>" was not messaged: <reason>.' }으로 resolve되며, 여기서 <name>은 팀원의 이름입니다
run_in_background: true를 사용한 Bash, Monitor 또는 CronCreate의 $.tool.call { deny: '<tool> was not run: it starts background work, a schedule or a goal, and <reason>' }으로 resolve되며, 여기서 <tool>은 도구의 이름입니다

대화형 세션에서 중단 후 이러한 $.session.send 또는 $.tool.call 호출 중 하나가 거부되면 사용자에게 Stopped: 토스트가 표시될 수 있습니다.

중단 전에 mod가 시작한 작업

사용자가 Esc 등으로 실행 중인 턴을 중지하면, mod가 이미 시작한 작업 중 일부는 삭제되고 일부는 사용자의 다음 입력을 기다립니다.

작업 사용자의 다음 입력 전까지 일어나는 일 조치
아직 대기열에 있는 $.prompt.submit의 프롬프트 삭제됩니다. 호출은 { drop: 'the prompt was removed from the queue before it ran' }으로 resolve됩니다. 거부되거나 삭제된 호출 재시도하기에 나온 대로 다시 제출합니다
아직 대기열에 있는 $.command.run의 명령 삭제됩니다. 호출은 메시지에 the command was removed from the queue before it ran이 포함된 오류와 함께 reject됩니다. 거부되거나 삭제된 호출 재시도하기에 나온 대로 다시 실행합니다
CronCreate 같은 도구로 예약했으며 사용자의 다음 입력 전에 실행 시점이 되는 일회성 작업 그 시점에는 턴을 시작하지 않으며, 해당 입력 이후에 실행됩니다 조치가 필요하지 않습니다
CronCreate 같은 도구로 예약했으며 사용자의 다음 입력 전에 실행 시점이 되는 반복 작업 그 시점에는 턴을 시작하지 않습니다. 작업은 해당 실행을 건너뛰고 일정은 유지됩니다. 조치가 필요하지 않습니다
mod가 시작한 모니터의 이벤트 모니터는 계속 실행됩니다. 사용자의 다음 입력 전에 모니터가 발생시킨 이벤트는 삭제되지만, 모니터가 종료되었다는 알림은 예외로 해당 입력 이후에 전달됩니다. 취할 수 있는 조치가 없습니다
mod가 생성한 후 $.session.send로 재개했으며 사용자의 다음 입력 전에 완료되는 서브에이전트 완료 알림이 대기하다가 해당 입력 이후에 대화에 전달됩니다 조치가 필요하지 않습니다
$.tool.call로 실행한 포그라운드 Bash 또는 PowerShell 명령으로, 사용자가 턴을 중지했을 때 이미 실행 중이었으며 사용자의 다음 입력 전에 타임아웃에 도달하는 명령 백그라운드로 이동하는 대신 타임아웃 시점에 중지됩니다. 호출은 isError가 설정된 채로 resolve되며, 출력에 Ended instead of moving to the background: plugins' input is held, by a Stop or by the host's limits. 줄이 포함됩니다. 거부되거나 삭제된 호출 재시도하기에 나온 대로 사용자의 다음 입력 이후에 다시 실행합니다

예약된 작업의 실행 시점이 되거나 모니터가 사용자의 다음 입력 전에 이벤트를 발생시키면 사용자에게 Stopped: 토스트가 표시될 수 있습니다.

claude -p 실행 및 Agent SDK 세션에서 거부되는 호출

claude -p 실행이나 Agent SDK 세션에서 { drop: 'the run reached its --max-turns limit' } 같은 결과는 실행이 mod의 호출을 거부했다는 의미입니다.

거부된 $.prompt.submit은 { drop: '<reason>' }으로 resolve됩니다. $.command.run과 $.agent.spawn 같은 다른 거부된 호출은 <reason> 자리에 사유가 들어간 채로 거부된 호출이 반환하는 값에 나온 결과를 반환합니다. 아래에서 해당하는 <reason>을 찾아 대처 방법을 확인하십시오.

  • the run reached its --max-turns limit: mod의 프롬프트와 mod가 생성한 서브에이전트의 턴은 --max-turns 또는 Agent SDK의 턴 제한에 포함됩니다. 더 많은 턴이 필요하면 더 높은 제한으로 실행을 시작하십시오. 그럴 수 없다면 mod가 거부된 호출을 더 이상 수행하지 않도록 하십시오. 실행이 사용자나 SDK 애플리케이션으로부터 다른 프롬프트를 받으면 턴 수가 초기화되고 실행이 다시 호출을 받아들입니다.
  • the run is past its --max-budget-usd limit: 더 높은 --max-budget-usd로 실행을 시작하거나 Agent SDK의 지출 제한을 높이십시오. 그럴 수 없다면 mod가 거부된 호출을 더 이상 수행하지 않도록 하십시오. 이 제한은 지금까지의 실행 총비용에 적용됩니다. 이 총액은 실행이 다른 프롬프트를 받아도 계속 누적되므로, 재시도해도 같은 사유가 반환됩니다.
  • the host has ended its input: 예를 들어 SDK 애플리케이션이 스트리밍 입력을 종료했거나, --input-format stream-json으로 시작한 실행에서 stdin이 닫힌 경우입니다. 실행이 종료되는 중이며 더 이상 입력을 받지 않으므로, 재시도해도 같은 사유가 반환됩니다. mod가 거부된 호출을 더 이상 수행하지 않도록 하십시오. 실행을 직접 시작하는 경우, 다음부터는 mod가 더 이상 수행할 호출이 없을 때까지 해당 입력을 열어 두십시오.
  • the session was stopped and nobody has sent input since: 예를 들어 Agent SDK의 interrupt()로 실행이 중단되었습니다. 사용자나 SDK 애플리케이션의 다음 메시지 이후에 호출을 다시 수행하려면 거부되거나 삭제된 호출 재시도하기를 따르십시오.

mod를 사용하는 claude -p 실행이 종료되지 않음

mod가 매 턴이 끝날 때마다 프롬프트를 제출하면 각 프롬프트가 또 다른 턴을 시작합니다. --max-turns나 --max-budget-usd가 없으면 mod를 로드하는 claude -p 실행은 직접 중지할 때까지 계속됩니다.

실행이 스스로 종료되도록 하려면 셸의 명령에 두 플래그 중 하나를 추가하십시오. 다른 사람이 실행을 시작하는 경우에는 작업이 끝나면 mod가 프롬프트 제출을 중지하도록 하십시오. 다음 명령은 --max-turns를 추가합니다.

claude -p "find the TODOs" --max-turns 5

실행이 해당 제한에 도달하면 mod의 다음 프롬프트는 턴을 시작하는 대신 거부되고, 실행이 종료됩니다.

그래도 실행이 제한을 넘어 계속되면 셸에서 claude --version을 실행하고, 버전이 v2.1.304보다 이전이면 Claude Code를 업데이트하십시오. v2.1.299부터는 제한에 도달하면 mod의 프롬프트가 거부됩니다. mod가 서브에이전트를 생성하는 경우, v2.1.304부터는 서브에이전트의 턴이 --max-turns에 포함됩니다.

세션 간 메시지 보내기 및 받기

mod는 사용자의 다른 세션, 이 세션의 서브에이전트 중 하나, 또는 에이전트 팀의 팀원에게 일반 텍스트 메시지를 보낼 수 있습니다. 또한 도착하고 나가는 메시지를 관찰할 수 있습니다.

메시지를 보내려면 $.session.send({ to, text })를 호출합니다. 이 호출은 SendMessage 도구와 동일한 방식으로 전달합니다. to는 메시지를 받는 대상에 따라 설정합니다.

  • 사용자의 다른 세션: { sessionId }
  • 서브에이전트 또는 팀원: $.agent.list()에서 가져온 id를 사용한 { agentId }
  • 수신된 메시지의 발신자: 해당 메시지의 발신 문자열 주소

이 호출은 메시지가 대기열에 추가되면 { isDelivered: true }로 resolve됩니다. 아무것도 전달되지 않은 경우 { isDelivered: false, reason }으로 resolve되며, reason에 그 이유가 담깁니다.

다음 훅은 명령으로 등록된 /ping 명령에 응답하여, 명령 뒤에 입력한 id의 세션에 상태를 요청합니다.

on('command.run', { command: 'ping' }, async ($, e) => {
  // e.args is the session id typed after /ping
  const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' })
  // The call resolves either way, so check isDelivered to learn what happened
  if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason)
  // An empty result prints nothing in this session's transcript
  return {}
})

메시지가 대기열에 추가되면 사용자의 세션에는 아무것도 표시되지 않으며, 다른 세션의 Claude가 Status? One line.을 읽습니다. 아무것도 전달되지 않은 경우 토스트 알림으로 이유가 표시됩니다.

session.receive와 session.send를 사용하면 mod가 메시지를 관찰할 수 있습니다. 두 이벤트 모두에서 next(e)를 반환하면 각 메시지를 변경 없이 통과시킵니다.

이벤트 발생 시점 유용한 필드
session.receive Claude가 읽기 전에 이 세션에 메시지가 도착할 때 e.text, 그리고 다른 세션이나 에이전트의 경우 peer 또는 peer-send-message, task-notification, scheduled-trigger 등의 e.origin.kind. Claude에게 전달되지 않도록 하려면 { consumed: reason }을 반환합니다.
session.send SendMessage 도구나 mod에서 메시지가 나가려고 할 때 e.to, e.text, 그리고 model 또는 plugin인 e.origin.kind

수신 메시지를 거부하도록 설정된 세션은 session.receive가 발생하기 전에 메시지를 거부하므로 훅에서는 해당 메시지를 볼 수 없습니다. 사용자의 승인을 기다리는 메시지는 먼저 훅에 도달하므로, mod는 아직 승인하지 않은 메시지를 읽을 수 있습니다. 메시지가 전달되지 않으면 훅의 next(e)는 reject됩니다.

수신된 메시지의 발신자 이름은 발신자가 작성한 그대로이므로, 이를 근거로 결정을 내리지 마십시오.

파일, 프로세스, 네트워크에 접근하기

mod는 mods API를 통해 파일 시스템, 프로세스, 네트워크에 접근하며, Claude Code를 실행하는 사용자와 동일한 권한을 가집니다. 훅 모듈 자체에는 Node.js API, setTimeout 같은 타이머 전역 객체가 없으며, 자체적인 네트워크 또는 파일 접근 기능도 없습니다. URL, TextEncoder, AbortController, crypto.subtle 같은 표준 JavaScript 및 웹 API는 사용할 수 있습니다. 아래의 각 네임스페이스는 한 가지 종류의 접근을 담당합니다.

네임스페이스 기능
$.fs read(path), write(path, text), exists(path), stat(path), list(path)로 파일과 디렉터리를 다룹니다
$.process run(['git', 'status'])는 명령을 시작하고 명령이 종료되면 resolve됩니다. spawn은 장시간 실행되는 명령의 출력을 스트리밍합니다.
$.http http 또는 https를 통한 fetch(url, init)입니다. 본문을 읽고 나면 { status, ok, headers, text }로 resolve됩니다.
$.store 플러그인 전용 JSON 키-값 저장소로, 세션 간에 유지됩니다
$.env 환경 변수를 get 및 set합니다. 이름은 문자열 리터럴로 작성합니다.
$.settings read()는 하나의 객체로 병합된 모든 설정으로 resolve됩니다. { source } 또는 { key }를 전달하면 대신 한 소스의 설정 또는 한 키의 값으로 resolve됩니다.
$.session messages()는 트랜스크립트를 { role, text, toolUses }의 목록으로 반환합니다. 작업 디렉터리, 모델 등도 제공합니다. usage()는 컨텍스트 윈도우 사용량과 플랜 한도를 반환합니다.
$.mcp 연결된 MCP 서버의 도구를 call합니다

파일, 프로세스, 요청에는 몇 가지 고유한 규칙이 있습니다.

  • 경로: 상대 경로는 세션의 작업 디렉터리, 또는 훅이 처리 중인 이벤트를 발생시킨 서브에이전트의 작업 디렉터리를 기준으로 해석됩니다
  • $.fs.list: 한 디렉터리의 항목을 { name, kind, size, isLink } 형태로 반환하며, 재귀적으로 동작하지 않습니다
  • $.process.run: 인수 목록을 받으며 셸을 사용하지 않습니다. 종료 코드와 관계없이 { exitCode, stdout, stderr }로 resolve됩니다. 프로그램을 시작할 수 없거나 타임아웃(기본값 30초) 시점에 여전히 실행 중이면 reject되므로 try와 catch로 감싸야 합니다.
  • $.http.fetch: 최대 5번까지 리디렉션을 따릅니다. 다른 origin으로 리디렉션되면 설정한 요청 헤더 중 accept, accept-language, content-type, user-agent만 유지하고 나머지는 제거하므로, Authorization 같은 다른 헤더에 의존하는 요청은 해당 리디렉션 후 실패할 수 있습니다. 타임아웃과 본문 크기는 제한 사항에 나와 있습니다.

이러한 호출 각각은 그 자체로 하나의 이벤트이며, $.를 제외한 네임스페이스와 메서드 이름으로 명명됩니다. 예를 들어 $.fs.read의 이벤트는 fs.read입니다. 체인의 앞쪽에 있는 mod는 사용자의 호출을 관찰하거나, 재작성하거나, 거부할 수 있으며, 조직은 이 방식으로 mod가 접근할 수 있는 범위를 제한합니다.

mod는 명령이 출력을 생성했거나 종료된 후에도 사용자의 $.process.spawn 호출을 거부할 수 있으며, 이 경우 명령이 수행한 작업은 되돌려지지 않습니다. 그러면 호출은 다음 문자열 중 하나와 거부한 mod의 사유로 끝나는 메시지와 함께 reject됩니다.

  • $.process.spawn started, and a plugin withheld its result:: 거부한 mod가 명령의 출력을 끝까지 읽지 않은 상태였습니다. 명령이 아직 실행 중이면 Claude Code가 명령을 중지합니다.
  • $.process.spawn ran, and a plugin withheld its result:: 거부한 mod가 명령의 출력을 끝까지 읽은 상태였으므로 명령은 이미 종료되었습니다

모든 설정, 한 소스 또는 한 키 읽기

$.settings.read()는 사용 중인 모든 소스의 설정을 우선순위에 따라 병합한 하나의 객체로 resolve됩니다. 대신 한 소스를 읽으려면 { source }를, 한 키를 읽으려면 { key }를 전달하거나 둘 다 전달합니다.

호출 resolve 결과
$.settings.read() 병합된 설정을 담은 하나의 객체입니다. 최상위 키는 permissions, hooks 등 settings.json 파일의 키와 같습니다.
$.settings.read({ source: 'policy' }) 해당 소스 하나의 설정을 담은 객체이며, 해당 소스에 설정이 없으면 {}입니다
$.settings.read({ key: 'spinnerTipsEnabled' }) 병합된 설정에서 해당 키의 값이며, 키가 설정되지 않았으면 undefined입니다
$.settings.read({ key: 'spinnerTipsEnabled', source: 'user' }) 해당 소스 하나에서 해당 키의 값이며, 해당 소스가 그 키를 설정하지 않았으면 undefined입니다

source에는 읽을 설정 소스 하나의 이름을 전달합니다.

  • user: ~/.claude/settings.json
  • project: .claude/settings.json
  • local: .claude/settings.local.json
  • flag: --settings로 전달한 내용과 SDK의 인라인 설정
  • policy: 관리형 설정

key에는 최상위 키 하나의 정확한 이름을 전달하며, 중첩된 객체를 가리키는 점 표기 경로는 사용할 수 없습니다. key 인수를 사용하려면 Claude Code v2.1.304 이상이 필요합니다.

다음 단계