fable-ish · 컨텍스트/프롬프트 주입 (additionalContext 채널)
fable-ish · 컨텍스트/프롬프트 주입 (additionalContext 채널)
한 줄 요약
fable-ish는 시스템 프롬프트를 갈아끼우는 대신, 클로드 코드가 제공하는 훅 출력 규약(additionalContext)으로 그때그때 짧은 지침 문장만 모델 귀에 속삭이는 방식으로 행동을 조종한다.
왜 배우나 — “모델을 통제한다”는 게 꼭 거창한 프롬프트 교체가 아니라, 런타임에 한 문장 얹기 + 멈춤 거부 두 채널만으로도 가능하다는 핵심 설계를 익히기 위해.
그림
flowchart TD
A[사용자 프롬프트 제출] --> B[UserPromptSubmit 훅]
B --> C[classify_prompt 정규식 분류]
C --> D["ledger 기록 + context_for_mode"]
D --> E["additionalContext 주입: 소프트"]
E --> M[모델 추론]
M --> T["Bash/Edit/Write 도구 호출"]
T --> P["PostToolUse 훅: detect_failure"]
P -->|실패| F["완료보고 금지 문장 주입: 소프트"]
P -->|정상| G["ledger 누적, 무개입"]
F --> M
M --> S[턴 종료 시도]
S --> ST["Stop 훅: should_block_stop"]
ST -->|차단| R["decision:block + reason: 하드 게이트"]
ST -->|통과| Z[종료 허용]
R --> M
쉽게 풀기
비유로 풀면, fable-ish는 모델 옆에 붙은 작은 비서 세 명이다. 모델의 성격(시스템 프롬프트)을 바꾸지 않고, 상황마다 한마디씩 거든다.
- 입구의 분류 비서 (UserPromptSubmit) — 사용자가 부탁을 던지는 순간, 작은 파이썬 분류기가 먼저 읽는다. “이건 가벼운 질문이네 / 위험한 배포 작업이네”를 정규식으로 판정한 뒤, 그 판단을 한두 문장으로 적어 매 프롬프트마다 모델 입력에 슬쩍 끼워 넣는다. 명령이 아니라 조언이라 모델을 멈추지는 않는다.
- 현장의 감독 비서 (PostToolUse) — 모델이
Bash/Edit/Write같은 도구를 쓴 직후, 결과가 실패였는지 본다. 실패면 “고치기 전엔 완료라고 말하지 마”라고 딱 한 문장을 더 속삭인다. 성공이면 아무 말 안 하고(빈{}) 기록만 남긴다. - 출구의 문지기 비서 (Stop) — 모델이 “다 했습니다” 하고 턴을 끝내려 하면, 일을 진짜 끝냈는지 검사한다. 검증 안 한 변경이 남아 있으면 아예 멈춤을 거부하고(“block”) “이 이유로 더 일해” 하고 되돌려 보낸다. 이건 조언이 아니라 강제다.
핵심 비대칭 — 앞 두 비서는 “얹기”만 하지 막지 않는 소프트 가이드, 출구 문지기만 멈춤 자체를 거부하는 하드 게이트다. 이 차이가 이 노트의 알맹이다.
또 하나 기억할 점: fable-ish는 CLAUDE.md / AGENTS.md 같은 영구 지침 파일을 만들지 않는다. 오히려 agents.md라는 파일은 변경 분류기가 “코드”가 아니라 “docs(문서)“로만 취급한다.
핵심 정리
세 채널은 같은 훅 규약을 쓰지만 효과의 세기가 다르다.
| 시점 | 채널 | 효과 |
|---|---|---|
| UserPromptSubmit | additionalContext (다줄) | 모드+위험+규칙 선주입 (소프트) |
| PostToolUse 실패 시 | additionalContext (한 문장) | “완료 보고 금지” 억제 (소프트) |
| Stop 차단 시 | decision:block + reason | 멈춤 거부, 강제 재가동 (하드) |
[!note] 훅이 내보내는 JSON 봉투의 키
hookSpecificOutput— 컨텍스트 주입용 컨테이너 (주입 시 필수)hookSpecificOutput.hookEventName—"UserPromptSubmit"/"PostToolUse"/"Stop"중 하나hookSpecificOutput.additionalContext— 모델에 실제로 주입되는 자연어 문자열decision— Stop 훅에서"block"이면 멈춤을 막고 재가동reason—decision=block일 때 필수. 차단 사유 = 모델에게 다시 일하라는 지시systemMessage— 선택. 사용자/세션에 보이는 메시지(주입과 별개, fail-open·경고용)- 빈 객체
{}를 내보내면 무개입(아무것도 주입 안 함)
[!note] 주입 문장을 만드는
context_for_mode()의 출력 구성
fable-ish task mode: {mode}.— 항상 (모드: quick/normal/deep/blocked)Risk flags: a, b.— risk_flags가 있을 때만- 모드별 한 줄 — quick=간결히 / normal=변경 시 검증 1개 / deep=종료증명 정의 후 검증 / blocked=경계 경고
Never claim verification that was not actually observed.— 항상 (마지막 고정 문장)- 최종적으로 최대 10줄로 잘림:
"\n".join(lines[:10])
차이의 핵심을 한 번 더: 앞 두 개는 additionalContext로 “조언”만 얹어 멈춤을 막지 않는다. Stop은 additionalContext가 아니라 decision/block 채널을 써서 턴을 끝내지 못하게 강제한다. 단, stop_hook_active가 이미 true이거나 max 차단 횟수에 도달하면 Stop도 additionalContext로 부드럽게 빠진다(무한 루프 방지).
각 훅이 작동하는 조건 체크리스트:
- UserPromptSubmit — 매 프롬프트마다.
classify_prompt()가 quick/normal/deep/blocked + risk_flags(production/database/secret-or-auth/remote-write/destructive) 판정 →context_for_mode()로 문장화 → 주입. 동시에 ledger(세션·cwd 해시 키 JSON) 초기화/기록. -
"fable-ish: run/add/resolve "로 시작하는 연속 프롬프트는 재분류 없이 ledger의 기존 mode/risks로 같은 컨텍스트를 재주입. - PostToolUse —
^(Bash|Edit|Write|MultiEdit|NotebookEdit)$매처에 걸리는 호출 직후.detect_failure()가 종료코드/success/ok필드 또는 FAILURE_RE로 실패 감지 시 한 문장 주입. 변경 경로·검증 결과·커버리지는 항상 ledger에 누적. - Stop — 턴 종료 시점.
should_block_stop()이 모드·변경여부·검증여부로 판정. deep인데 미검증·변경됨 / normal인데 변경됨·미검증 / blocked 등이면 차단. -
stated_but_unstarted()— 마지막 어시스턴트 턴이 “이제 ~하겠습니다”식 예고만 하고 tool_use도 질문도 없으면 차단. -
MAX_STOP_BLOCKS=2도달 시·stop_hook_active시엔 차단 해제.
실제 예시
주입 문자열을 만드는 함수
# /home/seunghyeong/harness-work/fable-ish/scripts/classify_task.py
def context_for_mode(mode: str, risk_flags: list[str]) -> str:
lines = [f"fable-ish task mode: {mode}."]
if risk_flags:
lines.append("Risk flags: " + ", ".join(risk_flags) + ".")
if mode == "quick":
lines.append("Keep the response concise; do not force deep planning or broad verification.")
elif mode == "normal":
lines.append("If files change, run one relevant verification command or state why none applies.")
elif mode == "deep":
lines.append("Define the exit proof before completion and verify changed behavior before final.")
elif mode == "blocked":
lines.append(
"This request touches a sensitive or destructive boundary. Confirm scope, prefer the safest "
"reversible action, and stop for user confirmation when the next step needs credentials, "
"irreversible remote writes, or destructive deletes. Rely on Claude Code permissions for hard enforcement."
)
lines.append("Never claim verification that was not actually observed.")
return "\n".join(lines[:10])
세 채널의 실제 주입 코드
# /home/seunghyeong/harness-work/fable-ish/hooks/user_prompt_submit.py
mode, risks, goal = classify_prompt(prompt)
# ... 대장(ledger)에 mode/risks 등을 기록한 뒤 ...
emit_json(
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": context_for_mode(mode, risks),
}
}
)
# /home/seunghyeong/harness-work/fable-ish/hooks/post_tool_use.py
if failure:
emit_json(
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "fable-ish observed a tool failure. Do not report completion until it is fixed, isolated as baseline, or explicitly documented.",
}
}
)
else:
emit_json({})
# /home/seunghyeong/harness-work/fable-ish/hooks/stop_gate.py
ledger = load_ledger(input_data)
block, reason = should_block_stop(ledger)
if block:
ledger["stop_blocks"] = int(ledger.get("stop_blocks") or 0) + 1
save_ledger(input_data, ledger)
emit_json({"decision": "block", "reason": reason})
return 0
# /home/seunghyeong/harness-work/fable-ish/scripts/ledger.py (agents.md → docs 로만 인식)
def classify_path_kind(path_value: str) -> str:
path = Path(path_value)
name = path.name.lower()
suffix = path.suffix.lower()
parts = {part.lower() for part in path.parts}
if suffix in DOC_EXTS or name in {"readme", "readme.md", "agents.md"} or "docs" in parts:
return "docs"
...
직접 만들 때 최소 템플릿
#!/usr/bin/env python3
# my_hook.py — UserPromptSubmit 용 최소 컨텍스트 주입기
import sys, json
def main():
raw = sys.stdin.read()
data = json.loads(raw) if raw.strip() else {}
prompt = str(data.get("prompt") or "").lower()
# 1) 아주 단순한 분류
mode = "deep" if ("deploy" in prompt or "배포" in prompt) else "quick"
risks = ["production"] if "production" in prompt else []
# 2) 주입 문자열 조립
lines = [f"my-gate task mode: {mode}."]
if risks:
lines.append("Risk flags: " + ", ".join(risks) + ".")
lines.append("Never claim verification that was not actually observed.")
# 3) additionalContext 채널로 주입
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "\n".join(lines),
}
}, ensure_ascii=True))
if __name__ == "__main__":
try:
main()
except Exception as exc: # fail-open: 절대 사용자 흐름을 막지 않음
print(json.dumps({"systemMessage": f"hook failed open: {exc}"}))
raise SystemExit(0)
Stop 차단(하드 게이트) 형식만 따로:
# 멈춤을 거부하고 다시 일 시키기
print(json.dumps({"decision": "block",
"reason": "Run the verification command before finishing."}))
[!warning] 직접 만들 때 잊지 말 원칙
- 주입은
additionalContext(소프트), 강제 재가동은decision:block+reason(하드)로 분리.- 모든 훅은 fail-open: 예외 시 빈/systemMessage만 내고
exit 0. 무개입은 빈{}.- 무한 차단 방지: 차단 횟수 상한(예: 2) +
stop_hook_active체크.- 시스템 프롬프트를 새로 만들지 말 것 — 런타임 컨텍스트 한 문장 주입으로 충분.
요약 & 셀프체크
3줄 요약
- fable-ish는 시스템 프롬프트를 바꾸지 않고 훅 출력의
additionalContext로 짧은 지침 문장을 런타임에 얹는다. - 입구(분류)·현장(실패 감지)은 멈춤을 막지 않는 소프트 가이드, 출구(Stop)만
decision:block으로 하드 게이트를 건다. - 모든 훅은 fail-open이고, Stop은
MAX_STOP_BLOCKS·stop_hook_active로 무한 루프를 방지한다.
스스로 답해보기
additionalContext로 주입하는 것과decision:block으로 막는 것의 결정적 차이는 무엇인가?- PostToolUse 훅이 성공일 때 빈
{}를 내보내는 이유는? (무개입의 의미) - Stop 훅이 끝없이 차단하지 않도록 막는 두 가지 안전장치는?
연결
FB_개요 · _분석축_루브릭 · FB_20_hook-event-loop · FB_40_task-classification-engine · FB_50_evidence-ledger-state · FB_70_stop-completion-gate · FB_90_guardrails-soft-vs-hard
[!tip] Codex 교차검증 (원문 보존) 이 노트의 근거 파일들이다. 사실관계는 아래 소스에서 교차 확인할 것.
- /home/seunghyeong/harness-work/fable-ish/scripts/classify_task.py —
classify_prompt,context_for_mode- /home/seunghyeong/harness-work/fable-ish/hooks/user_prompt_submit.py — UserPromptSubmit additionalContext 주입, 연속 프롬프트 재주입
- /home/seunghyeong/harness-work/fable-ish/hooks/post_tool_use.py — 실패 감지 시 “완료 보고 금지” 주입
- /home/seunghyeong/harness-work/fable-ish/hooks/stop_gate.py —
decision:block+reason하드 게이트, stop_hook_active 해제- /home/seunghyeong/harness-work/fable-ish/hooks/hooks.json — 세 이벤트 훅 등록·매처·timeout
- /home/seunghyeong/harness-work/fable-ish/scripts/ledger.py —
classify_path_kind(agents.md→docs), redact, ledger 저장- /home/seunghyeong/harness-work/fable-ish/scripts/parse_tool_result.py — detect_failure, verification_coverage
- /home/seunghyeong/harness-work/fable-ish/scripts/verify_state.py — should_block_stop, stated_but_unstarted, MAX_STOP_BLOCKS
- /home/seunghyeong/harness-work/fable-ish/skills/fable-ish/SKILL.md, references/verification.md — 워크플로 지침 계층(주입과 별개)
- /home/seunghyeong/harness-work/fable-ish/.claude-plugin/plugin.json, marketplace.json — 플러그인 메타(시스템프롬프트 없음, skills+hooks만)