fable-ish · 플러그인 매니페스트와 확장점 배선
fable-ish · 플러그인 매니페스트와 확장점 배선
한 줄 요약
fable-ish 플러그인은 세 개의 JSON 파일로 자기 신분(이름·버전)을 밝히고, Claude Code 확장점 중 스킬과 훅 단 두 개만 꽂아 검증 게이트를 만든다.
왜 배우나: 이 세 JSON이 플러그인의 골격이라, 여기서부터 베끼면 같은 구조의 검증 플러그인을 처음부터 만들 수 있다.
그림
매니페스트 3종이 자기를 등록하는 구조 → 그 중 훅이 도는 생명주기를 한 장으로 본다.
flowchart TD
subgraph 등록["신분증·배선도 (JSON 3종)"]
P["plugin.json<br/>(이름·버전·확장점 2개 선언)"]
M["marketplace.json<br/>(카탈로그, source: ./)"]
H["hooks.json<br/>(어느 순간 → 어떤 스크립트)"]
P -->|skills 키| S["skills/fable-ish/SKILL.md<br/>(사람이 읽는 작업 지침)"]
P -->|hooks 키| H
end
subgraph 런타임["설치 후 실제 동작"]
A["사용자 프롬프트 제출"] --> B["UserPromptSubmit 훅<br/>(난이도 분류 + 지침 주입)"]
B --> Mdl["모델 추론 / 도구 호출"]
Mdl --> C["PostToolUse 훅<br/>(변경·검증 증거 기록)"]
C --> Mdl
Mdl --> D["Stop 훅<br/>(검증 없이 끝내려 하면 되돌림)"]
end
H -.->|설치 시 배선| B
H -.-> C
H -.-> D
쉽게 풀기
플러그인을 새 직원에 비유하면 쉽다.
- 신분증 (
plugin.json) — 사원증이다. “이름은 fable-ish, 버전은 0.1.2”라고 밝히고, 동시에 “회사 시설 중 두 가지만 쓰겠다”고 선언한다 — 바로 스킬과 훅. - 시설 5개 중 2개만 — Claude Code가 열어주는 확장점은 다섯 종류(슬래시 커맨드·서브에이전트·MCP 서버·스킬·훅). fable-ish는 스킬·훅만 골라 쓰고 나머지 셋은 아예 신청하지 않는다(사원증에서 그 칸을 비움).
- 두 시설의 성격이 다르다
- 스킬 = “이렇게 일하세요” 업무 매뉴얼. 모델이 필요할 때 스스로 펼쳐 읽는다(사람이 읽는 지침층).
- 훅 = 회사가 정해진 순간마다 자동으로 돌리는 검문소. 프롬프트 진입 / 도구 사용 직후 / 종료 시도 — 세 시점에 파이썬이 자동 실행된다.
- 카탈로그 (
marketplace.json) — 채용 목록에 올리는 명함첩. “리포=마켓=플러그인”을 한 폴더에 합쳐, 출처를"./"(나 자신)로 적는 자기참조 구조다. - 배선도 (
hooks/hooks.json) — “어느 순간 → 어떤 스크립트”를 적은 전기 배선도. 경로는 절대경로 대신${CLAUDE_PLUGIN_ROOT}마법 변수를 써서 설치 위치가 어디든 알아서 찾아간다.
flowchart LR Plug["fable-ish 플러그인"] Plug -->|skills 키| Sk["스킬 <br/>모델이 스스로 호출"] Plug -->|hooks 키| Hk["훅 <br/>하네스가 자동 실행"] Plug -.->|키 없음| C1["슬래시 커맨드 "] Plug -.->|키 없음| C2["서브에이전트 "] Plug -.->|키 없음| C3["MCP 서버 "]
핵심 정리
| 파일 | 역할 | 한마디 |
|---|---|---|
plugin.json | 신분증 + 확장점 선언 | 이름·버전 + skills/hooks 두 키 |
marketplace.json | 카탈로그 | source: "./" 자기참조 |
hooks/hooks.json | 생명주기 배선 | 3시점 → 3개 파이썬 |
[!note] 훅이 도는 3개 시점
- UserPromptSubmit — 프롬프트 제출 시 → 난이도(quick/normal/deep/blocked) 분류 + 모드 지침 주입
- PostToolUse — 도구(
Bash|Edit|Write|MultiEdit|NotebookEdit) 사용 후 → 변경·검증 증거 기록- Stop — 턴 종료 시도 시 → 검증 없이 끝내려 하면 되돌림(최대 2회 차단 후 허용)
[!note]
${CLAUDE_PLUGIN_ROOT}변수 Claude Code가 설치 위치를 런타임에 이 환경변수로 넣어준다. hooks.json은 절대경로 대신python3 "${CLAUDE_PLUGIN_ROOT}/hooks/xxx.py"로 적어 어디 설치돼도 동작한다. 파이썬 쪽도 같은 변수를 읽어 데이터 경로를 잡는다(ledger.py의plugin_root()가CLAUDE_PLUGIN_ROOT/PLUGIN_ROOT를 먼저 보고, 없으면Path(__file__).parents[1]폴백).
실제 예시
A. plugin.json — 플러그인 본인 매니페스트
핵심은 마지막 두 키, skills("./skills/" 한 곳)와 hooks("./hooks/hooks.json" 별도 파일 위임). 나머지는 식별·메타데이터다.
[!note]- 펼쳐보기: 전체 필드표 + plugin.json 전문
필드 타입 필수 설명 namestring 식별자 "fable-ish". 스킬/디렉터리명과 일치versionstring (관례) "0.1.2". marketplace.json과 동일해야 정합descriptionstring 권장 한 줄 설명 author/contributorsobject/array 권장/선택 원작자·기여자 homepage/repository/license/keywords— 선택 메타데이터 skillsstring(경로) 확장점1 "./skills/"하나hooksstring(경로) 확장점2 "./hooks/hooks.json"위임// /home/seunghyeong/harness-work/fable-ish/.claude-plugin/plugin.json { "name": "fable-ish", "version": "0.1.2", "description": "Verification-gated workflow plugin for Claude Code tasks. Adds lightweight hooks for task classification, verification tracking, and stop-time completion review.", "author": { "name": "플라잉따릉이 (Agent Korea)" }, "contributors": [ { "name": "chrisryugj", "role": "Claude Code port" } ], "homepage": "https://github.com/chrisryugj/fable-ish", "repository": "https://github.com/chrisryugj/fable-ish", "license": "MIT", "keywords": ["verification", "workflow", "hooks", "verification-gate", "claude-code"], "skills": "./skills/", "hooks": "./hooks/hooks.json" }
B. marketplace.json — 단일 플러그인 카탈로그
핵심은 plugins[].source: "./" — “이 마켓 리포의 루트가 곧 플러그인 본체”라는 자기참조. 하위 폴더나 git URL로 분리하지 않는다.
[!note]- 펼쳐보기: marketplace.json 전문
// /home/seunghyeong/harness-work/fable-ish/.claude-plugin/marketplace.json { "name": "fable-ish", "owner": { "name": "chrisryugj" }, "plugins": [ { "name": "fable-ish", "source": "./", "description": "Verification-gated workflow for Claude Code — Fable-style discipline. Port of the Codex plugin by 플라잉따릉이 (Agent Korea).", "version": "0.1.2" } ] }
C. hooks/hooks.json — 생명주기 → 스크립트 배선
matcher는 정규식으로 “어떤 도구에 반응할지” 결정(PostToolUse에만 있음). type은 "command"(셸 실행형)이고 command는 ${CLAUDE_PLUGIN_ROOT} 변수로 경로를 해석한다.
[!note]- 펼쳐보기: hooks.json 전문 (3시점 배선)
// /home/seunghyeong/harness-work/fable-ish/hooks/hooks.json { "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_submit.py\"", "timeout": 10, "statusMessage": "Classifying fable-ish task" } ] } ], "PostToolUse": [ { "matcher": "^(Bash|Edit|Write|MultiEdit|NotebookEdit)$", "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.py\"", "timeout": 10, "statusMessage": "Recording fable-ish evidence" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_gate.py\"", "timeout": 10, "statusMessage": "Reviewing fable-ish completion" } ] } ] } }
D. 두 확장점이 모델에 들어가는 길이 다르다
스킬은 모델이 끌어당기고(pull), 훅은 하네스가 밀어넣는다(push). 같은 플러그인 안에서 컨텍스트 주입 경로가 정반대다.
flowchart LR
subgraph S["스킬 (pull)"]
M1["모델"] -->|트리거 매칭 시 호출| SK["SKILL.md 본문<br/>컨텍스트 진입"]
end
subgraph H["훅 (push)"]
HN["하네스"] -->|생명주기 시점| PY["파이썬 훅 실행"]
PY -->|stdin JSON 입력| PY
PY -->|stdout JSON 출력| M2["모델 컨텍스트 변경"]
end
- skills (
./skills/): Claude Code가skills/fable-ish/SKILL.md의 frontmatter(name,description)를 읽어 목록에 올린다. description 트리거(예: “fable-ish requests, debugging, refactoring, deployment”)에 맞으면 모델이 스스로 호출 → SKILL.md 본문이 컨텍스트로 진입. - hooks (
./hooks/hooks.json): 모델이 부르는 게 아니라 하네스가 생명주기 시점에 기계적으로 실행. stdin으로 JSON(prompt, tool_name, tool_input, tool_response, transcript_path, session_id, cwd 등)을 받고 stdout JSON으로 모델 컨텍스트에 영향을 준다.
왜 모델이 소비하나 — UserPromptSubmit의 additionalContext는 “이건 deep 모드, 검증 없이 완료 주장 금지” 같은 모드별 행동 지침이라 모델이 작업 깊이를 조정한다. Stop 훅의 {"decision":"block","reason":...}는 턴을 강제로 되돌려 reason을 컨텍스트에 다시 넣어, 검증 없이는 끝낼 수 없게 만든다.
[!note]- 펼쳐보기: 무한루프 방지 + ledger 상태 공유 상세 무한루프 방지:
stop_hook_active가 이미 true면 즉시 허용,MAX_STOP_BLOCKS=2회 차단 후 허용(대신 “검증 증거 누락을 최종 보고에 포함하라” 경고).상태 보관(ledger): 훅들은 서로 직접 통신하지 않고,
세션id|cwd를 sha256 해시한 키로 만든 JSON 파일({tmpdir}/fable-ish/ledgers/<24hex>.json)을 공유 원장으로 쓴다. UserPromptSubmit이 초기화·기록, PostToolUse가 증거 누적, Stop이 판정에 사용. 모든 훅은 예외 시failed open(차단 없이 통과)하도록 try/except로 감싼다.
E. 직접 만들 때 최소 골격
my-plugin/
├── .claude-plugin/
│ ├── plugin.json # 신분증 + 확장점 선언
│ └── marketplace.json # 카탈로그 (source: "./")
├── hooks/
│ └── hooks.json # 생명주기 → 스크립트 배선
└── skills/
└── my-plugin/
└── SKILL.md # name/description frontmatter
[!note]- 펼쳐보기: 복붙용 plugin.json / marketplace.json / hooks.json
// plugin.json (확장점 2개만) { "name": "my-plugin", "version": "0.1.0", "description": "한 줄 설명", "license": "MIT", "skills": "./skills/", "hooks": "./hooks/hooks.json" }// marketplace.json { "name": "my-plugin", "owner": { "name": "your-handle" }, "plugins": [ { "name": "my-plugin", "source": "./", "version": "0.1.0", "description": "..." } ] }// hooks.json (단일 PostToolUse 예) { "hooks": { "PostToolUse": [ { "matcher": "^(Bash|Edit|Write)$", "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/my_hook.py\"", "timeout": 10, "statusMessage": "Doing my thing" } ] } ] } }
만들기 체크리스트:
-
plugin.jsonname = marketplace plugins[].name =skills/<name>/디렉터리명이 전부 동일한가 -
version이 두 파일에서 일치하는가 - marketplace
source가"./"(자기참조) 또는 의도한 경로인가 - hooks.json command가 절대경로 대신
${CLAUDE_PLUGIN_ROOT}변수를 쓰는가(공백 대비 따옴표) - matcher 정규식이 의도한 도구만 잡는가(
^(Bash|Edit|Write)$) - 훅이 stdin JSON 읽고 stdout JSON 내보내며, 예외 시 failed open(종료 0) 하는가
- 안 쓸 확장점(commands/agents/mcpServers) 키는 넣지 않았는가
요약 & 셀프체크
- fable-ish는
plugin.json·marketplace.json·hooks.json세 JSON으로 등록되며, 확장점은 skills + hooks 두 개만 쓴다. - skills는 모델이 스스로 펼쳐 읽는 지침층(pull), hooks는 하네스가 정해진 시점에 자동 실행하는 검문소(push) 다.
- 훅은
UserPromptSubmit → PostToolUse → Stop세 시점에 걸리고,${CLAUDE_PLUGIN_ROOT}로 경로 독립성을, ledger 파일로 시점 간 상태 공유를 확보한다.
스스로 답해보기:
- 5개 확장점 중 fable-ish가 의도적으로 비워 둔 셋은 무엇이고, plugin.json에서 어떻게 확인하는가?
- 훅은 모델이 호출하는가, 하네스가 실행하는가? skills와의 차이를 한 문장으로.
- Stop 훅이 무한루프에 빠지지 않게 막는 두 장치는?
연결
FB_개요 · _분석축_루브릭 · FB_20_hook-event-loop · FB_80_skill-workflow-layer
[!tip] Codex 교차검증 메모 원 분석의 사실 골격(확장점 2개 = skills/hooks, 훅 3시점 = UserPromptSubmit/PostToolUse/Stop,
source: "./"자기참조,${CLAUDE_PLUGIN_ROOT}경로 해석, ledger 공유·failed open)은 근거 파일 기준으로 유지했다.[!note]- 펼쳐보기: 근거 파일 목록
.claude-plugin/plugin.json·.claude-plugin/marketplace.json·hooks/hooks.jsonhooks/user_prompt_submit.py·hooks/post_tool_use.py·hooks/stop_gate.pyscripts/classify_task.py·scripts/verify_state.py·scripts/ledger.py·scripts/parse_tool_result.pyskills/fable-ish/SKILL.md(모두/home/seunghyeong/harness-work/fable-ish/하위)