L28 — 자연어로 스킬을 부를 때 오발동을 막는 법: description floor(hook)와 실행-직전 가드 (수정중)
이번 강의는 L11 Skills·L13 Hooks 위에 얹힌다. 스킬이 뭔지, hook 이 exit 2 로 어떻게 막는지는 그 두 강의에서 다뤘으니, 여기서는 "스킬이 늘어나면 생기는 문제" 와 그걸 standarda-template 이 어떻게 정리했는지(PR #29·#30)에 집중한다.
1. 문제: 자연어 호출은 "이름"이 아니라 "description"으로 결정된다
L11 에서 스킬은 /release 처럼 슬래시로도, "배포해줘" 처럼 자연어로도 부를 수 있다고 했다. 그런데 스킬이 2개, 5개, 11개로 늘어나면 새 문제가 생긴다 — Claude 가 자연어 요청에서 "어떤 스킬을 쓸지" 고를 때 1차 신호로 삼는 건 스킬 이름이 아니라 description 이다.
- 이름이 비슷해도 description 의 "무엇을 / 언제 쓰는지"가 또렷이 갈리면 오발동은 거의 없다.
- 이름이 달라도 description 이 두루뭉술하게 겹치면 엉뚱한 스킬이 실행된다.
➡️ 위험의 원인은 "비슷한 이름"이 아니라 "겹치는 용도 설명"이다. 그래서 대책도 전부 description 을 또렷하게 만드는 데 모인다.
예:
client-test-guide(고객용 테스트 가이드)와client-usage-guide(고객용 사용 가이드)는 이름이 거의 같다. 둘이 안 헷갈리는 건 이름 덕이 아니라, 각 description 이 "테스트 절차" vs "사용법"으로 용도를 갈라 놓았기 때문이다.
2. PR #29 — description 의 최소 품질을 hook 으로 강제한다 (floor)
"description 을 잘 쓰자"는 권고만으로는 언젠가 누군가 description: 리포트를 보냅니다. 같은 모호한 한 줄로 스킬을 저장한다. 그 순간부터 자연어 호출은 오발동하기 시작한다. standarda-template PR #29 는 이걸 hook 으로 강제했다.
.claude/hooks/check-skill-description.sh — PreToolUse 의 Write|Edit 가 .claude/skills/*/SKILL.md 를 저장할 때만 발동해서, frontmatter 의 name/description 을 검사한다:
# .claude/hooks/check-skill-description.sh (발췌)
# .claude/skills/*/SKILL.md 가 아니면 통과
case "$FILE" in
*.claude/skills/*/SKILL.md) ;;
*) exit 0 ;;
esac
...
DESC_VAL=$(printf '%s' "$DESC_LINE" | sed -E 's/^[Dd]escription:[[:space:]]*//')
if [ "${#DESC_VAL}" -lt 20 ]; then
fail "description 이 너무 짧습니다. 무엇을·언제 쓰는지 구체적으로 적으세요"
fi
fail() 은 stderr 로 안내를 찍고 exit 2 로 저장 자체를 거부한다 — L13 에서 본 그 exit 2 차단 패턴이다. 차단 조건은 세 가지다:
- frontmatter 에
name이 없다 (Write=파일 전체일 때만 검사) - frontmatter 에
description이 없다 description값이 20자 미만이다 (빈/한두 단어 수준)
.claude/settings.json 에는 이렇게 등록돼 있다:
// .claude/settings.json — Write|Edit matcher
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/check-skill-description.sh" }
]
}
한 가지 섬세한 점: Edit 인데 description 줄을 안 건드리면 검사를 건너뛴다. 스킬 본문만 고치는 수정까지 매번 막으면 마찰만 커지기 때문이다 — hook 은 new_string 에 ^description: 이 있을 때만 검사한다.
이건 L13 에서 말한 hook 의 보수성(오탐을 줄이려 검사 범위를 좁게)과 같은 태도다. "모호한 description 으로 스킬이 저장되는 것" 이라는 가장 흔한 사고 원인 하나만 결정론적으로 막는 floor 이지, description 이 완벽한지까지 보장하진 않는다.
3. description 작성 표준 — hook 을 통과해도 이걸로 쓴다
hook 은 20자 floor 만 본다. "20자 넘고 형제 스킬과 안 겹치는" 좋은 description 은 사람이 쓴다. 팀 표준(wiki development/skill-policy.md, 자습용)의 체크리스트는 다섯 가지다:
- 무엇을 하는지 — 한 문장으로 명확히.
- 언제 쓰는지(트리거) — "~할 때 사용", "Use when ~" 형태로 발동 상황을 구체적으로.
- 언제 쓰지 않는지(경계) — 헷갈릴 형제 스킬이 있으면 "이런 경우엔
/other-skill사용"처럼 상호 참조로 서로 밀어낸다. - 고유 키워드 — 그 스킬만의 명사/동사를 넣어 의미 공간을 분리한다.
- 도메인 접두어 — 이름에
email-…·sheet-…같은 접두어를 둬 의미가 겹치지 않게.
# 나쁨 — 무엇을·언제 쓰는지 불명확, 형제 스킬과 헷갈림
description: 리포트를 보냅니다.
# 좋음 — 트리거 + 경계 + 고유 키워드
description: 주간 매출 리포트를 Excel 로 만들어 메일로 발송합니다.
매출 집계 결과를 정기 발송할 때 사용. 단발성 요약 텍스트만 보낼 때는
/send-summary 를 사용하세요.
이 프로젝트의 실제 스킬 description 들도 이 꼴이다 — 예컨대 create-lesson 은 "…형제 스킬과의 경계 — Word(.docx)는 create-docx, 기능 문서는 feature-doc…" 처럼 경계를 description 안에 박아 이웃 스킬을 밀어낸다.
4. PR #30 — "슬래시 전용"으로 막지 말고, "실행 직전 확인"으로 보호한다
여기서 자연스러운 반문이 나온다. "자연어 호출이 그렇게 위험하면, /release 처럼 비가역·위험한 스킬은 자연어로 못 부르게(슬래시 전용) 막으면 되지 않나?"
PR #29 초안엔 실제로 "위험 스킬은 슬래시 전용으로 둔다"는 문구가 있었다. PR #30 은 그걸 제거했다. 새 정책은 이렇다:
모든 스킬은 슬래시·자연어 둘 다로 호출 가능하게 둔다. 자연어 호출을 막는 "슬래시 전용" 스킬은 만들지 않는다. 배포·삭제·외부 발송·릴리즈처럼 위험한 스킬도 호출 방식을 제한하지 않는다. 대신 실행 자체를 안전하게 만든다 — 스킬 본문에 실행 전 확인 단계("X 를 Y 에 배포합니다. 진행할까요?")를 둬서, 자연어로 잘못 불려도 사람이 한 번 거른다.
핵심 원칙 한 줄: 가드는 입구가 아니라 행동 직전에 둔다.
- 입구를 막는 방식(슬래시 전용)의 문제: 접근성을 통째로 희생한다. 게다가 "호출을 못 하게" 하는 건 위험의 진짜 지점(비가역 행동)이 아니라 그 한참 앞(호출 경로)을 막는 것이라 어긋나 있다.
- 행동 직전 확인의 이점: 자연어든 슬래시든, 어떤 경로로 불려왔든 위험한 행동 바로 앞에서 한 번 사람이 거른다. 접근성과 안전을 동시에 얻는다.
실제로 이 프로젝트의 release 스킬에는 L20 에서 본 migration 휴먼 게이트("목록 보여주고 y/N")가 본문에 박혀 있다. 자연어로 "배포해줘" 라고 불러도, 되돌리기 어려운 migrate 직전에 확인이 뜬다 — 이게 PR #30 이 말하는 "실행 직전 가드"의 실물이다.
5. 큰 그림: 강제력은 3층으로 나뉜다
PR #29·#30 을 한 장의 표로 겹쳐 보면, 스킬 거버넌스가 강제력이 다른 3개 층으로 되어 있다는 게 보인다:
| 층 | 메커니즘 | 성격 | 강제력 | 이번 변화 |
|---|---|---|---|---|
| 문서 | wiki skill-policy.md |
사람용 근거·거버넌스 | 없음 (런타임 영향 0) | 정책 원문 |
| 유도 | 스킬 description + CLAUDE.md 규칙 |
컨텍스트에 주입돼 모델이 참고 | 소프트 (거의 따르나 보장 X) | 작성 표준(§3), 실행-직전 확인(§4) |
| 강제 | hook (check-skill-description.sh) |
하네스가 실행, Claude 우회 불가 | 하드 (결정론적) | description floor(§2) |
이건 L13·L24 에서 반복해 본 "말(CLAUDE.md) → 유도(description) → 강제(hook)" 제도화 사다리와 같은 구조다. 규칙 하나를 어느 층에 둘지가 설계 결정이다:
- description 이 비었는지(객관·이분법) → 결정론적으로 판정 가능 → hook(강제).
- description 이 충분히 또렷한지, 위험 스킬을 언제 확인할지(맥락·판단) → 기계로 못 가름 → 유도(사람 review + 본문 확인 단계).
hook 을 만능으로 밀어붙이면 오탐으로 마찰만 커지고, 전부 권고로 두면 언젠가 뚫린다. 판정이 결정론적인 최소한만 강제하고, 나머지는 유도로. 이게 이번 두 PR 이 보여주는 판단이다.
오늘 정리 + 다음
- 정리: 스킬이 늘면 자연어 호출이 오발동하는데, 그 원인은 비슷한 이름이 아니라 겹치는 description 이다. 그래서 (PR #29) description 의 최소 품질을 hook 으로 강제(floor) 하고, 좋은 description 은 작성 표준(무엇을/언제/경계/고유키워드/도메인접두어)으로 사람이 쓴다. 위험한 스킬은 (PR #30) 호출을 막는 대신 실행 직전 확인으로 보호한다 — 가드는 입구가 아니라 행동 직전에.
- 흔한 함정: "위험하니까 자연어로 못 부르게 막자"(입구 차단)로 반응하기 쉽다. 하지만 그건 접근성을 희생하면서 정작 위험의 진짜 지점(비가역 행동)은 안 지킨다. 막을 곳은 호출 경로가 아니라 되돌릴 수 없는 행동 바로 앞이다.
- 다음 시간: 이 3층 사다리(문서→유도→강제)를 실제 변경 이력에 적용해 추적하는 법 — L27 standarda 변경 이력 추적.
- 자습 권장: 팀 wiki 의
development/skill-policy.md6번("충돌 회귀 테스트") — "자연어 문장 → 기대 스킬" 매핑 표를 만들어 두면 스킬 추가/이름 충돌을 조기에 잡는다. 이 프로젝트.claude/skills/를 열어 각 스킬의 description 이 §3 체크리스트를 지키는지 직접 대조해 보자.