내 패턴 · 확장점: 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[종료 허용]

시지프스 비유

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


핵심 정리

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

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

실제 예시

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 스크립트]

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 정리"]

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 }));

요약 & 셀프체크

3줄 요약

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

스스로 답해보기

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

근거 파일


연결

MINE_개요 · _분석축_루브릭 · MINE_80_state-memory-persistence(상태 파일 핸드셰이크·<remember> 영속) · MINE_40_skills-and-slash-commands(키워드→스킬 실행) · MINE_70_guardrails-permissions-sandbox(PreToolUse deny·PermissionRequest)