[Claude Code] Skill 품질 관리 플러그인 만들기

2026-07-02 hit count image

Claude Code 스킬 파일을 38개 항목으로 평가하는 skill-quality-plugin을 만들면서 정리한 설계 원칙과 구현 과정을 공유합니다. 규칙으로 잡는 결정적 문제, 모델로 판단하는 의미론적 문제, 그리고 서브에이전트 격리 패턴까지 다룹니다.

generative_ai

목차

왜 만들었나: 조용한 실패

Claude Code의 스킬(Skill)은 YAML frontmatter가 붙은 Markdown 지시문 파일입니다. 구조가 단순해서 만들기는 쉽지만, 문제는 잘못 만들어도 아무 에러가 나지 않는다는 점입니다.

  • description이 빠져 있으면? Claude가 그냥 스킬을 호출하지 않습니다.
  • 트리거 조건이 모호하면? 엉뚱한 상황에서 호출되거나, 필요한 순간에 호출되지 않습니다.
  • frontmatter의 YAML이 깨져 있으면? 스킬 자체가 로드되지 않습니다.

전부 조용한 실패(silent failure) 입니다. 스킬을 마켓플레이스에 배포하기 전에 이런 문제를 체계적으로 잡아낼 방법이 없어서, 직접 만들기로 했습니다. 그렇게 나온 것이 /skill-quality 플러그인입니다. 소스 코드는 GitHub에 공개되어 있습니다.

38개 항목 루브릭

/skill-quality는 루브릭(Rubric) 기반으로 스킬을 평가합니다.

루브릭은 평가 대상을 여러 기준으로 나눠 각 기준마다 등급이나 합격/불합격을 매기는 평가표입니다. 원래 교육 현장에서 과제나 발표를 채점할 때 자주 쓰는 방식인데, “이 코드가 좋은가?”처럼 주관적으로 흐르기 쉬운 판단을 기준별로 쪼개서 객관화할 수 있다는 점에서 소프트웨어 품질 평가에도 그대로 적용할 수 있습니다.

“좋은 스킬이란 무엇인가”를 체크 가능한 항목으로 쪼게 보았습니다. 최종적으로 7개 차원, 38개 항목이 됐습니다. 각 항목은 심각도(BLOCKER · MAJOR · MINOR)와 검사 방식(rule · model)으로 분류했습니다.

V — 타당성 (Validity) · 2개

스킬이 존재할 이유가 있는지 검증합니다.

ID심각도방식내용
V1MAJORmodel일반 Claude 프롬프트로 얻을 수 없는 조직 고유 지식이나 절차가 본문에 1줄 이상 있는가
V2MAJORmodel기존 내장 스킬(/code-review, /security-review 등)과 크게 겹치지 않는가

V1이 핵심입니다. “이 스킬 없이 Claude에게 그냥 물어봐도 똑같은 결과를 얻을 수 있다”면 스킬로 만들 이유가 없습니다. 팀 고유의 규칙, 내부 도구 이름, 사내 관례 등이 최소 1줄 이상 있어야 합니다.

ST — 구조 (Structure) · 8개

frontmatter와 파일 자체의 구조적 유효성을 검사합니다. BLOCKER 3개가 모두 이 섹션에 있습니다.

ID심각도방식내용
ST1BLOCKERrulefrontmatter(--- 블록)가 유효한 YAML로 파싱되는가
ST2BLOCKERruledescription: 키가 존재하고 값이 비어 있지 않은가
ST3MAJORruledescription 전체 길이가 1,536자 이하인가
ST4MAJORrulename 값이 ^[a-z][a-z0-9-]*$ 형식인가 (소문자·숫자·하이픈만)
ST5BLOCKERrule파일이 UTF-8로 오류 없이 읽히는가
ST6MINORruleargument-hint가 있다면 <, [, ( 중 하나 이상을 포함하는가
ST7MAJORrulefrontmatter에 중복 키가 없는가
ST8MAJORrule본문(frontmatter 이후) 줄 수가 500줄 이하인가

ST2에는 단락 규칙이 있습니다. description이 아예 없으면 ST2만 BLOCKER로 기록하고 나머지 37개 항목은 전부 SKIP 처리합니다. frontmatter 자체가 없는 파일에서 의미 없는 연쇄 실패가 쏟아지는 것을 막기 위해서입니다.

F — 프론트매터 의미론 (Frontmatter Semantics) · 5개

frontmatter 각 필드의 값이 의미적으로 올바른지 검사합니다.

ID심각도방식내용
F1BLOCKERrulename: 키가 존재하는가 (값 무관, 없으면 스킬을 이름으로 호출할 수 없음)
F2MAJORruledisable-model-invocation: true 설정 시 description에 트리거 문구(“Use when”, “call this when” 등)가 없는가
F3MAJORmodelcontext: fork 설정 시 본문에 구체적인 작업 지시가 1개 이상 있는가
F4MAJORruleeffort: 값이 low, medium, high, xhigh, max 중 하나인가
F5MINORruletools: 값이 YAML 시퀀스 또는 쉼표 구분 문자열 형식인가

F2는 모순 검사입니다. “모델 호출을 끄겠다”고 설정해 놓고 description에 “이럴 때 호출하세요”라고 써 있으면, Claude가 스킬을 호출해야 하는지 말아야 하는지 판단할 수 없습니다.

T — 트리거 (Trigger) · 6개

Claude가 스킬을 언제, 어떻게 호출할지 판단하는 데 필요한 정보가 충분한지 검사합니다.

ID심각도방식내용
T1MAJORmodeldescription에 이 스킬이 무엇을 하는지(WHAT) 명확히 서술되어 있는가
T2MAJORmodeldescription에 언제 호출하는지(WHEN)가 명확히 서술되어 있는가
T3MAJORruledescription이 1인칭(“I ”, “We “)이나 2인칭(“You “)으로 시작하지 않는가
T4MAJORmodel트리거 조건이 다른 스킬과 충분히 구별되는가
T5MINORmodelWHAT과 WHEN이 description 첫 200자 안에 모두 있는가
T6MINORruledescription이 50자 이상인가

T1·T2·T4가 트리거 품질의 핵심입니다. “도움이 필요할 때 사용하세요”는 T2를 형식적으로 통과하더라도 T4에서 실패합니다. 다른 스킬과 구별이 안 되기 때문입니다. T3는 작성 관례입니다 — description은 3인칭 동사로 시작해야 합니다(“Generates…”, “Evaluates…” 형태).

C — 콘텐츠 (Content) · 6개

본문이 실제로 유용한 내용을 담고 있는지 검사합니다.

ID심각도방식내용
C1MAJORmodel공개 문서에서 얻을 수 없는 조직 고유 사실·규칙·절차가 1개 이상 있는가
C2MAJORmodel입력→출력, 전→후, 명령→결과 형태의 실제 예시가 1개 이상 있는가
C3MINORrule”as of”, “currently”, “현재”, 연도(2024 등) 같은 시한부 표현이 없는가
C4MINORmodel같은 개념을 가리키는 용어가 본문 전체에서 일관되게 쓰이는가
C5MINORmodel작업 위험도에 비례하는 지시 강도를 갖추고 있는가 (파일 삭제·배포 등 고위험 작업일수록 더 엄격하게)
C6MINORmodel실패 모드, 제약사항, “하지 말 것” 조항이 1개 이상 명시되어 있는가

C1과 C2는 콘텐츠 섹션의 두 MAJOR입니다. C1은 V1과 연결되어 있어서, 조직 고유 지식이 없으면 V1과 C1이 함께 실패하는 경우가 많습니다. C3는 미래에 틀려버릴 내용을 방지합니다. “2024년 기준으로 지원합니다”라는 문장은 1년 뒤 독자를 오도합니다.

R — 리소스 (Resources) · 8개

외부 파일 참조와 코드 블록의 건전성을 검사합니다.

ID심각도방식내용
R1MINORrule본문이 200줄 초과인데 references/ 디렉토리가 없으면 분리 권장
R2MINORrule30줄 초과 bash/python 블록이 있는데 scripts/ 디렉토리가 없으면 분리 권장
R3MAJORrule본문에 절대 경로(/Users/, /home/, C:\ 등)가 없는가
R4MAJORrule본문의 <<SKILL_DIR>>/path 참조가 실제 존재하는 파일을 가리키는가
R5MAJORrule본문의 bash/python 코드 블록이 문법 오류 없이 파싱되는가
R6MINORmodel본문의 셸 명령 시퀀스가 종료 동작이나 오류 처리(|| exit 1, set -e 등)를 명시하는가
R7MINORruleSKILL.md 전체 줄 수(frontmatter 포함)가 200줄 초과면 분리 권장
R8MINORrulereferences/ 디렉토리가 있다면 내부 파일이 1단계 이상 중첩되지 않는가

R3는 이식성 문제입니다. 절대 경로는 특정 머신에서만 동작합니다. R4는 존재하지 않는 파일을 가리키는 죽은 링크를 잡습니다. R5는 본문에 포함된 예시 코드가 실제로 문법적으로 올바른지 확인합니다.

SF — 안전성 (Safety) · 2개

배포하면 안 되는 보안·안전 문제를 검사합니다.

ID심각도방식내용
SF1BLOCKERrule본문에 실제 시크릿(API 키, 비밀번호 등)이 없는가
SF2MAJORrule+model본문에 보호 장치 없는 파괴적 명령이 없는가 (rm -rf, DROP TABLE, git push --force 등)

SF1은 BLOCKER입니다. API 키가 포함된 스킬을 마켓플레이스에 배포하면 즉시 유출됩니다. <your-api-key> 같은 플레이스홀더나 example, dummy 같은 명백한 예시 값은 제외됩니다. SF2가 rule+model 2단계인 이유는 다음 섹션에서 설명합니다.

X1 — 교차 스킬 충돌 (Cross-skill Collision) · 1개

ID심각도방식내용
X1MAJORmodel같은 디렉토리 내 두 스킬의 description이 너무 유사해 Claude가 어느 쪽을 호출할지 구별하지 못하는가

/skill-quality report 명령에서만 실행됩니다. 여러 스킬을 한 번에 점검할 때 충돌 쌍을 찾아냅니다.

핵심 설계 원칙: 규칙 vs 모델

38개 항목을 정의하고 나니 자연스럽게 한 가지 원칙이 도출됐습니다.

결정적 문제는 규칙으로, 의미 문제는 모델로.

“YAML이 파싱되는가”, “name이 ^[a-z][a-z0-9-]*$에 매칭되는가”, “시크릿 패턴이 있는가” 같은 검사는 정답이 하나입니다. 이런 검사를 LLM에게 시키는 것은 낭비이고, 무엇보다 비결정적이 됩니다. 그래서 결정적 검사는 Haiku 모델이 실행하는 rule_checks 아톰에 몰아넣었습니다. Haiku는 스크립트 실행기 역할만 하므로 빠르고 저렴합니다.

반면 “이 description은 Claude가 트리거하기에 충분히 구체적인가”, “본문에 공개 문서에서 얻을 수 없는 조직 고유 지식이 있는가” 같은 질문은 규칙으로 표현할 수 없습니다. 이런 의미론적 판단은 Sonnet/Opus가 실행하는 model_checks 아톰이 담당합니다.

이 분리 덕분에 38개 항목 각각에 type: rule 또는 type: model이 명시되어 있고, 어떤 판단이 재현 가능하고 어떤 판단이 모델 의존적인지 명확해졌습니다.

아키텍처: 서브에이전트 격리

파일 구조는 다음과 같습니다.

skill-quality-plugin/
├── .claude-plugin/
│   └── plugin.json
├── skills/
│   └── skill-quality/
│       ├── SKILL.md              # 라우터 + 공통 정의
│       └── commands/
│           ├── check.md          # 단일 스킬 평가 (오케스트레이터)
│           ├── report.md         # 디렉토리 일괄 평가
│           ├── rubric.md         # 38개 항목 표시
│           ├── help.md
│           └── atoms/
│               ├── rubric-spec.md    # 항목 정의 (단일 소스)
│               ├── rule_checks.md    # 결정적 검사 (Haiku)
│               └── model_checks.md   # 의미 검사 (Sonnet/Opus)
└── fixtures/
    ├── example-s-grade/
    ├── example-b-grade/
    └── example-f-grade/

가장 신경 쓴 부분은 메인 세션의 컨텍스트를 오염시키지 않는 것입니다. 38개 항목을 검사하면 중간 산출물이 상당히 많이 나오는데, 이것이 전부 메인 컨텍스트에 쌓이면 이후 작업 품질이 떨어집니다.

그래서 check 커맨드는 오케스트레이터로만 동작합니다. rule_checks → model_checks 순서로 서브에이전트를 띄우고, 각 아톰은 상세 결과를 /tmp/skill-quality-rule.json, /tmp/skill-quality-model.json에 기록한 뒤 마지막 줄에 한 줄짜리 요약만 출력합니다.

>>> RESULT <<<
OK FAIL: BLOCKER st1,sf1; MAJOR st4; MINOR st6

메인 세션은 이 터미널 라인과 JSON 파일만 읽습니다. 평가 대상인 SKILL.md 자체를 메인 세션이 읽는 일은 없습니다.

검사 깊이 조절

모든 상황에서 Opus까지 돌릴 필요는 없습니다. 상황에 따라 검사 깊이를 선택할 수 있도록 세 단계 옵션을 제공합니다.

플래그rule checksmodel checks용도
--rules-only / --depth=shallow생략pre-commit 훅 (빠름)
(기본)Sonnet일상적 점검
--depth=deepOpus배포 전 최종 게이트

커밋 전에는 shallow로 구조 문제만 빠르게 잡고, 마켓플레이스 배포 직전에만 deep으로 의미론적 품질까지 점검하는 식입니다.

/skill-quality check ./my-skill --rules-only   # 빠른 검사
/skill-quality check ./my-skill --depth=deep    # 최종 게이트
/skill-quality report ./skills                  # 디렉토리 일괄 평가

등급 시스템

결과는 BLOCKER/MAJOR 개수로 등급이 결정됩니다. MINOR는 등급에 영향을 주지 않고 “Suggestions”로만 표시됩니다.

BLOCKER ≥ 1        → F
MAJOR 0            → S
MAJOR 1–2          → A
MAJOR 3–5          → B
MAJOR 6–9          → C
MAJOR 10+          → D

MINOR를 등급에서 뺀 것은 의도적입니다. 사소한 제안 때문에 등급이 깎이면 사람들이 루브릭 자체를 신뢰하지 않게 되기 때문입니다.

재미있었던 문제 1: 파괴적 명령 감지의 문맥 문제

안전성 항목 중 하나로 “본문에 보호 장치 없는 파괴적 명령이 없는가”를 검사하는 항목이 있습니다. rm -rf, DROP TABLE, git push --force 같은 패턴을 정규식으로 잡는 것 자체는 쉽습니다. 문제는 문맥입니다.

절대 `rm -rf /`를 실행하지 마세요. ← 경고 문서 (PASS여야 함)
정리 단계: rm -rf ./build 를 실행 ← 실제 실행 지시 (FAIL이어야 함)

정규식만으로는 둘을 구분할 수 없습니다. 그래서 이 항목만 rule+model 2단계 확인으로 설계했습니다. 1단계에서 rule이 패턴에 매칭된 라인 번호를 기록해 두면, 2단계에서 모델이 해당 라인만 꺼내 보고 경고/문서 문맥인지 실제 실행 지시인지 판정합니다. 모델의 판정이 규칙의 플래그를 최종적으로 대체합니다.

정규식의 재현성과 모델의 문맥 이해를 결합한, 이 플러그인에서 가장 마음에 드는 부분입니다.

재미있었던 문제 2: report의 서브에이전트 중첩 제약

report 커맨드는 디렉토리 안의 모든 스킬을 일괄 평가합니다. 처음에는 “스킬마다 check.md를 서브에이전트로 실행하면 되겠지”라고 생각했는데, 여기서 막혔습니다. check.md는 자기 자신이 서브에이전트를 띄우는 구조인데, 서브에이전트가 또 서브에이전트를 띄우는 것은 런타임이 차단합니다.

해결책은 claude --print(비대화형 CLI 호출)를 활용하는 것이었습니다. 스킬당 하나의 서브에이전트(병렬 최대 4개)를 띄우되, 각 서브에이전트는 sub-agent를 스폰하는 대신 Bash로 claude --print "..." 명령을 실행합니다. 이 명령으로 시작된 프로세스는 완전히 새로운 독립 세션이므로, 자체적으로 rule_checks와 model_checks를 서브에이전트로 띄울 수 있습니다. 중첩 제약을 우회하는 것이 아니라, 런타임 외부의 독립 프로세스를 통해 동일한 품질을 얻는 방식입니다.

report (메인 세션)
  └─ 서브에이전트 × 4 (병렬, Task 하니스)
       └─ claude --print "..." (독립 프로세스, 새 세션)
            ├─ rule_checks 서브에이전트 (haiku)
            └─ model_checks 서브에이전트 (sonnet/opus)

결과적으로 report도 check와 동일한 38개 항목 전체를 평가합니다. Claude Code 플러그인 환경에서는 claude CLI가 기본 설치되어 있으므로, 이 방식이 자연스럽게 적용됩니다.

셀프 평가: 자기 자신을 채점하면?

품질 평가 도구라면 당연히 자기 자신부터 통과해야 합니다. 플러그인의 SKILL.md에 대해 --depth=deep으로 셀프 평가를 돌렸습니다.

/skill-quality check: .../skill-quality/SKILL.md
══════════════════════════════════════════════════
Grade: A  (rubric v1.0, depth=deep)

MAJOR (1)
  [C2] No worked example — body shows output format patterns
       but no end-to-end command → result scenario
       Fix: Add a ## Example section with sample invocation and output

══════════════════════════════════════════════════
BLOCKER: 0  MAJOR: 1  MINOR: 0  SKIP: 9

처음 결과는 Grade A였습니다. --depth=deep의 opus 모델이 C2(worked example 없음)를 잡아낸 것입니다. --rules-only나 기본 depth로는 통과했던 항목입니다.

SKILL.md에 /skill-quality check ./... 명령과 그 출력을 보여주는 ### Example 섹션을 추가하고 다시 평가했습니다.

/skill-quality check: .../skill-quality/SKILL.md
══════════════════════════════════════════════════
Grade: S  (rubric v1.0, depth=deep)
══════════════════════════════════════════════════
BLOCKER: 0  MAJOR: 0  MINOR: 0  SKIP: 9

--depth=deep이 의미 있는 이유가 바로 이것입니다. 규칙 검사만으로는 “예시가 충분히 구체적인가”를 판단할 수 없고, 모델이 직접 읽어봐야 알 수 있습니다. 도구를 만드는 과정 자체가 좋은 스킬 작성 연습이 된 셈입니다.

마치며

이번 플러그인을 만들면서 배운 것을 정리하면 다음과 같습니다.

  • 품질 기준은 쪼개야 실행 가능해진다. “좋은 스킬”이라는 막연한 개념을 38개의 pass/fail 항목으로 분해하니, 비로소 자동화할 수 있었습니다.
  • 결정적 문제는 규칙으로, 의미 문제는 모델로. LLM에게 정규식 매칭을 시키지 말고, 정규식에게 문맥 판단을 시키지 마세요. 각각 잘하는 것이 다릅니다.
  • 컨텍스트 격리는 설계 초기부터. 서브에이전트가 임시 파일에 쓰고 한 줄 요약만 반환하는 패턴은, 긴 파이프라인에서 메인 세션을 깨끗하게 유지하는 가장 확실한 방법이었습니다.
  • 자기 자신에게 도구를 적용해 보라. 셀프 평가에서 나온 지적이 루브릭의 허점을 가장 많이 드러냈습니다.

스킬을 만들어 배포할 계획이 있다면, 배포 전에 한 번쯤 이런 루브릭으로 점검해 보는 것을 추천합니다. 조용한 실패는 사용자에게 발견되기 전에 잡는 것이 가장 쌉니다.

플러그인 소스 코드와 38개 루브릭 스펙 전체는 GitHub에서 확인할 수 있습니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS