Claude Code의 스킬이 '선택되지만 읽히지 않는' 이유: 발화를 2단계로 나누어 측정하다
TL;DR
- 스킬이 사용되는지는 2단계로 결정된다. 1단계는 'description을 보고 후보에 포함되는가', 2단계는 '그 자리에서 모델이 실제로 그 스킬을 읽는가'이다.
- description 목록만 보여주고 선택하게 하는 테스트에서는 9/9로 선택된 스킬도 실제 작업에서는 한 번도 읽히지 않았다. description을 강화해도 변함이 없었으며, UserPromptSubmit hook을 통해 '이 스킬을 읽어라'라고 주입했을 때 비로소 처음으로 읽혔다.
- 스킬 간의 명시적 언급은 Parnas의 uses/invokes 구분을 빌려 '의존하는 쪽에서 단방향으로만' 설정했다. 이름을 지워도 다음 스킬은 읽혔다 (3패턴 × 3회, Codex에서도 동일한 순서).
- 시도 횟수는 조건별로 2~3회에 불과하다. 효과의 크기보다는 현상이 존재한다는 보고서로 읽어주시면 좋겠다.
직접 만든 스킬이 늘어나면서 '작성했는데 사용되지 않는' 상황을 여러 번 마주쳤다. 처음에는 description 작성 방식이 잘못되었다고 생각하여, 문구를 수정해 가며 목록 테스트를 통해 확인하는 과정을 반복했다.
하지만 목록 테스트에서는 만점을 받았음에도 불구하고, 실제 작업에서는 읽히지 않는 스킬들이 나왔다. 여기서 비로소 나는 '사용된다'는 것을 하나의 사건으로 측정하고 있었다는 사실을 깨달았다.
이 글은 그 구분과, 덧붙여 검토한 스킬 간의 참조 작성 방식을 정리한 것이다. 측정은 2026년 10월 21일~22일에 내 로컬 dotfiles 리포지토리(Claude Code와 Codex에서 공유하는 스킬군)를 통해 진행했다.
전제: 스킬은 어떻게 읽히는가
먼저 공식 설명을 확인해 보자. Anthropic의 엔지니어링 블로그에 따르면, 활성화 시 system prompt에는 각 스킬의 name과 description만 로드되며, 본문은 관련 있다고 판단되었을 때 읽힌다.
At startup, the agent pre-loads the name and description of every installed skill into its system prompt.
(시작 시, 에이전트는 설치된 모든 스킬의 name과 description을 system prompt에 사전 로드한다.) If Claude thinks the skill is relevant to the current task, it will load the skill by reading its full SKILL.md into context.
(Claude가 현재 작업과 관련 있다고 생각하면, SKILL.md 전체를 읽어 컨텍스트로 스킬을 로드한다.)
출처: Equipping agents for the real world with Agent Skills (Anthropic Engineering)
Claude Code의 문서에서도 description에 대해 'Claude uses this to decide when to apply the skill.'(Claude는 이것을 보고 언제 스킬을 적용할지 결정한다)라고 적혀있다 (Extend Claude with skills).
즉, 모델이 가진 단서는 description 목록과 눈앞의 요청문이다. 여기까지는 잘 알려진 이야기라고 생각한다.
발화는 2단계로 나누어 생각하기
내가 측정한 결과를 정리해 보면, 스킬이 사용되기까지에는 두 개의 관문이 있다.
그림 1: 스킬이 사용되기까지의 2단계
1단계를 통과했더라도, 2단계에서 다른 것을 먼저 읽고 답변을 끝내면 스킬은 사용되지 않는다.
식으로 쓰면 다음과 같다.
def p_used(p_candidate: float, p_read_given_candidate: float) -> float:
# P(사용됨) = P(후보에 포함될 확률) × P(후보에 포함된 상태에서 읽힐 조건부 확률)
return p_candidate * p_read_given_candidate
...
주의할 점은, 이 2단계는 실측을 정리하기 위한 나의 구분 방식일 뿐, Claude Code의 내부 구현으로 공개된 것은 아니라는 점이다.
그럼에도 불구하고, 이러한 구분 방식에는 실질적인 의미가 있습니다. description 목록만을 전달하고 '무엇을 선택할지'를 묻는 테스트(이하 목록 테스트)는 1단계의 능력만 측정하기 때문입니다.
목록 테스트는 9/9점이지만, 실제 작업에서는 0점
테스트에 사용된 것은 스킬을 분할 및 통합하는 절차를 정의한 재편 스킬(Reorganization Skill, 이하 재편 스킬)이었습니다.
목록 테스트에서는 요청된 9건의 모든 경우에서 재편 스킬이 선택되었습니다. 하지만 실제 세션에서 'Linear에 증거를 붙이는 스킬과 브라우저 조작을 GIF로 녹화하는 스킬 간의 영역 구분을 고려해 달라'고 요청하자, 모델은 명시적으로 언급된 두 개의 스킬을 직접 열어 답변했으며, 재편 스킬은 읽지 않았습니다.
| 조건 | 1회차 | 2회차 |
|---|
| 변경 전 (6턴 부여) | 읽히지 않음 | 읽히지 않음 |
| ... | | |
| 왜 description을 강화해도 효과가 없었을까요? 저의 해석은 이렇습니다. 요청 문구가 다른 스킬을 명시적으로 지칭하면, 모델은 '그것을 열면 답할 수 있다'고 판단합니다. 그리고 실제로 답변이 가능해지기 때문에, 절차를 정의한 재편 스킬을 건너뛰게 됩니다. description은 1단계의 단서일 뿐이며, 2단계인 '먼저 무엇을 열 것인가'라는 판단에는 도달하기 어렵습니다. | | |
다만, 각 조건별로 2회씩만 측정했습니다. 'description 강화가 효과가 없다'고까지는 말할 수 없으며, '이러한 요청 형태에서는 효과를 보지 못했다'라고 말하는 것이 정확합니다.
Codex에서도 hook을 추가한 후에도, 명시적 지칭이 포함된 두 번의 요청 모두 재편 스킬이 가장 먼저 읽혔습니다.
hook의 내용물
hook은 요청 문구가 조건에 맞는 경우에만 additionalContext로 '이 스킬을 읽으라'는 내용을 삽입합니다. UserPromptSubmit 훅은 프롬프트를 수정할 수는 없지만, additionalContext를 첨부하는 것은 가능합니다(Hooks reference).
// UserPromptSubmit hook (간략 버전. payload 로딩 및 스킬 이름 목록 가져오기는 생략)
const RESTRUCTURE_VERB = /棲み分け|整理|統合|分割|改名|分け方|重複/;
// '재편 동사'와 '실존하는 스킬명 또는 영단어 스킬'을 모두 포함할 때만 발동한다
...
동사만('영업과 CS의 영역 구분')이거나 일반 명사로서의 '스킬'인 경우에는 발동시키고 싶지 않기 때문에, 두 가지 조건을 걸었습니다. 그럼에도 불구하고 문자열만을 보고 있기 때문에, ' (그림을 그리는 스킬명)의 그림을 정리해 달라'와 같이 스킬명과 동사가 우연히 나란히 놓인 요청에서도 발동합니다. 놓치는 것보다는 과도하게 유도하는 편이 낫다고 판단하여, 이러한 오발동은 허용했습니다.
측정 방식에서 가장 어려웠던 점
사실, 측정 자체에서도 몇 번이나 실패를 경험했습니다. 요약하자면 다음과 같습니다.
| 측정하고 싶은 것 | 잘못된 측정 방식 | 발생한 일 | 올바른 측정 방식 |
|---|
| 1단계 | 여러 요청을 한 번의 프롬프트에 모아 판단시키기 | 답변들이 서로 독립적이지 않다. 모으면 1건이 틀렸지만, 독립적으로 6회씩 흘려보내면 변경 전/후 모두 6/6이었다 | 요청별로 독립적으로 흘려보내기 |
| ... | | | |
| 3번째 줄은 개인적으로 가장 큰 배움이 있었습니다. 불필요한 부작용을 피하고 싶어 도구를 제한했는데, 그 때문에 모델은 첫 번째 스킬을 읽었을 때 '더 이상 진행할 수 없다'고 판단하고 답변하며 끝마쳐 버립니다. 제가 보고 싶었던 것은 '다음 스킬로 연결하는지'였는데, 바로 그 앞에서 멈춰 있었습니다. | | | |
일반화하자면, 테스트 환경에서 다음 단계로 나아가는 수단을 제거하면, 측정하고 싶은 움직임 직전에 모델이 멈춘다는 것입니다. 측정하고 싶은 것이 '몇 번째 행동인가'를 먼저 결정하고, 거기까지 진행할 조건을 부여해야 합니다.
스킬 간의 명시적 지칭은 단방향으로만 하기
후반부는 연결(handover)을 측정하는 계기가 된 설계에 대한 이야기입니다.
제게는 결과물을 '만드는' 스킬과 '전달하는' 스킬이 있습니다. 만드는 쪽은 스크린샷 비교 이미지나 GIF 녹화를 만들고, 전달하는 쪽은 그것을 Linear의 코멘트나 PR 본문에 붙입니다. 처음에는 서로 상대방의 이름을 적어 주고받았습니다.
Parnas의 uses와 invokes
여기서 저는 Parnas가 1979년 논문에서 구분했던 'uses'와 'invokes' 개념을 빌려왔습니다(D. L. Parnas, "Designing Software for Ease of Extension and Contraction," IEEE TSE, 1979. 이하 Section IV-D에서 인용).
A가 B를 사용(uses)한다는 것은, A의 올바른 작동이 B의 올바른 구현 가능성에 의존하는 상황이 존재하는 경우이다.
만약 A의 사양(specification)이 특정 조건 발생 시 A가 B를 호출하기만 요구한다면, B에 대한 올바른 호출을 생성한 시점에서 A는 그 사양을 충족한 것이다. B가 부정확하거나 존재하지 않아도 A는 올바르다.
관계 "사용(uses)"을 그 그래프가 루프 프리(loop free)하도록 제한함으로써, 시스템 구성 요소들이 서로를 "사용"하는 주요 이점은 유지하면서 문제점을 제거할 수 있다.
스킬에 적용하기
여기부터는 저의 해석이며, Parnas 자신이 스킬(skill)에 대해 언급한 내용은 아닙니다.
| 참조 | 종류 | 이유 | 작성 방식 |
|---|
| 전달 → 생성 | 사용(uses) | 전달하는 측의 본문은, 생성하는 측이 제공하는 '결과물 + 이력 정보(어떤 커밋/환경/조건으로 만들었는지)'가 올바르지 않으면 작성할 수 없다 | 명시적으로 지칭해도 좋다 (단방향만 가능) |
| ... | | | |
| 그림 2: 명시하는 방향을 바꾸기 전과 후 | | | |
왼쪽은 서로를 상호 지칭하며 순환 구조가 있다. 오른쪽은 uses만을, 의존하는 쪽에서 단방향으로 지칭한다.
생성하는 측에 전달하는 측의 절차(예: '오펀 브랜치에 푸시하여 raw URL을 삽입')를 작성하는 것은 이름을 쓰는 것보다 더 강한 결합이 된다. 전달하는 측의 절차가 바뀌면, 생성하는 측의 기술 설명은 암묵적으로 구식이 되기 때문이다. 따라서 생성하는 측에는 '받는 주소별로 무엇을 제공할지'만 기재하도록 했다.
| 받는 주소 | 제공할 결과물 | 함께 제공할 것 |
|---|
| PR 본문 | GIF (MP4는 PR 본문에서 재생되지 않으므로 제공하지 않음) | 이력 정보의 모든 항목 |
| Linear 댓글 | GIF 또는 MP4 | 이력 정보의 모든 항목 |
"PR 본문에서는 MP4가 재생되지 않는다"는 것은 의존성이 아니라 사실이므로 유지해도 좋다.
이름을 제거해도 다음 스킬은 읽힐 수 있을까?
문제는, 생성하는 측에서 이름을 제거했을 때, 모델이 전달하는 측에 도달할 수 없게 되는 것이 아닌가 하는 점이다.
사전에 살펴본 메커니즘 하에서는, 인계 순간의 문맥에 다음 두 가지가 갖추어져 있다.
- 요청문에 받는 주소가 포함되어 있다 (예: 'Linear의
DEMO-42에 붙여서') - 이미 로드된 생성하는 측의 본문에 '해당 주소로 전달하는 스킬에게, 결과물과 이력 정보를 전달하라'는 내용이 있다.
나머지는 전달하는 측의 설명(description)이 받는 주소를 정확히 언급하고 있다면, 모델은 목록에서 선택할 수 있을 것이다. 이를 앞서 말한 '2턴째 질문하기' 방식으로 측정했다.
| 요청 | 1턴째에 읽힌 스킬 | '결과물은 완성되었다'고 전달한 2턴째 | 결과 |
|---|---|---|
| GIF를 녹화하여 DEMO-42에 붙여서 | GIF 녹화 | Linear에 전달하기 | 3/3 |
| 비교 이미지를 만들어서 PR #1 본문에 붙여서 | 비교 이미지 | PR 본문에 전달하기 | 3/3 |
| GIF를 녹화하여 PR #1 본문에 붙여서 | GIF 녹화 | PR 본문에 전달하기 | 3/3 |
Codex(codex exec)를 사용했을 때도, GIF→Linear와 비교 이미지→PR의 두 사례 모두 동일한 순서로 SKILL.md가 읽혔다.
결론적으로, 전달하는 스킬 자체의 설명(description)이 단서를 제공하는 중심이며, 생성하는 측에 작성된 이름은 필수적이지 않다는 것이다.
적용 범위는 명확히 해야 한다. 하나의 리포지토리, 네 개의 스킬, Claude Code와 Codex 사례를 통해 전달되는 스킬은 두 개밖에 없다. 전달되는 스킬이 늘어나고 description이 비슷해졌을 때에도 이름 없이 전달될 수 있는지는 아직 측정하지 않았다.
외부 관측과도 일치
같은 현상은 다른 환경에서도 관찰되었다.
- kaya님은 권한을 부여한 후에도 스킬 발동이 “체감상 약 25%”에 그쳤으며, UserPromptSubmit hook으로 평가를 강제하는 Forced Eval Hook의 경우 84%까지 상승한다고 언급했다 (84%는 이후 설명할 Scott Spence의 검증 수치 소개).
- Scott Spence의 검증에서는 단순한 지시를 삽입하는 hook이 전체적으로 20%(10/50)였고, Forced Eval Hook은 84%(42/50)였다. 단순한 hook은 여러 스킬이 관련된 작업에서는 0%가 되었다.
나는 이 둘 모두 “description을 다듬어도 2단계에서 읽히지 않는다”는 것을 보여준다고 해석한다.
스킬 간의 상호 참조에 대해서도, 143만 건의 스킬을 분석한 연구는 종속성을 갖는 root skill의 30.41%가 순환(cycle)을 포함한다고 보고했다 (Jia et al., "Skills Are Not Islands"). 해당 논문은 “Skills often reference one another bidirectionally, forming mutual-dependency clusters”라고 언급한다 (스킬은 종종 서로를 양방향으로 참조하며, 상호 의존 클러스터를 형성한다). 특정 이름을 한 방향으로 제한하는 것은 이 순환 구조가 처음부터 생기지 않도록 하는 수단이라고 생각한다.
요약
- 스킬 발동은 “후보에 포함되는지”와 “그 자리에서 읽히는지”의 2단계로 생각해야 한다. 목록 테스트(list test)로 측정할 수 있는 것은 1단계뿐이다.
- 요청문이 다른 스킬을 명시적으로 지정하면, 절차를 정한 스킬은 2단계에서 건너뛰어지기 쉽다. 내 환경에서는 description을 강화해도 변함이 없었으며, UserPromptSubmit hook으로 삽입했을 때 처음 읽혔다.
- 2단계나 인계는 실제 세션(실제 사용)에서 측정해야 한다. 다음 단계로 나아가는 수단을 제거하면, 측정하고 싶은 움직임 직전에 모델이 멈춘다.
- 스킬 간의 명시적 지정은 uses 관계만 종속하는 쪽에서 한 방향으로 작성한다. invokes는 목적지 유형으로 작성하면 이름 없이도 전달되었다.
실용적인 팁을 하나 드리자면, 새로 만든 스킬은 목록 테스트로 만족하지 말고, 그 스킬을 건너뛰어지기 쉬운 요청(다른 스킬을 명시적으로 지정하는 요청 등)으로 실제 세션을 2~3회 돌려보는 것이 좋다. 생각보다 읽히고 있지 않다는 것을 깨달을 수 있을 것이다.
앞으로는, 전달되는 스킬이 늘어났을 때의 오인식과 hook의 오발동 빈도를 조금 더 많은 횟수로 측정하고 싶다.
이상입니다.
Discussion