내 패턴 · 확장점: Hooks (전체 이벤트 라이프사이클과 system-reminder 주입)
한 줄 요약
훅(Hooks)은 대화의 정해진 순간마다 자동 실행되어, 짧은 텍스트를 모델 컨텍스트에 끼워 넣어 행동을 바꾸는 “코드가 모델을 조종하는 통로”다. → 왜 배우나: OMC 자동화(키워드로 스킬 실행, 멈추려는 루프 다시 돌리기, 기억 영구 저장)가 전부 이 통로 위에서 돈다. 훅을 이해하면 하네스 자동화의 심장을 이해한다.
그림
flowchart TD U[사용자 프롬프트] --> H[UserPromptSubmit 훅 발동] H --> R["run.cjs 런처<br/>타임아웃 재계산·스크립트 실행"] R --> K[keyword-detector 스크립트] K -->|입력 JSON 읽고 정제| D{"매직 키워드 있나?"} D -->|있음| S["상태 파일 생성<br/>ralph-state.json 등"] S --> I["모델 컨텍스트에 주입<br/>[MAGIC KEYWORD]"] D -->|없음| N[조용히 통과] I --> M["모델: 스킬 시작·작업 수행"] M --> P["작업 중 PreToolUse 리마인더<br/>'바위는 멈추지 않는다'"] M --> ST{모델이 작업 끝내려 함} ST --> PM["Stop 훅: persistent-mode"] PM -->|"모드 켜짐 & 미완료"| B["종료 거부 + 재지시<br/>RALPH LOOP - ITERATION N"] B --> M PM -->|"완료/취소/한계도달"| END[종료 허용]
쉽게 풀기
훅을 호텔의 자동 안내 방송에 비유하면, 핵심은 5가지다.
- 정해진 순간마다 방송이 켜진다. 손님이 체크인할 때·엘리베이터를 탈 때·방을 나설 때처럼, 하네스도 “사용자가 말할 때”, “도구 쓰기 직전·직후”, “세션 시작·끝”, “서브에이전트 켜짐·꺼짐”마다 등록된 작은 프로그램(훅)을 자동 실행한다.
- 방송 내용은 손님이 따른다. 훅의 핵심 일은
<system-reminder>·[MAGIC KEYWORD]같은 꼬리표가 붙은 짧은 텍스트를 내뱉는 것. 하네스가 이를 대화 속에 끼워 넣는다. - 모델은 누가 말했는지 모른다. 사람이 쓴 건지 코드가 뱉은 건지 구분 못 하고 “방금 들어온 지시”로 읽는다 → 코드가 모델을 옆구리에서 조종할 수 있다.
- 상태 파일로 손을 맞잡는다. 훅과 모델은 직접 대화하지 않고 메모지(상태 파일)를 주고받는다. 키워드 감지 시 “랄프 모드 켜짐” 메모를 남기고, 종료 훅이 그 메모를 보고 “아직 안 끝났다, 계속해”라고 판단한다.
- OMC는 이 통로로 자동화 층을 쌓는다. 키워드→스킬 실행, 멈추려는 루프 재주입, 기억 영구 저장 — 전부 이 방송 통로 위에서 돈다.
이 핸드셰이크 구조를 그림으로 보면:
flowchart LR P[프롬프트 키워드] -->|감지| ST1["상태 파일<br/>active=true 기록"] ST1 -.책상 위 메모.-> ST2[Stop 훅이 메모 읽음] ST2 -->|"active & 미완료"| BLK["종료 거부 → 재지시"] ST2 -->|"완료/취소"| OK[종료 허용]
시지프스 비유
OMC 대표 문구 “The boulder never stops”(바위는 멈추지 않는다)는 시지프스 신화에서 왔다. 작업이 끝날 때까지 모델을 멈추지 못하게 계속 떠미는 장치다.
핵심 정리
가장 자주 쓰는 이벤트 5가지
| 이벤트 | 언제 터지나 | 대표 역할 |
|---|---|---|
| UserPromptSubmit | 사용자가 프롬프트 보낼 때 | 키워드 감지 → 스킬 주입 |
| SessionStart | 세션 시작 시 | 모드 복원·메모리·위키 주입 |
| PreToolUse | 도구 쓰기 직전 | 리마인더 주입 / 위반 시 차단 |
| PostToolUse | 도구 쓴 직후 | 결과 검증 / <remember> 영구 저장 |
| Stop | 모델이 종료하려 할 때 | 미완료면 종료 거부 = 랄프 루프 |
펼쳐보기: 전체 주입 신호 사전
[MAGIC KEYWORD: RALPH]— 프롬프트에서 키워드 발견 시 “이 스킬을 즉시 시작하라”.The boulder never stops. Continue until all tasks complete.— 모드 활성 중 도구 사용 직전 리마인더.[RALPH LOOP - ITERATION N/M] Work is NOT done. Continue working.— 종료 훅이 종료를 막으며 다시 던지는 지시문.<remember>...</remember>/<remember priority>...</remember>— 모델 출력에 넣으면 정규식으로 잡아 저장(일반=7일, priority=영구).<mnemosyne>— 학습한 스킬 설명을 감싸 주입하는 꼬리표.
펼쳐보기: 직접 만들 때 안전장치 4원칙
timeout은 초 단위 (런처가 ×1000 해서 ms로 변환).- 모든 에러 경로에서
{continue:true, suppressOutput:true}— 훅이 절대 하네스를 막지 않게.- 킬스위치
DISABLE_OMC=1,OMC_SKIP_HOOKS=<훅이름>가드를 맨 위에.- 에코 재주입 방지: 사용자가
[... LOOP ...]를 복붙해도 재발동 안 되게 정제.
실제 예시
1) hooks.json 구조와 필드
기본 골격은 이벤트 → 매처 블록 → 훅 명령의 3중 중첩이다.
{ "description": string, "hooks": { <이벤트명>: [ <매처블록>, ... ] } }
flowchart TD E["이벤트<br/>예: UserPromptSubmit"] --> MB["매처 블록<br/>matcher + hooks[]"] MB --> HC1["훅 명령 1<br/>type·command·timeout"] MB --> HC2["훅 명령 2<br/>(순서대로 실행)"] HC1 --> RUN[run.cjs 런처] --> SCR[실제 .mjs 스크립트]
펼쳐보기: 전체 필드표 (매처 블록 + 훅 명령 + 주입 JSON)
매처 블록·훅 명령 필드
위치 필드 필수 설명 매처 블록 matcher발동 대상 필터. "*"(전체) / 도구명("Bash") / 소스("init"·"maintenance")매처 블록 hooks실행할 훅 명령 목록 훅 명령 type현재 전부 "command"(셸 명령 실행)훅 명령 command런처(run.cjs) → 실제 스크립트(.mjs) 2단 구조 훅 명령 timeout초 단위 (run.cjs가 ×1000 변환) 훅이 내뱉는 JSON (모델 주입 프로토콜)
필드 쓰는 이벤트 설명 continue전부 보통 true. 흐름 계속suppressOutput전부 주입할 게 없을 때 truehookSpecificOutput.additionalContextUserPromptSubmit/PreToolUse/PostToolUse/SessionStart 모델 컨텍스트에 끼워 넣을 텍스트 hookSpecificOutput.permissionDecisionPreToolUse "deny"→ 도구 실행 차단decisionStop "block"→ 종료 막고 계속 시킴 (랄프 루프 핵심)reasonStop block 사유 = 다시 주입되는 지시문 systemMessageSessionStart 사용자에게 보여줄 메시지
펼쳐보기: 실제 hooks.json 발췌
// /home/seunghyeong/.claude/plugins/marketplaces/omc/hooks/hooks.json { "description": "OMC orchestration hooks with async capabilities", "hooks": { "UserPromptSubmit": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PLUGIN_ROOT\"/scripts/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/scripts/keyword-detector.mjs", "timeout": 5 }, { "type": "command", "command": "node \"$CLAUDE_PLUGIN_ROOT\"/scripts/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/scripts/skill-injector.mjs", "timeout": 3 } ] } ], "SessionStart": [ { "matcher": "*", "hooks": [ /* session-start, project-memory-session, wiki-session-start */ ] }, { "matcher": "init", "hooks": [ { "command": "... setup-init.mjs", "timeout": 30 } ] }, { "matcher": "maintenance", "hooks": [ { "command": "... setup-maintenance.mjs", "timeout": 60 } ] } ], "PreToolUse": [ { "matcher": "*", "hooks": [ { "command": "... pre-tool-enforcer.mjs", "timeout": 3 } ] } ], "PermissionRequest": [ { "matcher": "Bash", "hooks": [ { "command": "... permission-handler.mjs", "timeout": 5 } ] } ], "Stop": [ { "matcher": "*", "hooks": [ { "command": "... context-guard-stop.mjs", "timeout": 5 }, { "command": "... persistent-mode.mjs", "timeout": 10 }, { "command": "... code-simplifier.mjs", "timeout": 5 } ] } ] /* PostToolUse, PostToolUseFailure, SubagentStart, SubagentStop, PreCompact, SessionEnd 도 동일 패턴 */ } }
2) 전체 이벤트 라이프사이클
세션 한 번이 도는 동안 이벤트가 터지는 순서:
flowchart LR SS["SessionStart<br/>모드 복원·메모리"] --> UP["UserPromptSubmit<br/>키워드·스킬 주입"] UP --> PRE["PreToolUse<br/>리마인더/차단"] PRE --> POST["PostToolUse<br/>검증·remember"] POST -.반복.-> PRE POST --> STOP{Stop} STOP -->|미완료| UP STOP -->|완료| SE["SessionEnd<br/>state 정리"]
펼쳐보기: 전체 이벤트 → 스크립트 → timeout 표
이벤트 matcher 스크립트 timeout 핵심 역할 UserPromptSubmit *keyword-detector.mjs 5 키워드 감지 → [MAGIC KEYWORD]주입 / state 생성UserPromptSubmit *skill-injector.mjs 3 학습 스킬 매칭 → <mnemosyne>주입SessionStart *session-start.mjs 5 모드 복원( [RALPH LOOP RESTORED]), 버전·HUD·업데이트 알림SessionStart *project-memory-session.mjs / wiki-session-start.mjs 5 / 5 프로젝트 메모리·위키 주입 SessionStart initsetup-init.mjs 30 신규 설치 초기화 SessionStart maintenancesetup-maintenance.mjs 60 유지보수 분기 PreToolUse *pre-tool-enforcer.mjs 3 리마인더 주입 / 위반 시 permissionDecision: denyPermissionRequest Bashpermission-handler.mjs 5 Bash 권한 요청 자동 판정 PostToolUse *post-tool-verifier.mjs 3 결과 검증 + <remember>영속 저장PostToolUse *project-memory-posttool.mjs / post-tool-rules-injector.mjs 3 / 3 메모리 갱신 / 규칙 재주입 PostToolUseFailure *post-tool-use-failure.mjs 3 도구 실패 기록 → 재시도 가이드 SubagentStart *subagent-tracker.mjs start 3 서브에이전트 추적 시작 SubagentStop *subagent-tracker.mjs stop 5 추적 종료 SubagentStop *verify-deliverables.mjs 5 산출물 검증(경고만, 비차단) PreCompact *pre-compact.mjs / project-memory-precompact.mjs / wiki-pre-compact.mjs 10 / 5 / 3 컴팩션 직전 컨텍스트 보존 Stop *context-guard-stop.mjs 5 컨텍스트 한계 종료 감지 Stop *persistent-mode.mjs 10 모드 활성 시 decision:block으로 종료 거부 → 랄프 루프Stop *code-simplifier.mjs 5 종료 시 정리 SessionEnd *session-end.mjs / wiki-session-end.mjs 30 / 30 세션 state 정리·위키 마감
3) 직접 만들 때 최소 템플릿
system-reminder를 뱉는 주입 훅의 골격은 ① 킬스위치 → ② stdin JSON 읽기 → ③ 조건 만족 시 주입 → ④ 아니면 조용히 통과의 4단계다.
#!/usr/bin/env node
// scripts/my-detector.mjs
import { readStdin } from './lib/stdin.mjs';
async function main() {
// 1) 킬스위치 (맨 위에)
const skip = (process.env.OMC_SKIP_HOOKS || '').split(',').map(s => s.trim());
if (process.env.DISABLE_OMC === '1' || skip.includes('my-detector')) {
console.log(JSON.stringify({ continue: true })); return;
}
// 2) stdin JSON 읽기 (하네스가 prompt·cwd·session_id 등을 줌)
const input = await readStdin();
let data = {}; try { data = JSON.parse(input); } catch {}
const prompt = data.prompt || '';
// 3) 조건 만족 시 컨텍스트 주입
if (/\bmagicword\b/i.test(prompt)) {
console.log(JSON.stringify({
continue: true,
hookSpecificOutput: {
hookEventName: 'UserPromptSubmit',
additionalContext: '<system-reminder>\n[MAGIC KEYWORD: MYSKILL] 즉시 MYSKILL 워크플로를 시작하라.\n</system-reminder>'
}
}));
return;
}
// 4) 주입할 것 없으면 조용히 통과
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
}
main();Stop 훅으로 루프 만들기 — decision:block이 핵심이다.
// scripts/my-loop.mjs (요지) — persistent-mode.mjs와 동일 패턴
// state.active === true && 미완료 이면:
console.log(JSON.stringify({
decision: 'block',
reason: '[MY LOOP - ITERATION 3/100] 아직 안 끝났다. 계속하라. 완료 시 /cancel.'
}));
// 완료/취소면: console.log(JSON.stringify({ continue: true, suppressOutput: true }));펼쳐보기: 최소 hooks.json (UserPromptSubmit + Stop)
{ "description": "my hooks", "hooks": { "UserPromptSubmit": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PLUGIN_ROOT\"/scripts/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/scripts/my-detector.mjs", "timeout": 5 } ] } ], "Stop": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PLUGIN_ROOT\"/scripts/run.cjs \"$CLAUDE_PLUGIN_ROOT\"/scripts/my-loop.mjs", "timeout": 10 } ] } ] } }
요약 & 셀프체크
3줄 요약
- 훅은 정해진 순간마다 자동 실행되어 짧은 텍스트를 모델 컨텍스트에 끼워 넣어 행동을 바꾼다.
- 모델은 그 텍스트가 코드에서 왔는지 모르고 “방금 들어온 지시”로 따르므로, 코드가 모델을 우회 조종한다.
- 훅과 모델은 상태 파일로 악수한다 — 키워드가 모드를 켜고, Stop 훅이 그 모드를 보고 종료를 거부해 랄프 루프를 돌린다.
스스로 답해보기
- 훅이 모델의 다음 행동을 바꾸는 두 가지 출력 경로는? (힌트: 대부분 이벤트 vs Stop 이벤트)
- 사용자가
[RALPH LOOP - ITERATION 3]을 그대로 복붙해도 루프가 다시 켜지지 않는 이유는? timeout: 5라고 적으면 실제 몇 ms가 적용되며, 그 변환은 누가 하나?
근거 파일
펼쳐보기: 전체 근거 파일 목록
.../hooks/hooks.json— 전체 이벤트/매처/timeout 정의.../scripts/run.cjs— 런처, timeout 초→ms 변환, 스테일 경로 폴백.../scripts/keyword-detector.mjs— 매직 키워드 감지,[MAGIC KEYWORD]/createHookOutput(additionalContext), state 활성화, 에코 정제, DISABLE_OMC/OMC_SKIP_HOOKS 가드.../scripts/skill-injector.mjs— 학습 스킬 매칭,<mnemosyne>주입.../scripts/session-start.mjs— 모드 복원([RALPH LOOP RESTORED]등),<system-reminder>/systemMessage, init·maintenance 별도 매처.../scripts/persistent-mode.mjs— Stop 훅decision:block+reason([RALPH LOOP - ITERATION N]), hardMax/extended 처리.../scripts/pre-tool-enforcer.mjs— 도구별 리마인더,The boulder never stops,permissionDecision:'deny'.../scripts/permission-handler.mjs— Bash PermissionRequest 위임.../scripts/subagent-tracker.mjs— start/stop 인자 분기 추적.../scripts/verify-deliverables.mjs— SubagentStop 산출물 검증(advisory).../scripts/post-tool-verifier.mjs—<remember>/<remember priority>정규식 처리.../scripts/post-tool-rules-injector.mjs— PostToolUse 규칙 재주입.../skills/ralph/SKILL.md— “The boulder never stops” 소비 규칙.../CLAUDE.md—<hooks_and_context>섹션(주입 신호·영속성·킬스위치 요약)(공통 경로:
/home/seunghyeong/.claude/plugins/marketplaces/omc/)
연결
MINE_개요 · _분석축_루브릭 · MINE_80_state-memory-persistence(상태 파일 핸드셰이크·<remember> 영속) · MINE_40_skills-and-slash-commands(키워드→스킬 실행) · MINE_70_guardrails-permissions-sandbox(PreToolUse deny·PermissionRequest)