레슨 03

스킬 시스템

Claude Code가 재사용 가능한 프롬프트 워크플로를 발견하고, 로드하고, 파싱하고, 실행하는 방식

1. 스킬이란 무엇인가?

스킬은 Claude Code가 발견하고 실행할 수 있는, 이름이 붙은 재사용 가능한 프롬프트 워크플로입니다. 스킬은 YAML 프런트매터 헤더가 있는 SKILL.md Markdown 파일로 저장되거나, bundled skills 형태로 CLI 바이너리에 직접 컴파일됩니다.

사용자 관점에서 스킬은 슬래시 명령입니다. /commit, /simplify, /review-pr 같은 형태죠. Claude 관점에서 스킬 호출은 Skill tool(Skill)을 부르는 일이며, 이 도구는 전체 프롬프트를 로드하고, 인자를 치환하고, 필요하면 인라인 셸 블록을 실행한 뒤, 결과를 대화에 주입합니다.

왜 일반 프롬프트 대신 스킬을 쓸까요? 스킬은 버전 관리가 가능하고, 공유할 수 있으며(리포지토리에 커밋 가능), 발견 가능하고(Claude가 스킬 메뉴를 보고 자동 호출), 조합 가능합니다(한 스킬이 다른 스킬을 호출 가능). 여기에 권한 힌트, 모델 오버라이드, 생명주기 훅도 담을 수 있습니다.
슬래시 명령 SKILL.md 파일 CLI에 번들됨 MCP로 제공됨

2. 스킬 생명주기

모든 스킬은 Claude Code가 시작되는 순간부터 Claude가 실제로 그것을 실행하는 순간까지 여섯 단계를 거칩니다.

flowchart LR D["발견
managed / user /
project / --add-dir 경로를 순회.
디렉터리 항목 읽기.
심볼릭 링크 해석."] L["로드
SKILL.md 읽기.
YAML 프런트매터 파싱.
토큰 예산 추정."] P["파싱
모든 프런트매터 필드를
Command 객체로 추출.
hooks, paths,
effort, shell 검증."] S["치환
$ARGUMENTS,
$1/$name, shell !backtick,
${CLAUDE_SKILL_DIR},
${CLAUDE_SESSION_ID} 치환."] E["실행
inline으로 실행(대화에 확장)
또는 fork 실행
(자기 토큰 예산을 가진
격리된 서브 에이전트)."] I["주입
도구 결과가 대화에 들어감.
허용 도구와 모델 오버라이드가
이번 턴에 적용됨."] D --> L --> P --> S --> E --> I style D fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style L fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style P fill:#22201d,stroke:#6e9468,color:#b8b0a4 style S fill:#22201d,stroke:#c47a50,color:#b8b0a4 style E fill:#22201d,stroke:#8e82ad,color:#b8b0a4 style I fill:#22201d,stroke:#6e9468,color:#b8b0a4

발견

시작 시점에 getSkillDirCommands()는 네 위치를 병렬로 순회하고, 레거시 .claude/commands/ 디렉터리도 함께 로드합니다. 심볼릭 링크는 realpath()로 해석되므로, 중복 파일은 내용이 같고 경로만 달라도 Claude에 도달하기 전에 제거됩니다.

로드

skill-name/SKILL.md 파일을 읽습니다. 토큰 수는 프런트매터만 기준으로 추정하며(name + description + whenToUse), 전체 본문은 시작 시 토큰화하지 않습니다. 덕분에 스킬이 수백 개여도 목록을 빠르게 보여줄 수 있습니다. 목록 자체도 컨텍스트 윈도우의 1%로 예산이 제한됩니다.

파싱

프런트매터는 Command 객체로 파싱됩니다. 여기서 description, allowed tools, argument hints, model override, hooks, paths, effort, shell 같은 모든 필드를 검증합니다. paths 필드가 있는 스킬은 조건부 스킬이 되며, 저장은 되지만 사용자가 일치하는 파일을 열기 전까지는 Claude에 노출되지 않습니다.

치환

스킬이 호출되면 본문에서 다음 순서로 인자 치환이 일어납니다.

  1. 이름 붙은 인자: $foo, $bar (arguments 프런트매터의 위치 기반 매핑)
  2. 인덱스 인자: $ARGUMENTS[0], $0, $1
  3. 전체 인자 문자열: $ARGUMENTS
  4. 플레이스홀더가 없고 인자가 있으면 ARGUMENTS: ... 형태로 뒤에 붙임
  5. 셸 주입: !`command` 또는 ```! 블록(로컬 스킬만 가능, MCP 스킬은 차단됨)
  6. 특수 변수: ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}

실행

스킬은 프런트매터의 context: fork 여부에 따라 두 방식 중 하나로 실행됩니다.

  • Inline (기본값): 확장된 프롬프트가 현재 대화에 사용자 메시지로 주입됩니다. Claude는 같은 컨텍스트 윈도우 안에서 이를 처리합니다.
  • Forked: 스킬은 자기 토큰 예산을 가진 격리된 서브 에이전트(runAgent())에서 실행됩니다. 상위 대화는 최종 텍스트 출력만 받습니다. 독립적인 작업에 적합합니다.

주입

Skill tool은 ToolResult를 반환합니다. inline 스킬에서는 결과에 allowedTools와 선택적 model 오버라이드가 담겨, 이번 턴의 이후 도구 호출에 적용됩니다. forked 스킬에서는 Done 표시가 나오고, 서브 에이전트의 출력이 다시 컨텍스트로 들어갑니다.

3. 네 가지 스킬 출처와 우선순위

스킬은 네 가지 서로 다른 출처에서 올 수 있습니다. 두 출처가 같은 이름의 스킬을 정의하면 먼저 로드된 쪽이 이깁니다 (managed > user > project > bundled). 중복 제거는 이름이 아니라 해석된 파일 경로 기준이므로, 심볼릭 링크된 스킬이 실제 파일을 가릴 수도 있습니다.

우선순위 1

Managed / Policy

managed/.claude/skills/

IT가 배포하는 엔터프라이즈 관리 스킬입니다. CLAUDE_CODE_DISABLE_POLICY_SKILLS로 사용자 오버라이드를 막도록 잠글 수 있습니다.

우선순위 2

User (개인)

~/.claude/skills/

프로젝트를 가로질러 쓰는 개인 스킬 라이브러리입니다. Claude Code를 실행하는 어디서나 사용할 수 있습니다. chokidar로 변경 사항을 실시간 감시합니다.

우선순위 3

프로젝트

.claude/skills/

리포지토리에 커밋되는, 저장소 범위의 스킬입니다. Claude는 프로젝트 루트에서 위쪽으로 순회하므로, 중첩된 .claude/skills/ 디렉터리도 파일을 열면서 동적으로 발견됩니다.

우선순위 4

Bundled

compiled into CLI

/simplify, /loop, /remember처럼 바이너리에 함께 들어가는 스킬입니다. 시작 시 registerBundledSkill()로 등록됩니다. 일부는 feature-flagged 상태입니다.

가장 높은 우선순위 Managed User Project Bundled 가장 낮은 우선순위

다섯 번째 출처, MCP skills

MCP 서버가 스킬을 노출하면 (loadedFrom === 'mcp'로 감지), 연결 시점에 가져와 명령 레지스트리에 추가합니다. MCP skills는 같은 프런트매터 스키마를 따르지만 셸 주입은 차단되며, 원격의 신뢰할 수 없는 콘텐츠에 대해서는 !backtick```! 블록을 절대 실행하지 않습니다.

레거시: .claude/commands/

예전의 /commands/ 디렉터리도 아직 지원됩니다 (loadedFrom: 'commands_DEPRECATED'). 단일 .md 파일과 skill-name/SKILL.md 디렉터리 형식을 모두 받습니다. 새 작업은 .claude/skills/를 써야 합니다.

라이브 리로딩

skillChangeDetector 모듈은 chokidar로 스킬 디렉터리를 감시합니다. 어떤 SKILL.md든 바뀌면 300 ms 디바운스를 거친 뒤 ConfigChange 훅을 실행하고, 모든 메모이제이션 캐시를 비워 다음 호출에서 새 스킬을 보게 합니다. Bun에서는 알려진 데드락 버그를 피하려고 FSWatcher 대신 stat-polling을 사용합니다.

4. SKILL.md 형식과 프런트매터

스킬 파일은 ---로 구분된 선택적 YAML 프런트매터 블록이 붙은 일반 Markdown입니다. 파일은 반드시 <skill-name>/SKILL.md 위치에 있어야 하며, 디렉터리 이름이 슬래시 명령 이름이 됩니다.

--- name: "My Workflow" # 표시 이름(디렉터리 이름보다 우선) description: "One-line summary" # /skills 메뉴와 예산 목록에 표시됨 when_to_use: "Use when X. Examples: ..." # Claude를 위한 자동 호출 힌트 allowed-tools: # 최소 권한(가산 방식) - Bash(gh:*) - Read - Write argument-hint: "[branch] [message]" # 자동완성 UI에 표시됨 arguments: branch message # 이름 붙은 인자 → $branch, $message context: fork # 'fork' = 격리된 서브 에이전트, inline이면 생략 model: claude-opus-4-5 # 이 스킬에만 적용되는 모델 오버라이드 effort: high # low | medium | high | integer version: "1.0.0" # 임의 버전 문자열(정보용) user-invocable: true # false → /skills 메뉴에서 숨김 paths: src/payments/** # 조건부, 일치하는 파일에서만 활성화 hooks: # 전후 생명주기 훅 PreToolUse: - matcher: ... agent: code # fork 실행에 쓰이는 에이전트 유형 shell: # !backtick 주입을 위한 셸 설정 interpreter: bash --- # My Workflow 본문 텍스트. 이름 붙은 인자 치환에는 $branch$message를 사용합니다. 또는 원본 인자 문자열에는 $ARGUMENTS를 사용합니다. 또는 인라인 셸: !`git log --oneline -5`

필드 참고

필드타입설명
namestring디렉터리에서 유도된 표시 이름을 덮어씁니다
descriptionstring스킬 목록에 보이는 한 줄 요약입니다. 없으면 본문의 첫 문단을 사용합니다.
when_to_usestringClaude를 위한 자세한 트리거 설명입니다. 목록에서는 description과 함께 표시됩니다.
allowed-toolslist이 스킬 실행 중 부여되는 도구 권한 패턴입니다. Bash 대신 Bash(gh:*)처럼 가능한 한 좁게 지정합니다.
argument-hintstringCLI 자동완성에서 플레이스홀더 힌트로 표시됩니다.
argumentsstring or list이름 붙은 인자 식별자입니다. 본문의 $name 치환에 위치 기반으로 매핑됩니다.
contextfork스킬을 격리된 서브 에이전트로 실행합니다. inline이 기본값이며, 그 경우 생략합니다.
modelstring이 스킬 실행에 사용할 모델 별칭입니다. inherit는 세션 기본값을 뜻합니다.
effortlow/medium/high/int이 스킬 실행 시 적용되는 사고 예산입니다.
versionstring정보용 버전 태그이며, 런타임 동작에는 영향이 없습니다.
user-invocablebool기본값은 true입니다. false로 두면 /skills 메뉴에서 숨겨집니다 (에이전트 전용 스킬).
pathsglob string(s)조건부 활성화입니다. 일치하는 파일이 열릴 때만 스킬이 나타납니다.
hooksobject도구 사용 전후의 생명주기 훅입니다. 로드 시 HooksSchema로 검증합니다.
agentstringfork 시 사용하는 에이전트 유형 식별자입니다. 예: code, browser.
shellobject스킬 본문 안에서 !backtick 실행에 쓰는 셸 인터프리터 설정입니다.
disable-model-invocationbooltrue이면 이 스킬은 Skill tool로 호출할 수 없고, 슬래시 명령으로만 호출할 수 있습니다.

5. 고급 스킬 패턴

조건부 스킬(paths 기반 활성화)

paths 프런트매터 필드가 있는 스킬은 조건부 스킬입니다. 시작할 때 로드되지만, 경로가 glob 패턴 중 하나와 맞는 파일을 사용자가 열거나 수정하기 전까지는 Claude에 노출되지 않습니다.

덕분에 결제 전용, 인프라 전용, iOS 전용 워크플로를 실제로 관련이 생기기 전까지는 숨겨 둘 수 있고, 관련 없는 작업에서 스킬 목록이 시끄러워지는 일을 막을 수 있습니다.

---
paths: src/payments/**
description: "Stripe refund workflow"
---

# Stripe Refund
Stripe API로 환불을 처리하는 단계...

패턴은 .gitignore 및 CLAUDE.md 조건부 규칙과 같은 문법을 씁니다. ** (전체 매치) 패턴은 무조건 활성로 취급되며, 이는 paths를 생략한 것과 같습니다.

조건부 스킬은 동적 스킬 발견과도 상호작용합니다. 모델이 프로젝트 트리의 더 깊은 파일을 읽어 갈수록 Claude Code는 cwd 방향으로 위쪽을 탐색하고, 시작 시에는 찾지 못했던 추가 .claude/skills/ 디렉터리를 발견할 수 있습니다. Gitignored 디렉터리는 건너뜁니다.

Bundled Skills(CLI에 컴파일됨)

Bundled skills는 Claude Code 바이너리 안에 함께 들어가며, 시작 시 registerBundledSkill()를 통해 등록됩니다. 파일 기반 스킬과 같은 BundledSkillDefinition 인터페이스를 따르지만, SKILL.md 파일 대신 getPromptForCommand(args, context) 함수를 제공합니다.

잘 알려진 bundled skills는 다음과 같습니다.

  • /simplify, 재사용성, 품질, 효율성을 보는 세 개의 병렬 리뷰 에이전트를 띄웁니다
  • /loop, interval과 프롬프트를 파싱해 cron job을 만듭니다(feature-flagged)
  • /remember, /verify, /debug, /stuck
  • /skillify, 현재 세션에 대해 질문하고 SKILL.md를 작성합니다(Anthropic 내부용)

Bundled skills는 files 속성으로 참조 파일을 함께 넣을 수도 있습니다. 이런 파일은 첫 호출 시 프로세스별 nonce 디렉터리로 추출되며, 심볼릭 링크 공격을 막기 위해 O_EXCL | O_NOFOLLOW | 0o600 플래그를 사용합니다. 그리고 Claude가 이를 Read/Grep할 수 있도록 프롬프트 앞에 Base directory for this skill: ... 접두사가 붙습니다.

일부 bundled skills는 feature flag (feature('AGENT_TRIGGERS'), feature('KAIROS'))나 런타임 검사 (isKairosCronEnabled())에 따라 조건부로 활성화됩니다. 디스크의 SKILL.md를 수정하지 않고도 추가하거나 제거할 수 있습니다.

// bundled skill 등록
registerBundledSkill({
  name: 'simplify',
  description: 'Review changed code for reuse, quality, and efficiency.',
  userInvocable: true,
  async getPromptForCommand(args) {
    return [{ type: 'text', text: SIMPLIFY_PROMPT }]
  },
})
MCP Skills(Model Context Protocol을 통해 제공됨)

MCP 서버는 도구뿐 아니라 스킬도 노출할 수 있습니다. Claude Code가 MCP_SKILLS 기능이 켜진 MCP 서버에 연결되면 fetchMcpSkillsForClient()를 호출하고, 여기서 파일 기반 스킬에 쓰는 것과 같은 parseSkillFrontmatterFields()createSkillCommand() 함수로 스킬 프런트매터를 파싱합니다.

MCP skills는 SkillsMenu 안의 별도 "MCP skills" 그룹에 나타납니다. 이름은 server-name:skill-name 규칙을 따릅니다.

로컬 스킬과의 핵심 차이는 다음과 같습니다.

  • 셸 주입 없음: !backtick```! 블록은 조용히 건너뜁니다. 코드 주석에는 이렇게 적혀 있습니다. "보안: MCP skills는 원격이며 신뢰할 수 없으므로, markdown 본문 안의 인라인 셸 명령은 절대 실행하지 않는다."
  • ${CLAUDE_SKILL_DIR}는 의미가 없음: MCP skills에는 로컬 디렉터리가 없어서 이 변수는 치환되지 않습니다.
  • 별도로 등록됨: MCP skills는 로컬 스킬 레지스트리가 아니라 AppState.mcp.commands에 저장됩니다. Skill tool은 호출 시점에 getAllCommands()로 이들을 합칩니다.
  • 레지스트리 브리지: mcpSkillBuilders.tscreateSkillCommandparseSkillFrontmatterFields 참조를 들고 있는 의존 그래프 말단 모듈이며, client.ts → mcpSkills.ts → loadSkillsDir.ts 사이의 순환 import 문제를 해결합니다.
// getAllCommands()가 MCP와 로컬 스킬을 합치는 방식
const mcpSkills = context.getAppState().mcp.commands
  .filter(cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp')
const localCommands = await getCommands(getProjectRoot())
return uniqBy([...localCommands, ...mcpSkills], 'name')
스킬 권한과 자동 허용 로직

모든 Skill tool 호출은 checkPermissions()를 거칩니다. 결정 흐름은 다음과 같습니다.

  1. Deny 규칙: deny 규칙이 스킬 이름과 맞거나 :* 접두 규칙과 맞으면 즉시 차단합니다.
  2. 원격 canonical skills: 자동 허용됩니다(ant 전용 실험 기능).
  3. Allow 규칙: 명시적인 allow 규칙이 맞으면 묻지 않고 진행합니다.
  4. 안전 속성 검사: 스킬이 "안전한" 속성만 사용하면(allowed-tools 없음, model override 없음, hooks 없음, paths 없음 등) 사용자에게 묻지 않고 자동 허용합니다.
  5. 질문: 그 외에는 사용자에게 정확 일치 또는 접두 (:*) allow 규칙을 로컬 설정에 추가하라는 제안을 보여주며 확인을 받습니다.

즉, 단순 정보성 스킬은 권한 대화상자 없이 실행되지만, Bash 접근 권한을 얻거나 모델을 오버라이드하는 스킬은 명시적 승인이 필요합니다.

인자 치환 자세히 보기

인자 파싱에는 shell-quote를 사용하므로 따옴표로 감싼 문자열이 하나의 토큰으로 처리됩니다.

/myskill "hello world" foo  →  ["hello world", "foo"]

지원되는 치환 패턴은 세 가지이며, 순서대로 처리됩니다.

패턴무엇으로 해석되는가
$foo, $bar위치 기준 이름 붙은 인자(arguments: foo bar 프런트매터 필요)
$ARGUMENTS[0], $0인덱스 위치 인자
$ARGUMENTS원본 전체 인자 문자열

스킬 본문에 플레이스홀더가 하나도 없는데 사용자가 인자를 넘기면, 인자는 ARGUMENTS: <args> 형태로 자동으로 뒤에 붙습니다. 덕분에 플레이스홀더를 선언하지 않은 단순 스킬에서도 인자가 조용히 버려지지 않습니다.

6. 핵심 정리

  • 📁 스킬은 <skill-name>/SKILL.md에 있습니다. 디렉터리 이름이 슬래시 명령이 됩니다. 모든 메타데이터는 YAML 프런트매터가 제어하고, Markdown 본문이 프롬프트입니다.
  • 🔍 Claude는 우선순위 순서대로 네 출처에서 스킬을 발견합니다. managed → user → project → bundled 순서입니다. MCP skills는 호출 시점에 별도로 합쳐집니다.
  • 스킬은 inline (기본값, 같은 대화 컨텍스트) 또는 forked (context: fork, 격리된 서브 에이전트)로 실행됩니다. fork는 독립적인 작업에, inline은 중간에 방향을 잡아야 하는 작업에 더 적합합니다.
  • 🔒 MCP skills는 셸 주입을 절대 실행하지 않습니다. !backtick 블록을 실행할 수 있는 것은 로컬(managed/user/project/bundled) 스킬뿐입니다. 이것은 소스 코드에 박혀 있는 강한 보안 경계입니다.
  • 🎯 paths 프런트매터는 스킬을 조건부로 만듭니다. 일치하는 파일이 열리기 전까지는 보이지 않습니다. 도메인 특화 워크플로를 관련 있을 때만 노출할 때 쓰면 좋습니다.
  • 💡 스킬 목록은 컨텍스트 윈도우의 1%로 예산 제한을 받습니다. bundled skill 설명은 절대 잘리지 않고, 사용자 정의 스킬 설명은 목록이 예산을 넘기면 짧아질 수 있습니다.
  • 🔄 스킬 파일은 실시간 감시됩니다. SKILL.md를 저장하면 디바운스된 리로드가 일어나며 (300 ms), 재시작은 필요 없습니다.
  • 단순한 스킬(allowed-tools 없음, model override 없음)은 권한 프롬프트 없이 자동 승인됩니다. 더 강한 권한의 스킬은 명시적인 사용자 allow 규칙이 필요합니다.
07 이해도 확인

퀴즈 — 6문제

Q1. ~/.claude/skills/deploy/SKILL.md에 있는 스킬 하나와, 현재 프로젝트의 .claude/skills/deploy/SKILL.md에 있는 스킬 하나가 있습니다. Claude는 어느 쪽을 사용할까요?
정답입니다. User 출처(우선순위 2)는 project 출처(우선순위 3)보다 앞섭니다. 먼저 로드된 스킬이 이기며, 중복 제거는 해석된 파일 경로 기준입니다.
Q2. 대화 기록을 공유하지 않는 격리된 컨텍스트에서 데이터베이스 마이그레이션 스크립트를 실행하는 스킬을 만들고 싶습니다. 어떤 프런트매터 필드를 설정해야 할까요?
정답입니다. context: fork는 스킬을 자기 토큰 예산을 가진 격리된 서브 에이전트로 실행합니다. 상위 대화는 최종 텍스트 출력만 받습니다.
Q3. MCP 서버가 !`rm -rf /tmp/cache`가 들어 있는 SKILL.md를 보냈습니다. 무슨 일이 일어날까요?
정답입니다. 소스 코드는 MCP skills에 대한 셸 주입을 명시적으로 차단합니다. "보안: MCP skills는 원격이며 신뢰할 수 없으므로, markdown 본문 안의 인라인 셸 명령은 절대 실행하지 않는다."
Q4. 스킬의 프런트매터에 paths: src/payments/**를 추가했습니다. 이 스킬은 언제 Claude에게 보이게 될까요?
정답입니다. paths 프런트매터가 있는 스킬은 로드 시점에 조건부 스킬로 저장됩니다. 세션 중 glob 패턴과 맞는 파일을 열거나 수정하면 활성화되어 보이게 됩니다.
Q5. SKILL.md 본문에 $ARGUMENTS 플레이스홀더가 없습니다. 사용자가 /myskill feature-123를 실행했습니다. 이 인자는 어떻게 될까요?
정답입니다. substituteArguments()는 어떤 플레이스홀더도 치환되지 않았음을 감지하고, 인자가 조용히 버려지지 않도록 ARGUMENTS: feature-123를 뒤에 붙입니다.
Q6. 사용자 정의 스킬이 200개 있습니다. 전체 목록이 컨텍스트 윈도우 예산의 1%를 넘었습니다. 어떤 스킬 설명이 절대 잘리지 않을까요?
정답입니다. prompt.ts 코드는 스킬을 bundled(절대 잘리지 않음)와 그 외로 명시적으로 나눕니다. bundled skills는 항상 전체 설명을 유지하고, 사용자 정의 스킬은 예산에 맞추기 위해 짧아질 수 있습니다.
0/6