OMC는 자기 실행 루프가 없다. Claude Code 본체 루프의 “정해진 순간(생명주기 이벤트)“마다 끼어들어 컨텍스트를 주입하거나 도구를 통제하는 플러그인이다. — 왜 배우나: 하네스를 만들 때 “루프를 새로 짜야 한다”는 오해를 버리고, 이벤트에 작은 스크립트만 꽂으면 된다는 핵심 설계를 잡기 위해서다.
그림
OMC가 직접 루프를 돌리는 게 아니라, CC 루프 중간에 “이벤트가 울릴 때만” 호출된다.
flowchart LR
U[사용자 메시지 전송] --> CC[Claude Code 본체 루프]
CC -->|"이벤트 발생 시<br/>stdin: JSON 페이로드"| RUN[run.cjs 공통 런처]
RUN -->|".mjs 스크립트 실행"| KD["훅 스크립트<br/>(keyword-detector.mjs 등)"]
KD -->|"stdout: 결정 JSON 한 줄"| CC
CC -->|"additionalContext 합침"| M[모델 호출]
M -->|"응답·도구 사용 결정"| CC
CC -->|"루프 계속<br/>(다음 이벤트로)"| CC
생명주기 이벤트가 한 세션 동안 울리는 순서.
sequenceDiagram
participant CC as Claude Code 루프
participant H as OMC 훅 스크립트
CC->>H: SessionStart (세션 시작)
CC->>H: UserPromptSubmit (사용자 메시지)
CC->>H: PreToolUse (도구 쓰기 직전)
CC->>H: PostToolUse (도구 쓴 직후)
CC->>H: Stop (응답 끝낼 때)
CC->>H: SessionEnd (세션 종료, 비동기)
Note over CC,H: 매 시점마다 OMC는 JSON 한 줄 뱉고 종료. 루프 제어는 CC가 함.
쉽게 풀기
비유: 공항 보안검색대. 운항 전체 흐름은 공항(=Claude Code)이 돌린다. OMC는 길목에 선 검색 요원이다. 승객이 들어올 때(이벤트), 짐을 들고 게이트로 갈 때(또 다른 이벤트)마다 잠깐 멈춰 세워 “이 안내문을 더 들려줘라”(컨텍스트 주입)거나 “이 짐은 막아라”(도구 차단) 하고 쪽지(JSON) 한 장을 건넨 뒤 끝낸다. 비행기를 띄우거나 다시 부르는 것은 어디까지나 공항이다.
핵심 흐름을 한눈에 본다.
flowchart TD
E[이벤트 발생] --> S["OMC 스크립트 호출<br/>(stdin: 이벤트 정보)"]
S --> O["stdout: 결정 JSON 한 줄<br/>additionalContext / permissionDecision"]
O --> X[스크립트 종료]
X --> CC["나머지는 CC가 처리<br/>(모델 재호출·도구 우회·루프 진행)"]
요약하면: ①OMC는 루프가 없다 ②하는 일은 “끼어들기 지점 등록” ③이벤트가 울리면 CC가 스크립트를 stdin과 함께 불러준다 ④스크립트는 stdout으로 JSON 한 줄(additionalContext 주입 / permissionDecision: deny 차단)을 뱉는다 ⑤모델 재호출·도구 우회 등 나머지는 CC가 한다.
그래서 만들 것은 딱 셋
(1) 플러그인 매니페스트, (2) “어떤 이벤트에 어떤 스크립트를 꽂을지” 적은 hooks.json, (3) stdin을 읽고 stdout으로 JSON을 뱉는 스크립트. OMC의 “진입점”은 단 하나의 main()이 아니라, hooks.json에 나열된 여러 이벤트 진입점의 묶음이다.
핵심 정리
플러그인으로 인식되는 데 필요한 파일 묶음.
파일
역할
핵심 필드
.claude-plugin/plugin.json
플러그인 본체 매니페스트
name, version, skills[], mcpServers
.claude-plugin/marketplace.json
설치 카탈로그 항목
plugins[].source:"./"
.mcp.json
MCP 도구 서버 등록(t)
mcpServers.t.command/args
hooks/hooks.json
이벤트 → 스크립트 매핑(진입점 표)
<EventName>[].matcher, .hooks[]
hooks.json 한 항목과 결정 JSON의 핵심 필드만 추리면.
구분
필드
의미
입력(hooks.json)
matcher
필터(*=모두, Bash, init/maintenance)
입력
hooks[].command/timeout
node run.cjs <hook>.mjs 고정 · 타임아웃 초 단위
출력(결정 JSON)
additionalContext
모델에 추가 주입할 텍스트
출력
permissionDecision
도구 호출 허용/차단("deny")
펼쳐보기: 전체 필드표 (hooks.json 스키마 + 출력 필드)
hooks.json 한 항목의 스키마
필드
필수
설명
<EventName>
O
이벤트 이름별 등록 목록(한 이벤트에 매처 여러 개 가능)
[].matcher
O
하위 필터. "*"=모두, "Bash"=Bash만, "init"/"maintenance"=SessionStart의 source 분기
왜 모든 명령이 run.cjs를 거치나 — 공통 런처를 경유하는 이유 셋: ①크로스플랫폼(Windows에서 /bin/sh 없이 Node가 직접 .mjs 실행) ②CLAUDE_PLUGIN_ROOT가 낡았을 때(stale) 캐시에서 최신 스크립트를 찾아 fail-open ③timeout(초)을 읽어 ms로 환산.
실제 예시
플러그인 매니페스트와 MCP 서버 등록. plugin.json은 hooks를 직접 안 적는다 — CC가 플러그인 루트의 hooks/hooks.json을 관례적으로 읽는다.