지식위키

내 패턴 · 확장점: Hooks (전체 이벤트 라이프사이클과 system-reminder 주입)

내 패턴 · 확장점: 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가지다.

  1. 정해진 순간마다 방송이 켜진다. 손님이 체크인할 때·엘리베이터를 탈 때·방을 나설 때처럼, 하네스도 “사용자가 말할 때”, “도구 쓰기 직전·직후”, “세션 시작·끝”, “서브에이전트 켜짐·꺼짐”마다 등록된 작은 프로그램(훅)을 자동 실행한다.
  2. 방송 내용은 손님이 따른다. 훅의 핵심 일은 <system-reminder>·[MAGIC KEYWORD] 같은 꼬리표가 붙은 짧은 텍스트를 내뱉는 것. 하네스가 이를 대화 속에 끼워 넣는다.
  3. 모델은 누가 말했는지 모른다. 사람이 쓴 건지 코드가 뱉은 건지 구분 못 하고 “방금 들어온 지시”로 읽는다 → 코드가 모델을 옆구리에서 조종할 수 있다.
  4. 상태 파일로 손을 맞잡는다. 훅과 모델은 직접 대화하지 않고 메모지(상태 파일)를 주고받는다. 키워드 감지 시 “랄프 모드 켜짐” 메모를 남기고, 종료 훅이 그 메모를 보고 “아직 안 끝났다, 계속해”라고 판단한다.
  5. OMC는 이 통로로 자동화 층을 쌓는다. 키워드→스킬 실행, 멈추려는 루프 재주입, 기억 영구 저장 — 전부 이 방송 통로 위에서 돈다.

이 핸드셰이크 구조를 그림으로 보면:

flowchart LR
  P[프롬프트 키워드] -->|감지| ST1["상태 파일<br/>active=true 기록"]
  ST1 -.책상 위 메모.-> ST2[Stop 훅이 메모 읽음]
  ST2 -->|"active & 미완료"| BLK["종료 거부 → 재지시"]
  ST2 -->|"완료/취소"| OK[종료 허용]

[!note] 시지프스 비유 OMC 대표 문구 “The boulder never stops”(바위는 멈추지 않는다)는 시지프스 신화에서 왔다. 작업이 끝날 때까지 모델을 멈추지 못하게 계속 떠미는 장치다.


핵심 정리

가장 자주 쓰는 이벤트 5가지

이벤트언제 터지나대표 역할
UserPromptSubmit사용자가 프롬프트 보낼 때키워드 감지 → 스킬 주입
SessionStart세션 시작 시모드 복원·메모리·위키 주입
PreToolUse도구 쓰기 직전리마인더 주입 / 위반 시 차단
PostToolUse도구 쓴 직후결과 검증 / <remember> 영구 저장
Stop모델이 종료하려 할 때미완료면 종료 거부 = 랄프 루프

[!note]- 펼쳐보기: 전체 주입 신호 사전

  • [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> — 학습한 스킬 설명을 감싸 주입하는 꼬리표.

[!note]- 펼쳐보기: 직접 만들 때 안전장치 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 스크립트]

[!note]- 펼쳐보기: 전체 필드표 (매처 블록 + 훅 명령 + 주입 JSON) 매처 블록·훅 명령 필드

위치필드필수설명
매처 블록matcher발동 대상 필터. "*"(전체) / 도구명("Bash") / 소스("init"·"maintenance")
매처 블록hooks실행할 훅 명령 목록
훅 명령type현재 전부 "command" (셸 명령 실행)
훅 명령command런처(run.cjs) → 실제 스크립트(.mjs) 2단 구조
훅 명령timeout초 단위 (run.cjs가 ×1000 변환)

훅이 내뱉는 JSON (모델 주입 프로토콜)

필드쓰는 이벤트설명
continue전부보통 true. 흐름 계속
suppressOutput전부주입할 게 없을 때 true
hookSpecificOutput.additionalContextUserPromptSubmit/PreToolUse/PostToolUse/SessionStart모델 컨텍스트에 끼워 넣을 텍스트
hookSpecificOutput.permissionDecisionPreToolUse"deny" → 도구 실행 차단
decisionStop"block" → 종료 막고 계속 시킴 (랄프 루프 핵심)
reasonStopblock 사유 = 다시 주입되는 지시문
systemMessageSessionStart사용자에게 보여줄 메시지

[!note]- 펼쳐보기: 실제 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 정리"]

[!note]- 펼쳐보기: 전체 이벤트 → 스크립트 → timeout 표

이벤트matcher스크립트timeout핵심 역할
UserPromptSubmit*keyword-detector.mjs5키워드 감지 → [MAGIC KEYWORD] 주입 / state 생성
UserPromptSubmit*skill-injector.mjs3학습 스킬 매칭 → <mnemosyne> 주입
SessionStart*session-start.mjs5모드 복원([RALPH LOOP RESTORED]), 버전·HUD·업데이트 알림
SessionStart*project-memory-session.mjs / wiki-session-start.mjs5 / 5프로젝트 메모리·위키 주입
SessionStartinitsetup-init.mjs30신규 설치 초기화
SessionStartmaintenancesetup-maintenance.mjs60유지보수 분기
PreToolUse*pre-tool-enforcer.mjs3리마인더 주입 / 위반 시 permissionDecision: deny
PermissionRequestBashpermission-handler.mjs5Bash 권한 요청 자동 판정
PostToolUse*post-tool-verifier.mjs3결과 검증 + <remember> 영속 저장
PostToolUse*project-memory-posttool.mjs / post-tool-rules-injector.mjs3 / 3메모리 갱신 / 규칙 재주입
PostToolUseFailure*post-tool-use-failure.mjs3도구 실패 기록 → 재시도 가이드
SubagentStart*subagent-tracker.mjs start3서브에이전트 추적 시작
SubagentStop*subagent-tracker.mjs stop5추적 종료
SubagentStop*verify-deliverables.mjs5산출물 검증(경고만, 비차단)
PreCompact*pre-compact.mjs / project-memory-precompact.mjs / wiki-pre-compact.mjs10 / 5 / 3컴팩션 직전 컨텍스트 보존
Stop*context-guard-stop.mjs5컨텍스트 한계 종료 감지
Stop*persistent-mode.mjs10모드 활성 시 decision:block으로 종료 거부 → 랄프 루프
Stop*code-simplifier.mjs5종료 시 정리
SessionEnd*session-end.mjs / wiki-session-end.mjs30 / 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 }));

[!note]- 펼쳐보기: 최소 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줄 요약

  1. 훅은 정해진 순간마다 자동 실행되어 짧은 텍스트를 모델 컨텍스트에 끼워 넣어 행동을 바꾼다.
  2. 모델은 그 텍스트가 코드에서 왔는지 모르고 “방금 들어온 지시”로 따르므로, 코드가 모델을 우회 조종한다.
  3. 훅과 모델은 상태 파일로 악수한다 — 키워드가 모드를 켜고, Stop 훅이 그 모드를 보고 종료를 거부해 랄프 루프를 돌린다.

스스로 답해보기

  • 훅이 모델의 다음 행동을 바꾸는 두 가지 출력 경로는? (힌트: 대부분 이벤트 vs Stop 이벤트)
  • 사용자가 [RALPH LOOP - ITERATION 3]을 그대로 복붙해도 루프가 다시 켜지지 않는 이유는?
  • timeout: 5라고 적으면 실제 몇 ms가 적용되며, 그 변환은 누가 하나?

근거 파일

[!note]- 펼쳐보기: 전체 근거 파일 목록

  • .../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)