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

쉽게 풀기

플러그인을 새 직원에 비유하면 쉽다.

  1. 신분증 (plugin.json) — 사원증이다. “이름은 fable-ish, 버전은 0.1.2”라고 밝히고, 동시에 “회사 시설 중 두 가지만 쓰겠다”고 선언한다 — 바로 스킬.
  2. 시설 5개 중 2개만 — Claude Code가 열어주는 확장점은 다섯 종류(슬래시 커맨드·서브에이전트·MCP 서버·스킬·훅). fable-ish는 스킬·훅만 골라 쓰고 나머지 셋은 아예 신청하지 않는다(사원증에서 그 칸을 비움).
  3. 두 시설의 성격이 다르다
    • 스킬 = “이렇게 일하세요” 업무 매뉴얼. 모델이 필요할 때 스스로 펼쳐 읽는다(사람이 읽는 지침층).
    • = 회사가 정해진 순간마다 자동으로 돌리는 검문소. 프롬프트 진입 / 도구 사용 직후 / 종료 시도 — 세 시점에 파이썬이 자동 실행된다.
  4. 카탈로그 (marketplace.json) — 채용 목록에 올리는 명함첩. “리포=마켓=플러그인”을 한 폴더에 합쳐, 출처를 "./"(나 자신)로 적는 자기참조 구조다.
  5. 배선도 (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개 파이썬

훅이 도는 3개 시점

  • UserPromptSubmit — 프롬프트 제출 시 → 난이도(quick/normal/deep/blocked) 분류 + 모드 지침 주입
  • PostToolUse — 도구(Bash|Edit|Write|MultiEdit|NotebookEdit) 사용 후 → 변경·검증 증거 기록
  • Stop — 턴 종료 시도 시 → 검증 없이 끝내려 하면 되돌림(최대 2회 차단 후 허용)

${CLAUDE_PLUGIN_ROOT} 변수

Claude Code가 설치 위치를 런타임에 이 환경변수로 넣어준다. hooks.json은 절대경로 대신 python3 "${CLAUDE_PLUGIN_ROOT}/hooks/xxx.py"로 적어 어디 설치돼도 동작한다. 파이썬 쪽도 같은 변수를 읽어 데이터 경로를 잡는다(ledger.pyplugin_root()CLAUDE_PLUGIN_ROOT/PLUGIN_ROOT를 먼저 보고, 없으면 Path(__file__).parents[1] 폴백).

실제 예시

A. plugin.json — 플러그인 본인 매니페스트

핵심은 마지막 두 키, skills("./skills/" 한 곳)와 hooks("./hooks/hooks.json" 별도 파일 위임). 나머지는 식별·메타데이터다.

B. marketplace.json — 단일 플러그인 카탈로그

핵심은 plugins[].source: "./" — “이 마켓 리포의 루트가 곧 플러그인 본체”라는 자기참조. 하위 폴더나 git URL로 분리하지 않는다.

C. hooks/hooks.json — 생명주기 → 스크립트 배선

matcher는 정규식으로 “어떤 도구에 반응할지” 결정(PostToolUse에만 있음). type"command"(셸 실행형)이고 command${CLAUDE_PLUGIN_ROOT} 변수로 경로를 해석한다.

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을 컨텍스트에 다시 넣어, 검증 없이는 끝낼 수 없게 만든다.

E. 직접 만들 때 최소 골격

my-plugin/
├── .claude-plugin/
│   ├── plugin.json        # 신분증 + 확장점 선언
│   └── marketplace.json   # 카탈로그 (source: "./")
├── hooks/
│   └── hooks.json         # 생명주기 → 스크립트 배선
└── skills/
    └── my-plugin/
        └── SKILL.md       # name/description frontmatter

만들기 체크리스트:

  • plugin.json name = 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 파일로 시점 간 상태 공유를 확보한다.

스스로 답해보기:

  1. 5개 확장점 중 fable-ish가 의도적으로 비워 둔 셋은 무엇이고, plugin.json에서 어떻게 확인하는가?
  2. 훅은 모델이 호출하는가, 하네스가 실행하는가? skills와의 차이를 한 문장으로.
  3. Stop 훅이 무한루프에 빠지지 않게 막는 두 장치는?

연결

FB_개요 · _분석축_루브릭 · FB_20_hook-event-loop · FB_80_skill-workflow-layer

Codex 교차검증 메모

원 분석의 사실 골격(확장점 2개 = skills/hooks, 훅 3시점 = UserPromptSubmit/PostToolUse/Stop, source: "./" 자기참조, ${CLAUDE_PLUGIN_ROOT} 경로 해석, ledger 공유·failed open)은 근거 파일 기준으로 유지했다.