플러그인이 미들웨어가 된 Claude Code Mods

Claude Code v2.1.287부터 플러그인 안의 JavaScript·TypeScript 함수가 이벤트를 지켜보고, 고쳐 쓰고, 대신 처리합니다. mod의 파일 구성과 next(e)로 이어지는 체인, 끄는 설정을 그림으로 보여 줍니다.

동딩
2026.10.07·8분 읽기·

mod는 Claude Code의 동작 사이에 끼워 넣는 작은 스크립트입니다. 도구 호출, 프롬프트 제출, 화면 그리기 같은 일이 생길 때마다 Claude Code가 mod의 함수를 부르고, 함수는 그 일을 지켜보거나 바꾸거나 대신 처리합니다. 릴리스 노트에는 "plugins may now modify deeper behavior" 한 줄뿐이지만, 플러그인이 Claude Code에 끼어드는 방식이 통째로 바뀌었습니다.

설정 훅은 바깥에서, mod는 안에서 실행됩니다

플러그인은 전에도 hooks/hooks.json에 설정 훅을 넣을 수 있었습니다. 설정 훅은 셸 명령이나 HTTP 요청으로 Claude Code 바깥에서 실행되고, 도구 호출을 허용하거나 막는 데 쓰입니다. mod는 Claude Code 프로세스 안에서 함수로 돌기 때문에 프롬프트와 도구 호출을 고쳐 쓰고 화면까지 그립니다.

  • 실행 위치Claude Code 바깥Claude Code 프로세스 안
  • 만드는 방법셸 명령·HTTP 요청·프롬프트JavaScript·TypeScript 함수
  • 바꿀 수 있는 것진행 여부, 도구 인자·결과프롬프트·도구 호출·턴
  • 화면못 그림창(pane)·띠(band), 기존 UI 교체
  • 훅끼리 값 공유안 됨같은 파일의 변수
설정 훅과 mod가 할 수 있는 일

설정 훅은 없어지지 않고 mod와 함께 실행됩니다. mod가 그리는 창(pane)은 대화 기록 옆에, 띠(band)는 프롬프트 위에 뜹니다. Mods overview에 따르면 이 화면은 터미널과 데스크톱 앱 Code 탭(v2.1.286부터)에만 보이고, VS Code 채팅 패널이나 claude -p에서는 훅만 실행됩니다.

가장 작은 mod는 파일 세 개입니다

mod는 새 형식이 아니라 플러그인입니다. hooks.json의 modules 키가 코드 파일을 가리키면 그 플러그인이 mod가 됩니다.

  • first-mod/
    • .claude-plugin/
      • plugin.json이름·버전
    • hooks/
      • hooks.jsonmodules로 코드 지정
      • register.jshooks module
공식 문서 예제 mod의 파일 구성

register.js가 내보내는 register(on) 안에서 on(이벤트, 함수)로 훅을 등록합니다. 문서의 첫 예제는 도구 호출 수를 세어 스피너 옆에 보여 주고, 두 훅이 파일 위쪽의 calls 변수를 함께 씁니다.

javascript
let calls = 0
export function register(on) {
on('tool.call', async ($, e, next) => {
calls += 1
$.ui.invalidate('ui.render')
return next(e)
})
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}

claude --plugin-dir ./first-mod로 띄우면 마켓플레이스 없이 한 세션에만 불러옵니다. 문서에 따르면 Claude가 일하는 동안 스피너가 Thinking · tool calls: 3…처럼 바뀝니다.

설치하기 전에는 claude plugin validate로 코드를 실행하지 않고 mod가 하는 일을 봅니다. 직접 v2.1.292에서 이 예제로 돌려 보니 받는 이벤트(hooks:)와 부르는 API(calls:) 사이에 gating hook without .catch 경고가 나왔습니다.

v2.1.292 claude plugin validate 출력, 예제 mod의 hooks와 calls

이 경고는 도구 호출을 막을 수 있는 훅에 오류 처리기가 없다는 뜻입니다. 이벤트 문서에 따르면 이런 훅이 예외를 던지거나 시간 한도를 넘기면 Claude Code는 그 훅을 건너뛰고 다음 처리기를 부릅니다. 명령을 막으려던 훅이 실패하면 명령은 그대로 실행됩니다.

  • 10초훅 하나가 이벤트 하나에 쓰는 시간Mods reference
  • 1초실패한 훅 대신 도는 .catch 처리기Mods reference
훅에 주어지는 시간 한도

레퍼런스의 10초에는 next나 mods API 호출 안에서 기다린 시간이 들어가지 않습니다($.clock.sleep은 예외). 명령을 막는 mod라면 on이 돌려주는 등록에 .catch를 달아 실패했을 때도 deny로 답하게 합니다. 이벤트 문서의 예이고, guard가 원래 훅 함수입니다.

javascript
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

훅은 next(e)로 이어지는 미들웨어입니다

훅 함수는 인자 세 개를 받습니다. $는 파일·화면·명령에 닿는 mods API, e는 이벤트, next는 다음 처리기입니다. 처리기가 다음 처리기를 불러 줄줄이 이어지는 이런 구조를 미들웨어라고 부릅니다. 훅이 next를 어떻게 부르느냐에 따라 결과가 세 갈래로 나뉩니다.

관찰next(e)보고 그대로 넘김
같은 이벤트기본 동작 실행
고쳐 쓰기next({ ...e })복사본을 바꿔 넘김
바뀐 이벤트기본 동작 실행
직접 응답return { deny }next를 부르지 않음
체인 중단기본 동작 없음
훅이 next를 부르는 방식에 따른 세 갈래

이벤트 객체는 통째로 읽기 전용이라(deep freeze) 필드에 값을 넣으면 예외가 납니다. 고쳐 쓸 때는 복사본을 next에 넘깁니다. 직접 응답의 예는 강제 푸시를 막는 훅입니다. next를 부르지 않으므로 명령은 실행되지 않고 권한 프롬프트도 뜨지 않으며, Claude는 deny 문구를 도구 결과로 읽습니다.

javascript
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
}
return next(e)
})

같은 이벤트에 걸린 훅들은 체인 하나를 이루고, 순서는 mod를 누가 넣었는지로 정해집니다. 회사가 기기에 내려보내는 관리형 설정(managed settings)의 훅과 내장 가드가 바깥쪽, 내가 설치한 mod가 안쪽입니다. 바깥쪽 mod일수록 이벤트를 먼저 보고 결과는 마지막에 봅니다.

tool.call 하나가 mod 체인을 지나는 길
  1. 1관리형 훅체인보다 먼저 검사
  2. 2sec-default가장 바깥 mod
  3. 3사용자 modnext를 부를지 정함
  4. 4기본 동작설정 훅 → 권한 → 실행
  5. 5결과들어온 길로 되돌아감
관리형 훅

관리형 설정의 PreToolUse 훅이 가장 먼저 봅니다. 여기서 막힌 호출은 어떤 mod에도 가지 않습니다.

sec-default

Team·Enterprise로 로그인했거나 관리형 설정이 있으면 내장 가드가 맨 앞에 섭니다. 조직이 prependPlugins로 넣은 mod도 이 자리입니다.

사용자 mod

내가 설치한 mod 차례입니다. next(e)를 부르면 다음으로 넘어가고, { deny }로 답하면 체인이 여기서 끝납니다.

기본 동작

마지막 next가 Claude Code의 기본 동작에 닿습니다. 일반 설정과 플러그인의 PreToolUse 훅, 권한 검사, 도구 실행 순서입니다.

결과

도구 결과는 사용자 mod, sec-default 순으로 거꾸로 올라갑니다. 그래서 바깥 mod가 결과를 가장 늦게 봅니다.

공식 예제 셋은 지켜보기·그리기·붙잡기를 하나씩 보여 줍니다

Anthropic은 claude-code-playground 저장소에 완성된 mod 세 개를 올려 두었습니다. replay-theater는 편집을 지켜보기만 하고, token-weather는 프롬프트 위에 띠를 그리고, blast-radius는 위험한 명령을 붙잡아 deny로 답합니다.

공식 예제 mod 셋을 띄운 세션(실제 화면·README를 줄여 다시 그림)
claude · ~/demotool.call → next(e)
> 파일 세 개를 고쳐 줘
● Edit ×3
● 세 파일을 고쳤습니다.
Replay: 3 edits
>
> /replay
Replay Theater step 1 of 3
1 2 3
greet.js Edit +1 -1
- "hi "
+ "hello "
PrevNextClose
> 큰 파일 넷 읽고 요약해 줘
● Read ×4
● 네 파일을 요약했습니다.
Showers 67% of context 134.4k / 200k last turns ▁▂█ ▲ +98.3k last turn
>
> build 폴더 지워 줘
Command rm -rf build
Would delete 4 files (about 68 KB)
build/index.html · build/app.css · build/assets/logo.svg · build/app.js
1 : Proceed2 : CancelClaude is waiting on your answer
⎿ Blast Radius held this command and did not run it: the user pressed Cancel. …
replay-theater · 턴이 끝나면

턴이 끝나면 프롬프트 위에 Replay: 3 edits가 뜹니다. 그동안 tool.call은 편집을 적어 두기만 하고 늘 next(e)로 넘겼습니다.

replay-theater · /replay

/replay를 치면 편집을 diff 한 장씩 넘겨 보는 창이 열립니다. 위쪽 1·2·3이 편집 세 개입니다.

token-weather · 턴마다

턴이 끝날 때마다 프롬프트 위 띠가 컨텍스트가 찬 정도를 날씨로 보여 줍니다. 찰수록 Clear에서 Cloudy, Showers로 바뀌고, 그림의 67%는 Showers입니다. turn.complete에서 읽고 ui.render로 그립니다.

blast-radius · 실행 직전

rm -rf build가 실행되기 전에 멈추고 지워질 파일 네 개를 보여 줍니다. 1이나 2를 고를 때까지 명령은 기다립니다.

blast-radius · Cancel

Cancel을 고르면 훅이 next를 부르지 않고 deny로 답합니다. Claude는 그 문구를 도구 결과로 받고, 다시 시도하지 않습니다.

blast-radius는 버튼을 기다리는 동안 $.process.run으로 sleep을 0.25초씩 되풀이하므로 10초 한도에 걸리지 않고, 10분 동안 답이 없으면 deny로 끝냅니다.

셋 다 저장소를 내려받아 claude --plugin-dir ./claude-code/mods/blast-radius처럼 한 세션에만 불러와 볼 수 있습니다. 이벤트 문서에는 PR 이야기가 나오면 현재 브랜치 이름을 덧붙이는 prompt.submit 훅, main에서 git push를 막는 tool.check 훅 같은 짧은 예제도 있습니다. tool.check는 권한 규칙과 설정 훅이 실행 여부를 정한 뒤에 오는 이벤트라 그 결정을 뒤집을 수 있습니다.

샌드박스가 없어서 끄는 방법이 함께 나왔습니다

mod는 사용자 권한으로 실행됩니다. 파일을 읽고 쓰고, 프로세스를 띄우고, 네트워크 요청을 보내고, 권한 프롬프트가 뜨기 전에 도구 호출을 승인할 수도 있습니다. 막혀 있는 것은 권한 프롬프트 화면을 바꾸는 일 정도입니다. 다만 sec-default가 도는 기기에서는 이 가드가 체인 맨 앞에 서므로 사용자 mod가 deny 규칙과 관리형 훅을 넘지 못합니다.

끄는 방법은 범위에 따라 넷입니다. --safe-mode는 한 세션을 띄울 때 붙이는 플래그이고, disableAllHooks는 ~/.claude/settings.json에 true로 적는 설정입니다. /plugin을 뺀 셋은 /diff나 AGENTS.md 로딩 같은 내장 mod를 끄지 않으므로, 내장 mod는 /plugin에서 하나씩 꺼야 합니다.

설치한 mod내장 mod설정 훅
/plugin 비활성화하나씩하나씩그 플러그인 것만
--safe-mode지원미지원지원
disableAllHooks지원미지원지원
allowManagedModsOnly사용자 mod만미지원미지원
끄는 방법마다 멈추는 대상(✓ 멈춤)

조직 관리 문서에 따르면 allowManagedModsOnly는 관리형 설정의 pluginConfigs["cc-plugin-sec-default@builtin"].options 아래에 둡니다. 조직이 배포한 mod와 사용자의 설정 훅은 그대로 두고 사용자가 가져온 mod만 막는 설정입니다. 얼리 액세스 때 쓰던 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS는 v2.1.287부터 무시되므로 0으로 둬도 mod는 꺼지지 않습니다.

Anthropic은 소개 글에서 새 기능 출시를 기다리지 않고 원하는 동작을 직접 붙이게 하는 것이 목적이라고 설명합니다. 그 대신 남이 만든 mod는 내 권한으로 도는 코드로 다뤄야 합니다. 설치 전에 claude plugin validate의 calls: 줄에서 $.process.run이나 $.http.fetch를 확인하고, 직접 만든 가드에는 .catch를 다는 것까지가 지금 사용자가 챙길 일입니다.

Comments

댓글

powered by giscus ↗