목차
- 왜 만들었나: 조용한 실패
- 38개 항목 루브릭
- 핵심 설계 원칙: 규칙 vs 모델
- 아키텍처: 서브에이전트 격리
- 검사 깊이 조절
- 등급 시스템
- 재미있었던 문제 1: 파괴적 명령 감지의 문맥 문제
- 재미있었던 문제 2: report의 서브에이전트 중첩 제약
- 셀프 평가: 자기 자신을 채점하면?
- 마치며
왜 만들었나: 조용한 실패
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 | 심각도 | 방식 | 내용 |
|---|---|---|---|
| V1 | MAJOR | model | 일반 Claude 프롬프트로 얻을 수 없는 조직 고유 지식이나 절차가 본문에 1줄 이상 있는가 |
| V2 | MAJOR | model | 기존 내장 스킬(/code-review, /security-review 등)과 크게 겹치지 않는가 |
V1이 핵심입니다. “이 스킬 없이 Claude에게 그냥 물어봐도 똑같은 결과를 얻을 수 있다”면 스킬로 만들 이유가 없습니다. 팀 고유의 규칙, 내부 도구 이름, 사내 관례 등이 최소 1줄 이상 있어야 합니다.
ST — 구조 (Structure) · 8개
frontmatter와 파일 자체의 구조적 유효성을 검사합니다. BLOCKER 3개가 모두 이 섹션에 있습니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| ST1 | BLOCKER | rule | frontmatter(--- 블록)가 유효한 YAML로 파싱되는가 |
| ST2 | BLOCKER | rule | description: 키가 존재하고 값이 비어 있지 않은가 |
| ST3 | MAJOR | rule | description 전체 길이가 1,536자 이하인가 |
| ST4 | MAJOR | rule | name 값이 ^[a-z][a-z0-9-]*$ 형식인가 (소문자·숫자·하이픈만) |
| ST5 | BLOCKER | rule | 파일이 UTF-8로 오류 없이 읽히는가 |
| ST6 | MINOR | rule | argument-hint가 있다면 <, [, ( 중 하나 이상을 포함하는가 |
| ST7 | MAJOR | rule | frontmatter에 중복 키가 없는가 |
| ST8 | MAJOR | rule | 본문(frontmatter 이후) 줄 수가 500줄 이하인가 |
ST2에는 단락 규칙이 있습니다. description이 아예 없으면 ST2만 BLOCKER로 기록하고 나머지 37개 항목은 전부 SKIP 처리합니다. frontmatter 자체가 없는 파일에서 의미 없는 연쇄 실패가 쏟아지는 것을 막기 위해서입니다.
F — 프론트매터 의미론 (Frontmatter Semantics) · 5개
frontmatter 각 필드의 값이 의미적으로 올바른지 검사합니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| F1 | BLOCKER | rule | name: 키가 존재하는가 (값 무관, 없으면 스킬을 이름으로 호출할 수 없음) |
| F2 | MAJOR | rule | disable-model-invocation: true 설정 시 description에 트리거 문구(“Use when”, “call this when” 등)가 없는가 |
| F3 | MAJOR | model | context: fork 설정 시 본문에 구체적인 작업 지시가 1개 이상 있는가 |
| F4 | MAJOR | rule | effort: 값이 low, medium, high, xhigh, max 중 하나인가 |
| F5 | MINOR | rule | tools: 값이 YAML 시퀀스 또는 쉼표 구분 문자열 형식인가 |
F2는 모순 검사입니다. “모델 호출을 끄겠다”고 설정해 놓고 description에 “이럴 때 호출하세요”라고 써 있으면, Claude가 스킬을 호출해야 하는지 말아야 하는지 판단할 수 없습니다.
T — 트리거 (Trigger) · 6개
Claude가 스킬을 언제, 어떻게 호출할지 판단하는 데 필요한 정보가 충분한지 검사합니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| T1 | MAJOR | model | description에 이 스킬이 무엇을 하는지(WHAT) 명확히 서술되어 있는가 |
| T2 | MAJOR | model | description에 언제 호출하는지(WHEN)가 명확히 서술되어 있는가 |
| T3 | MAJOR | rule | description이 1인칭(“I ”, “We “)이나 2인칭(“You “)으로 시작하지 않는가 |
| T4 | MAJOR | model | 트리거 조건이 다른 스킬과 충분히 구별되는가 |
| T5 | MINOR | model | WHAT과 WHEN이 description 첫 200자 안에 모두 있는가 |
| T6 | MINOR | rule | description이 50자 이상인가 |
T1·T2·T4가 트리거 품질의 핵심입니다. “도움이 필요할 때 사용하세요”는 T2를 형식적으로 통과하더라도 T4에서 실패합니다. 다른 스킬과 구별이 안 되기 때문입니다. T3는 작성 관례입니다 — description은 3인칭 동사로 시작해야 합니다(“Generates…”, “Evaluates…” 형태).
C — 콘텐츠 (Content) · 6개
본문이 실제로 유용한 내용을 담고 있는지 검사합니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| C1 | MAJOR | model | 공개 문서에서 얻을 수 없는 조직 고유 사실·규칙·절차가 1개 이상 있는가 |
| C2 | MAJOR | model | 입력→출력, 전→후, 명령→결과 형태의 실제 예시가 1개 이상 있는가 |
| C3 | MINOR | rule | ”as of”, “currently”, “현재”, 연도(2024 등) 같은 시한부 표현이 없는가 |
| C4 | MINOR | model | 같은 개념을 가리키는 용어가 본문 전체에서 일관되게 쓰이는가 |
| C5 | MINOR | model | 작업 위험도에 비례하는 지시 강도를 갖추고 있는가 (파일 삭제·배포 등 고위험 작업일수록 더 엄격하게) |
| C6 | MINOR | model | 실패 모드, 제약사항, “하지 말 것” 조항이 1개 이상 명시되어 있는가 |
C1과 C2는 콘텐츠 섹션의 두 MAJOR입니다. C1은 V1과 연결되어 있어서, 조직 고유 지식이 없으면 V1과 C1이 함께 실패하는 경우가 많습니다. C3는 미래에 틀려버릴 내용을 방지합니다. “2024년 기준으로 지원합니다”라는 문장은 1년 뒤 독자를 오도합니다.
R — 리소스 (Resources) · 8개
외부 파일 참조와 코드 블록의 건전성을 검사합니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| R1 | MINOR | rule | 본문이 200줄 초과인데 references/ 디렉토리가 없으면 분리 권장 |
| R2 | MINOR | rule | 30줄 초과 bash/python 블록이 있는데 scripts/ 디렉토리가 없으면 분리 권장 |
| R3 | MAJOR | rule | 본문에 절대 경로(/Users/, /home/, C:\ 등)가 없는가 |
| R4 | MAJOR | rule | 본문의 <<SKILL_DIR>>/path 참조가 실제 존재하는 파일을 가리키는가 |
| R5 | MAJOR | rule | 본문의 bash/python 코드 블록이 문법 오류 없이 파싱되는가 |
| R6 | MINOR | model | 본문의 셸 명령 시퀀스가 종료 동작이나 오류 처리(|| exit 1, set -e 등)를 명시하는가 |
| R7 | MINOR | rule | SKILL.md 전체 줄 수(frontmatter 포함)가 200줄 초과면 분리 권장 |
| R8 | MINOR | rule | references/ 디렉토리가 있다면 내부 파일이 1단계 이상 중첩되지 않는가 |
R3는 이식성 문제입니다. 절대 경로는 특정 머신에서만 동작합니다. R4는 존재하지 않는 파일을 가리키는 죽은 링크를 잡습니다. R5는 본문에 포함된 예시 코드가 실제로 문법적으로 올바른지 확인합니다.
SF — 안전성 (Safety) · 2개
배포하면 안 되는 보안·안전 문제를 검사합니다.
| ID | 심각도 | 방식 | 내용 |
|---|---|---|---|
| SF1 | BLOCKER | rule | 본문에 실제 시크릿(API 키, 비밀번호 등)이 없는가 |
| SF2 | MAJOR | rule+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 | 심각도 | 방식 | 내용 |
|---|---|---|---|
| X1 | MAJOR | model | 같은 디렉토리 내 두 스킬의 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 checks | model checks | 용도 |
|---|---|---|---|
--rules-only / --depth=shallow | ✓ | 생략 | pre-commit 훅 (빠름) |
| (기본) | ✓ | Sonnet | 일상적 점검 |
--depth=deep | ✓ | Opus | 배포 전 최종 게이트 |
커밋 전에는 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에서 확인할 수 있습니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.