Add complete Korean-language technical documentation analyzing Claude Code CLI internals, organized in a leveled guide structure (Level 1-3 + Appendix). Level 1 (입문): architecture overview, request lifecycle, key concepts Level 2 (시스템): QueryEngine, Tool system, Command system, Permission system, Agent coordinator, UI/Ink components Level 3 (심화): MCP/LSP integration, IDE bridge, Plugin/Skill system, Context compression, OAuth/Auth, Telemetry Appendix: Korean-English glossary, file map, design patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 KiB
플러그인 & 스킬 확장 시스템 분석
1. 개요
Claude Code의 확장 시스템은 두 개의 독립적이지만 상호 연관된 레이어로 구성된다: **플러그인 시스템(Plugin System)**과 스킬 시스템(Skill System). 두 시스템 모두 CLI 바이너리 외부에서 기능을 추가하거나 내장 기능을 사용자가 제어할 수 있도록 설계된 확장 메커니즘이다.
시스템 간 주요 차이점:
| 구분 | 플러그인 시스템 | 스킬 시스템 |
|---|---|---|
| 사용자 제어 | /plugin UI에서 활성화/비활성화 가능 |
항상 활성화(번들) 또는 디렉토리 기반 로딩 |
| 진입점 | builtinPlugins.ts 레지스트리 |
bundledSkills.ts 레지스트리 또는 loadSkillsDir.ts |
| 식별자 형식 | {name}@builtin (내장) / {name}@{marketplace} (마켓플레이스) |
스킬 이름 (예: commit, review-pr) |
| 구성 요소 | 스킬, 훅(hooks), MCP 서버를 복합 제공 가능 | 단일 스킬 단위 |
| 현재 상태 | 스캐폴딩(scaffolding) 단계 — 등록된 내장 플러그인 없음 | 10개 이상의 번들 스킬 제공 |
파일 구조 요약:
src/plugins/
builtinPlugins.ts — 내장 플러그인 레지스트리 및 쿼리 함수
bundled/
index.ts — initBuiltinPlugins() 진입점 (현재 빈 스캐폴딩)
src/skills/
bundledSkills.ts — 번들 스킬 레지스트리, 파일 추출 로직
loadSkillsDir.ts — 디스크 기반 스킬 로딩, 프런트매터 파싱
mcpSkillBuilders.ts — MCP 스킬 빌더 싱글턴 레지스트리
bundled/
index.ts — initBundledSkills() 진입점
remember.ts, simplify.ts, verify.ts, ... — 개별 스킬 구현체
src/tools/SkillTool/
SkillTool.ts — 스킬 실행 도구 본체
constants.ts — SKILL_TOOL_NAME 상수
prompt.ts — 스킬 목록 프롬프트 생성 및 예산 관리
UI.tsx — 렌더링 컴포넌트
2. 플러그인 시스템
2.1 플러그인 로딩 메커니즘
플러그인 시스템은 시작 시점에 initBuiltinPlugins()를 호출하여 초기화된다. 이 함수는 src/plugins/bundled/index.ts에 위치하며, 각 플러그인은 registerBuiltinPlugin()을 통해 중앙 Map<string, BuiltinPluginDefinition> 레지스트리에 등록된다.
// builtinPlugins.ts
const BUILTIN_PLUGINS: Map<string, BuiltinPluginDefinition> = new Map()
export function registerBuiltinPlugin(
definition: BuiltinPluginDefinition,
): void {
BUILTIN_PLUGINS.set(definition.name, definition)
}
플러그인 활성화 여부는 다음 우선순위로 결정된다:
- 사용자 설정 (
settings.enabledPlugins[pluginId]): 명시적으로true/false지정 시 최우선 적용 - 플러그인 기본값 (
definition.defaultEnabled): 사용자 설정이 없을 때 플러그인이 선언한 기본값 - 시스템 기본값 (
true): 위 두 값이 모두 없을 때 활성화 상태가 기본
플러그인 가용성 필터링은 isAvailable() 콜백으로 처리된다. 이 콜백이 false를 반환하면 해당 플러그인은 활성화/비활성화 목록 모두에서 제외된다:
for (const [name, definition] of BUILTIN_PLUGINS) {
if (definition.isAvailable && !definition.isAvailable()) {
continue // 조건 미충족 시 목록에서 완전히 제외
}
// ...
}
2.2 번들 플러그인 구조
src/plugins/bundled/index.ts는 현재 빈 스캐폴딩 상태다. 주석에 따르면 이 구조는 기존 번들 스킬 중 사용자 토글이 필요한 것들을 마이그레이션하기 위한 준비 단계다:
// bundled/index.ts
export function initBuiltinPlugins(): void {
// No built-in plugins registered yet — this is the scaffolding for
// migrating bundled skills that should be user-toggleable.
}
새 내장 플러그인 추가 절차는 주석에 명시되어 있다:
builtinPlugins.ts에서registerBuiltinPlugin임포트initBuiltinPlugins()내에서registerBuiltinPlugin()호출
2.3 플러그인 확장 포인트
BuiltinPluginDefinition 타입은 플러그인이 제공할 수 있는 세 가지 확장 포인트를 정의한다:
| 확장 포인트 | 타입 | 설명 |
|---|---|---|
skills |
BundledSkillDefinition[] |
스킬 목록 (플러그인이 비활성화되면 스킬도 비활성화) |
hooks |
HooksConfig |
전처리/후처리 훅 설정 |
mcpServers |
MCPServerConfig[] |
MCP 서버 자동 연결 설정 |
플러그인에서 제공된 스킬은 skillDefinitionToCommand() 함수를 통해 Command 객체로 변환된다. 이 변환 과정에서 source: 'bundled'로 설정되는 것이 중요한 설계 결정이다:
// builtinPlugins.ts - skillDefinitionToCommand()
return {
// ...
// 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded
// slash commands (/help, /clear). Using 'bundled' keeps these skills in
// the Skill tool's listing, analytics name logging, and prompt-truncation
// exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin.
source: 'bundled',
loadedFrom: 'bundled',
}
이 설계를 통해 플러그인 스킬은 스킬 목록에서 번들 스킬과 동일하게 취급되며, 프롬프트 예산 우선 배정과 분석 이름 로깅 대상에 포함된다.
2.4 LoadedPlugin 구조체
getBuiltinPlugins()가 반환하는 LoadedPlugin 객체는 다음 주요 필드를 포함한다:
const plugin: LoadedPlugin = {
name,
manifest: { name, description, version },
path: BUILTIN_MARKETPLACE_NAME, // 'builtin' — 파일시스템 경로 없음
source: pluginId, // '{name}@builtin'
repository: pluginId,
enabled: isEnabled,
isBuiltin: true,
hooksConfig: definition.hooks,
mcpServers: definition.mcpServers,
}
path: 'builtin'은 센티넬(sentinel) 값으로, 파일시스템 경로가 없는 내장 플러그인임을 나타낸다.
3. 스킬 시스템
3.1 스킬 로딩 메커니즘
스킬 로딩은 두 가지 경로로 진행된다:
경로 1 — 번들 스킬 (programmatic registration):
시작 시 initBundledSkills()가 호출되어 각 번들 스킬의 register*() 함수를 순차 호출한다. 각 register*() 함수는 registerBundledSkill(definition)을 호출하여 모듈 수준의 bundledSkills: Command[] 배열에 추가한다.
경로 2 — 디스크 기반 스킬 (file-system loading):
loadSkillsDir.ts의 loadSkillsFromSkillsDir()가 다음 경로들을 순회하여 스킬을 로드한다:
| 소스 | 경로 |
|---|---|
policySettings |
{managedFilePath}/.claude/skills/ |
userSettings |
{claudeConfigHomeDir}/skills/ |
projectSettings |
.claude/skills/ |
plugin |
플러그인 디렉토리 |
파일 기반 스킬은 {skill-name}/SKILL.md 형식의 디렉토리 구조를 따른다. 단일 .md 파일은 /skills/ 디렉토리에서 지원되지 않는다.
getSkillsPath() 함수는 소스별 경로 해석을 담당한다:
export function getSkillsPath(
source: SettingSource | 'plugin',
dir: 'skills' | 'commands',
): string {
switch (source) {
case 'policySettings': return join(getManagedFilePath(), '.claude', dir)
case 'userSettings': return join(getClaudeConfigHomeDir(), dir)
case 'projectSettings': return `.claude/${dir}`
case 'plugin': return 'plugin'
default: return ''
}
}
3.2 번들 스킬 목록
src/skills/bundled/index.ts의 initBundledSkills()는 다음 스킬들을 등록한다:
| 스킬 | 파일 | 특이사항 |
|---|---|---|
update-config |
updateConfig.ts |
설정 파일 업데이트 |
keybindings-help |
keybindings.ts |
키 바인딩 설명 |
verify |
verify.ts |
코드 검증 |
debug |
debug.ts |
디버깅 보조 |
lorem-ipsum |
loremIpsum.ts |
더미 텍스트 생성 |
skillify |
skillify.ts |
스킬 자동 생성 |
remember |
remember.ts |
메모리 레이어 검토 |
simplify |
simplify.ts |
코드 단순화 |
batch |
batch.ts |
배치 작업 |
stuck |
stuck.ts |
막힌 상황 탈출 보조 |
기능 플래그(feature flag)로 제어되는 조건부 스킬도 존재한다:
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
const { registerDreamSkill } = require('./dream.js')
registerDreamSkill()
}
if (feature('AGENT_TRIGGERS')) {
const { registerLoopSkill } = require('./loop.js')
registerLoopSkill()
}
if (feature('BUILDING_CLAUDE_APPS')) {
const { registerClaudeApiSkill } = require('./claudeApi.js')
registerClaudeApiSkill()
}
조건부 스킬에 require()를 사용하는 이유는 정적 임포트(static import)를 사용할 경우 트리 셰이킹(tree-shaking)을 통과하는 사이드 이펙트(side-effecting initializer)가 Bun 번들 바이너리에 포함되어 불필요한 모듈 초기화가 발생하기 때문이다.
3.3 번들 스킬의 파일 추출 메커니즘
BundledSkillDefinition의 files 필드를 통해 번들 스킬은 추가 참조 파일을 디스크에 추출할 수 있다. 이 메커니즘은 모델이 Read/Grep 도구로 스킬 파일을 접근할 수 있도록 허용한다:
export type BundledSkillDefinition = {
// ...
files?: Record<string, string> // 상대경로 → 파일 내용
getPromptForCommand: (args: string, context: ToolUseContext) => Promise<ContentBlockParam[]>
}
files가 존재할 경우 registerBundledSkill()은 getPromptForCommand를 래핑하여 지연(lazy) 추출을 수행한다:
// 프로세스당 한 번만 추출 — Promise 메모이제이션으로 경쟁 조건 방지
let extractionPromise: Promise<string | null> | undefined
getPromptForCommand = async (args, ctx) => {
extractionPromise ??= extractBundledSkillFiles(definition.name, files)
const extractedDir = await extractionPromise
const blocks = await inner(args, ctx)
if (extractedDir === null) return blocks
return prependBaseDir(blocks, extractedDir)
}
보안 설계: 파일 추출 시 경로 순회 공격(path traversal)을 방지하기 위해 엄격한 검증을 적용한다:
function resolveSkillFilePath(baseDir: string, relPath: string): string {
const normalized = normalize(relPath)
if (
isAbsolute(normalized) ||
normalized.split(pathSep).includes('..') ||
normalized.split('/').includes('..')
) {
throw new Error(`bundled skill file path escapes skill dir: ${relPath}`)
}
return join(baseDir, normalized)
}
파일 쓰기는 O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW 플래그와 0o600 퍼미션으로 수행된다. getBundledSkillsRoot()의 프로세스별 난수(nonce)가 심볼릭 링크 선점 공격에 대한 1차 방어선이며, O_NOFOLLOW | O_EXCL는 추가 방어층이다.
3.4 스킬 프런트매터 파싱
디스크 기반 스킬의 SKILL.md는 YAML 프런트매터를 통해 메타데이터를 정의한다. parseSkillFrontmatterFields()가 처리하는 주요 필드:
| 프런트매터 키 | 타입 | 설명 |
|---|---|---|
description |
string | 스킬 설명 |
when_to_use |
string | 모델에게 제공하는 사용 시점 힌트 |
allowed-tools |
string[] | 허용된 도구 목록 |
argument-hint |
string | 인수 힌트 |
arguments |
string | string[] | 인수 이름 목록 |
model |
string | 모델 오버라이드 (inherit이면 undefined) |
user-invocable |
boolean | 사용자 직접 호출 가능 여부 |
disable-model-invocation |
boolean | SkillTool 사용 비활성화 |
context |
'fork' |
실행 컨텍스트 (fork 시 서브에이전트 실행) |
agent |
string | 에이전트 타입 지정 |
effort |
string | number | 에포트(effort) 레벨 |
paths |
string[] | 적용 경로 패턴 |
hooks |
HooksSettings | 스킬별 훅 설정 |
paths 필드는 ignore 라이브러리를 활용하여 glob 패턴 매칭을 수행한다. /** 접미사는 자동 제거되고 ** 패턴만 있으면 undefined(전체 적용)로 처리된다.
3.5 MCP 기반 스킬 빌더
mcpSkillBuilders.ts는 모듈 간 순환 의존성(circular dependency) 문제를 해결하기 위한 쓰기 전용(write-once) 레지스트리 패턴을 구현한다.
문제 배경:
client.ts → mcpSkills.ts → loadSkillsDir.ts → ... → client.ts
이 의존성 그래프는 순환을 형성한다. loadSkillsDir.ts를 동적 임포트(await import(variable))로 처리하면 Bun 번들 바이너리에서 /$bunfs/root/… 경로로 해석되어 런타임 오류가 발생한다.
해결 방법 — 쓰기 전용 레지스트리:
// mcpSkillBuilders.ts — 의존성 그래프의 리프(leaf) 노드
export type MCPSkillBuilders = {
createSkillCommand: typeof createSkillCommand
parseSkillFrontmatterFields: typeof parseSkillFrontmatterFields
}
let builders: MCPSkillBuilders | null = null
export function registerMCPSkillBuilders(b: MCPSkillBuilders): void {
builders = b
}
export function getMCPSkillBuilders(): MCPSkillBuilders {
if (!builders) {
throw new Error(
'MCP skill builders not registered — loadSkillsDir.ts has not been evaluated yet',
)
}
return builders
}
loadSkillsDir.ts가 모듈 초기화 시점에 registerMCPSkillBuilders()를 호출하여 빌더 함수를 등록한다. commands.ts의 정적 임포트를 통해 시작 시점에 항상 평가되므로, MCP 서버가 연결되기 훨씬 전에 등록이 완료된다.
4. SkillTool 구현 분석
4.1 SkillTool 개요
SkillTool은 Claude Code의 스킬 실행 전용 도구(name: 'Skill')다. 모델은 이 도구를 통해 슬래시 커맨드(slash command) 스킬을 호출한다.
입력 스키마:
z.object({
skill: z.string().describe('The skill name. E.g., "commit", "review-pr", or "pdf"'),
args: z.string().optional().describe('Optional arguments for the skill'),
})
출력 스키마 (유니언 타입):
// 인라인 실행 결과
z.object({
success: z.boolean(),
commandName: z.string(),
allowedTools: z.array(z.string()).optional(),
model: z.string().optional(),
status: z.literal('inline').optional(),
})
// 포크 실행 결과
z.object({
success: z.boolean(),
commandName: z.string(),
status: z.literal('forked'),
agentId: z.string(),
result: z.string(),
})
4.2 커맨드 조회 및 MCP 스킬 통합
getAllCommands()는 로컬/번들 스킬과 MCP 스킬을 통합한다:
async function getAllCommands(context: ToolUseContext): Promise<Command[]> {
const mcpSkills = context
.getAppState()
.mcp.commands.filter(
cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
)
if (mcpSkills.length === 0) return getCommands(getProjectRoot())
const localCommands = await getCommands(getProjectRoot())
return uniqBy([...localCommands, ...mcpSkills], 'name')
}
MCP 스킬(loadedFrom === 'mcp')만 포함하고 일반 MCP 프롬프트는 제외한다. 이전에는 모델이 mcp__server__prompt 이름을 추측하여 일반 MCP 프롬프트를 SkillTool로 호출할 수 있었으나, 이 필터로 차단된다.
4.3 입력 검증 (validateInput)
검증은 다음 단계로 진행된다:
- 빈 스킬 이름 검사 —
errorCode: 1 - 슬래시 접두사 정규화 —
/commit→commit(이벤트 로깅 후 정규화) - 원격 스킬 처리 (
EXPERIMENTAL_SKILL_SEARCH) —_canonical_<slug>형식은 별도 경로 처리 - 커맨드 존재 확인 —
findCommand()실패 시errorCode: 2 disableModelInvocation확인 — 모델 호출 비활성화 스킬 거부 (errorCode: 4)- 프롬프트 타입 확인 —
type !== 'prompt'거부 (errorCode: 5)
4.4 실행 컨텍스트: 인라인 vs. 포크
스킬 실행은 command.context 필드에 따라 두 가지 모드로 분기된다:
인라인 실행 (기본값):
스킬 프롬프트를 현재 대화의 다음 응답으로 주입한다. 모델은 스킬 내용을 읽고 현재 컨텍스트에서 직접 응답한다. SkillTool이 { success: true, status: 'inline', allowedTools, model } 결과를 반환하면 호출 루프가 허용된 도구와 모델을 적용하여 다음 턴을 실행한다.
포크 실행 (context: 'fork'):
executeForkedSkill()이 독립적인 서브에이전트를 생성하여 스킬을 실행한다:
async function executeForkedSkill(
command, commandName, args, context, canUseTool, parentMessage, onProgress
): Promise<ToolResult<Output>> {
const agentId = createAgentId()
// prepareForkedCommandContext()로 서브에이전트 환경 구성
const { modifiedGetAppState, baseAgent, promptMessages, skillContent } =
await prepareForkedCommandContext(command, args || '', context)
// 스킬의 effort를 에이전트 정의에 병합
const agentDefinition = command.effort !== undefined
? { ...baseAgent, effort: command.effort }
: baseAgent
for await (const message of runAgent({ agentDefinition, promptMessages, ... })) {
agentMessages.push(message)
// 도구 사용 진행 상황을 onProgress로 보고
}
return { data: { success: true, status: 'forked', agentId, result: resultText } }
}
포크 실행의 특성:
- 독립적인 토큰 예산을 가진 격리된 에이전트 실행
- 완료 후 결과 텍스트만 상위 컨텍스트에 반환
- 종료 시
clearInvokedSkillsForAgent(agentId)로 메모리 해제
4.5 스킬 목록 프롬프트 및 예산 관리
prompt.ts의 formatCommandsWithinBudget()은 스킬 목록을 컨텍스트 창 예산 내에서 포맷한다:
예산 계산:
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01 // 컨텍스트 창의 1%
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000 // 200k 컨텍스트 × 4 × 1%
export function getCharBudget(contextWindowTokens?: number): number {
if (Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET)) {
return Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET)
}
if (contextWindowTokens) {
return Math.floor(contextWindowTokens * CHARS_PER_TOKEN * SKILL_BUDGET_CONTEXT_PERCENT)
}
return DEFAULT_CHAR_BUDGET
}
예산 초과 시 우선순위 처리:
번들 스킬(source === 'bundled')은 항상 전체 설명이 보존되며, 나머지 스킬에 잔여 예산이 배분된다. 극단적 예산 부족 시에는 비번들 스킬을 이름만 표시하는 축약 모드로 전환한다:
// 정상: - commit: Commit staged changes with a conventional message
// 예산 부족: - commit
항목당 최대 설명 길이:
export const MAX_LISTING_DESC_CHARS = 250
whenToUse와 description을 결합한 문자열이 250자를 초과하면 말줄임표(…)로 절단된다.
4.6 보안: MCP 스킬의 셸 실행 차단
createSkillCommand()의 getPromptForCommand 구현에서 MCP 스킬은 인라인 셸 실행(`!``...```)이 명시적으로 차단된다:
// Security: MCP skills are remote and untrusted — never execute inline
// shell commands (!`…` / ```! … ```) from their markdown body.
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, ...)
}
로컬 스킬(번들, 사용자, 프로젝트, 플러그인)만 셸 실행(executeShellCommandsInPrompt)을 허용한다.
5. 설계 결정
5.1 플러그인과 번들 스킬의 이원화
현재 아키텍처는 사용자 토글 가능 여부에 따라 플러그인과 번들 스킬을 명확히 분리한다. 이 결정은 src/plugins/bundled/index.ts의 주석에 명시되어 있다:
"Not all bundled features should be built-in plugins — use this for features that users should be able to explicitly enable/disable. For features with complex setup or automatic-enabling logic (e.g. claude-in-chrome), use src/skills/bundled/ instead."
claude-in-chrome처럼 환경 자동 감지(shouldAutoEnableClaudeInChrome())로 활성화되는 스킬은 플러그인 시스템이 아닌 번들 스킬 시스템에 남아있다.
5.2 Command.source 필드의 의미론적 일관성
source: 'bundled' vs source: 'builtin'의 구분은 미묘하지만 중요하다:
'builtin': 하드코딩된 슬래시 커맨드(/help,/clear등) —Command시스템 외부에 위치'bundled': 번들 스킬 —Command객체로 등록되어 스킬 목록, 분석, 프롬프트 예산 로직에 포함
플러그인 스킬이 source: 'bundled'를 사용하는 이유는 이 값이 세 가지 시스템 동작을 결정하기 때문이다: (1) 스킬 목록 표시, (2) 분석 이벤트 이름 로깅, (3) 프롬프트 예산에서 설명 보존 우선순위.
5.3 싱글턴 도구 실행 정책
SkillTool은 toAutoClassifierInput: ({ skill }) => skill ?? ''을 정의하여 자동 분류기(auto-classifier)에 스킬 이름만 전달한다. 주석에 따르면 한 번에 하나의 스킬만 실행되어야 한다:
"Only one skill/command should run at a time, since the tool expands the command into a full prompt that Claude must process before continuing."
이 제약은 스킬 확장 프롬프트가 다음 턴 처리 전에 완전히 평가되어야 한다는 구조적 요구에서 비롯된다.
5.4 지연 로딩과 메모이제이션 전략
번들 스킬의 파일 추출은 두 가지 레벨의 지연 로딩을 적용한다:
- 첫 호출 시 추출 (
??=연산자로 Promise 메모이제이션): 동일 프로세스에서 동시 호출이 와도 단일 쓰기 작업만 수행 - 실패 시 계속 실행 (
null반환): 파일 쓰기 실패가 스킬 실행을 중단하지 않음
스킬 목록 프롬프트는 memoize(async (_cwd: string) => ...) 패턴으로 캐시된다. _cwd 파라미터는 캐시 키로만 사용되며, 작업 디렉토리별로 별도 캐시가 유지된다.
5.5 로딩 출처(LoadedFrom) 추적
LoadedFrom 타입은 스킬의 출처를 세분화하여 추적한다:
export type LoadedFrom =
| 'commands_DEPRECATED' // 레거시 /commands/ 디렉토리
| 'skills' // .claude/skills/ 디렉토리
| 'plugin' // 플러그인 제공
| 'managed' // 정책 관리자 배포
| 'bundled' // CLI 내장
| 'mcp' // MCP 서버 제공
이 값은 보안 정책(셸 실행 허용 여부), 분석 이벤트 메타데이터, 스킬 중복 제거 로직에서 활용된다.
내비게이션
- 이전: level-3-internals 개요
- 상위: 목차