Claude Code를 내 손에 맞게 깎는 법: GNU Stow 기반 설정 동기화와 LSP 통합

(수정: 2026년 3월 21일) · 15분 읽기
목차

핵심은 3계층 CLAUDE.md + 커스텀 statusline + slash command 체계. GNU Stow로 2대 Mac 간 설정을 완전히 동기화한다.

Claude Code 개인 설정 구성 커버 이미지

Claude Code를 본격적으로 사용하면서 매번 같은 지시를 반복하는 것이 병목이 됐다. “한글로 답해”, “이 프로젝트는 pnpm이야”, “블로그 발행할 때는 이 순서대로” — 이런 반복을 설정과 skill로 옮기면서 설정 파일이 점점 늘어났고, 두 번째 Mac에서 동일한 환경을 재현하려면 체계가 필요했다. 최근에는 커스텀 skill이 15개로 늘어나고 플러그인 시스템이 추가되면서 한 단계 더 확장됐다. Claude Code를 자기 워크플로우에 맞게 커스터마이징하려는 개발자를 위해, 현재 구성된 전체 설정을 정리한 기록이다.

설정 원칙

Claude Code에 적용한 핵심 규칙들이다. 전역 CLAUDE.md에 명시하여 모든 프로젝트에서 일관되게 동작한다.

  • 한글 간결 응답: Output Style(korean-concise.md)로 모든 응답을 한국어로, 동료와 대화하듯 자연스러운 톤으로 출력한다. 과도한 설명이나 이모지는 금지했다.
  • 지시 전 타당성 검증: 가장 중요한 규칙이다. 사용자 지시를 바로 실행하지 말고, 기존 구조와의 일관성/영향 범위/부작용을 먼저 검토하도록 했다. 이 규칙이 없으면 Claude가 요청을 곧이곧대로 실행하다가 다른 곳이 깨지는 일이 반복된다.
  • Git 규칙: 한글 커밋 메시지, conventional commits 형식, push 전 사용자 확인 필수.
  • Context 최적화: 불필요한 설명을 제거하고, 실행 중심의 커뮤니케이션을 지향한다.

CLAUDE.md 3계층 구조

Claude Code는 프로젝트 디렉터리에서 상위로 탐색하면서 만나는 모든 CLAUDE.md를 병합한다. 이 구조를 활용해 3계층으로 설정을 분리했다.

계층경로적용 범위역할
전역~/.claude/CLAUDE.md모든 프로젝트핵심 원칙, Git 규칙, MCP 가이드
작업공간<workspace>/.claude/CLAUDE.md작업공간 하위 전체UI 규칙, 프로젝트 간 관계 정의
프로젝트<project>/CLAUDE.md해당 프로젝트만아키텍처, 개발 환경, fork custom

예를 들어 NanoClaw 프로젝트에서 Claude Code를 실행하면, 전역(원칙) → 작업공간(UI/인프라 규칙) → 프로젝트(NanoClaw 세부사항)가 모두 로드된다. 각 계층은 독립적으로 관리되므로, 프로젝트별 규칙을 추가해도 전역 원칙이 깨지지 않는다.

운영하면서 체감한 팁이 있다. CLAUDE.md는 500줄 이하를 유지하는 것이 좋다. 길어지면 Claude가 중요한 규칙을 놓치는 경우가 생긴다. 전문적인 지시(블로그 변환 규칙, 코드 리뷰 체크리스트 등)는 skill로 분리하여 호출 시에만 컨텍스트에 로드하는 것이 효율적이다.

Settings

~/.claude/settings.json에서 환경변수와 권한을 관리한다.

{
  "env": {
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
    "ENABLE_LSP_TOOL": "1"
  },
  "language": "한국어",
  "teammateMode": "tmux",
  "enabledPlugins": {
    "frontend-design@claude-plugins-official": true,
    "pyright-lsp@claude-plugins-official": true,
    "typescript-lsp@claude-plugins-official": true,
    "skill-creator@claude-plugins-official": true
  }
}

language를 설정하면 모든 응답이 한국어로 출력된다. Output Style과 별개로 시스템 레벨에서 언어를 고정하는 설정이다.

Teammate(AGENT_TEAMS)는 서브에이전트를 생성해 병렬 작업을 수행하는 기능이다. teammateMode: "tmux"를 함께 설정하면 각 서브에이전트가 별도의 tmux 패널에서 실행되어, 메인 에이전트와 동시에 작업 진행 상황을 볼 수 있다.

LSP는 Language Server Protocol 기반 코드 인텔리전스를 활성화한다. 이전에는 claude lsp install 명령으로 설치했지만, 현재는 enabledPlugins에서 공식 플러그인(pyright-lsp, typescript-lsp)을 활성화하는 방식으로 바뀌었다. skill-creator는 새로운 skill을 생성하고 성능을 측정하는 플러그인이다.

권한 설정(permissions.allow)에는 자주 쓰는 도구들을 등록해두었다. 매번 승인 프롬프트를 거치지 않아도 된다.

{
  "permissions": {
    "allow": [
      "Bash(gh api:*)",
      "Bash(curl:*)",
      "Bash(npm install:*)",
      "Bash(git commit:*)",
      "Bash(terraform plan:*)",
      "mcp__atlassian__searchJiraIssuesUsingJql",
      "mcp__atlassian__getJiraIssue"
    ]
  }
}

패턴에 와일드카드(*)를 사용하면 해당 명령의 모든 인자를 허용한다. MCP 도구는 전체 이름으로 등록한다.

주의: settings.json을 dotfiles로 관리할 때, API 키나 토큰 같은 민감 정보가 포함되어 있지 않은지 확인해야 한다. 민감한 값은 환경변수나 OS 키체인으로 분리하고, 필요하면 .gitignore로 특정 파일을 제외한다.

GNU Stow로 설정 동기화

여기까지 나온 설정 파일들 — CLAUDE.md, settings.json, statusline.sh, commands/, skills/ — 이 모든 것을 2대의 Mac에서 동일하게 유지해야 한다. 핵심 도구는 GNU Stow다.

Stow는 심볼릭 링크 팜(symlink farm) 관리자다. 지정한 디렉터리 구조를 타겟 디렉터리(기본값: 부모 디렉터리)에 심볼릭 링크로 매핑한다. dotfiles 관리에서 사실상 표준 도구로, ~/dotfiles/ 레포에 패키지별 디렉터리를 만들고 stow <패키지>를 실행하면 홈 디렉터리에 심볼릭이 생성된다.

brew install stow  # macOS

# ~/dotfiles/ 레포 구조
dotfiles/
├── claude/           # stow claude → ~/.claude/
   └── .claude/
       ├── CLAUDE.md
       ├── settings.json
       ├── statusline.sh
       ├── commands/
       ├── hooks/
       ├── output-styles/
       └── skills/
├── zsh/              # stow zsh → ~/.zshrc
   └── .zshrc
└── ...
cd ~/dotfiles
stow claude    # ~/dotfiles/claude/.claude/ → ~/.claude/ 심볼릭 생성
stow zsh       # ~/dotfiles/zsh/.zshrc → ~/.zshrc

새 PC 세팅은 세 단계면 끝난다.

git clone [email protected]:user/dotfiles.git ~/dotfiles
cd ~/dotfiles && stow claude
~/dotfiles/claude/setup-skills.sh  # npx skills 일괄 설치

운영 시 주의할 점은 절대 경로를 쓰면 안 된다는 것이다. 다른 Mac에서는 사용자명이 다를 수 있으므로, 경로는 반드시 상대 경로나 ~를 사용해야 한다. 절대 경로를 쓰면 stow 심볼릭이 깨진다. 설정 파일을 수정한 후에는 ~/dotfiles에서 commit/push를 잊지 말아야 한다. 그래야 다른 PC에서 pull로 동기화할 수 있다.

이후 나오는 모든 ~/.claude/ 경로의 파일들은 실제로는 ~/dotfiles/claude/.claude/의 심볼릭 링크다. 이 점을 염두에 두면 이후 설명이 자연스럽게 이어진다.

커스텀 Statusline

~/.claude/statusline.sh는 터미널 하단에 작업 상태를 실시간으로 표시하는 스크립트다. Claude Code가 제공하는 JSON 입력(context_window, model, cost 등)을 jq로 파싱하여 구성했다.

<project> on  main | orbstack/default | ▰▰▰▰▱▱▱▱▱▱ 21% (1M) | Opus | +2883 -1232 | 61h14m | $122.36

좌측에는 프로젝트 경로, Git 상태(p10k 스타일), 런타임 정보가 표시된다. 우측에는 컨텍스트 사용률, 모델명, 코드 변경량, 세션 시간, 비용이 나란히 나온다.

특히 신경 쓴 부분들이 있다.

컨텍스트 사용률은 15칸 progress bar에 5단계 색상을 적용했다. cyan(여유) → green(양호) → yellow(주의) → orange(경고) → red(위험) 순이다. Opus 4.6은 1M 컨텍스트라 (1M)으로 표시되고, Sonnet/Haiku는 (200K)로 표시된다. 200K 초과 경고([!200k])도 있는데, Opus 4.6이 1M 컨텍스트로 확장되면서 Opus에서는 무의미해졌다. 모델별로 분기하여 Opus일 때만 숨기도록 설정했다.

런타임 감지는 프로젝트 루트의 설정 파일을 기반으로 한다. package.json이 있으면 Node.js + 패키지 매니저(npm/pnpm/yarn/bun)를, pom.xml이면 Java + Maven/Gradle을, go.mod면 Go를 자동으로 표시한다. TypeScript, Python, Ruby, Elixir, Rust까지 지원한다.

세션 시간과 비용도 각각 5단계 색상이다. 비용이 $20을 넘으면 노란색으로 바뀌면서 자연스럽게 “오늘은 이 정도면 됐나” 하는 신호가 된다.

전체 코드를 공유한다. ~/.claude/statusline.sh에 저장하면 바로 사용할 수 있다.

#!/bin/bash

# Claude Code Status Line Script
# 구조: path git | k8s | CW bar | model | time | cost

input=$(cat)
cwd=$(echo "$input" | jq -r '.workspace.current_dir')

# 홈 디렉토리를 ~ 로 치환 + 깊은 경로 축약 (4단계 이상이면 .../last2)
display_path="${cwd/#$HOME/~}"
depth=$(echo "$display_path" | tr '/' '\n' | wc -l)
if [ "$depth" -gt 4 ]; then
    parent=$(dirname "$display_path")
    gparent=$(basename "$parent")
    leaf=$(basename "$display_path")
    root=$(echo "$display_path" | cut -d'/' -f1-2)
    display_path="${root}/.../${gparent}/${leaf}"
fi

# Python venv 확인
venv=""
[ -n "$VIRTUAL_ENV" ] && venv=" \033[35m($(basename "$VIRTUAL_ENV"))\033[0m"

# 런타임/언어 환경 감지
lang_info=""
if [ -f "${cwd}/package.json" ]; then
    node_ver=$(node -v 2>/dev/null)
    if [ -n "$node_ver" ]; then
        if [ -f "${cwd}/pnpm-lock.yaml" ]; then pm="pnpm"
        elif [ -f "${cwd}/bun.lockb" ] || [ -f "${cwd}/bun.lock" ]; then pm="bun"
        elif [ -f "${cwd}/yarn.lock" ]; then pm="yarn"
        else pm="npm"; fi
        lang_info=" \033[32m(${pm}:${node_ver})\033[0m"
    fi
elif [ -f "${cwd}/pom.xml" ] || [ -f "${cwd}/build.gradle" ] || [ -f "${cwd}/build.gradle.kts" ]; then
    java_ver=$(java -version 2>&1 | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
    if [ -n "$java_ver" ]; then
        if [ -f "${cwd}/build.gradle" ] || [ -f "${cwd}/build.gradle.kts" ]; then bld="gradle"
        else bld="maven"; fi
        lang_info=" \033[31m(${bld}:java-${java_ver})\033[0m"
    fi
elif [ -f "${cwd}/go.mod" ]; then
    go_ver=$(go version 2>/dev/null | sed -E 's/go version go([^ ]+).*/\1/')
    [ -n "$go_ver" ] && lang_info=" \033[36m(go:${go_ver})\033[0m"
elif [ -f "${cwd}/Cargo.toml" ]; then
    rust_ver=$(rustc --version 2>/dev/null | sed -E 's/rustc ([^ ]+).*/\1/')
    [ -n "$rust_ver" ] && lang_info=" \033[33m(rust:${rust_ver})\033[0m"
elif [ -f "${cwd}/requirements.txt" ] || [ -f "${cwd}/pyproject.toml" ] || [ -f "${cwd}/setup.py" ]; then
    py_ver=$(python3 --version 2>/dev/null | sed -E 's/Python //')
    [ -n "$py_ver" ] && lang_info=" \033[34m(python:${py_ver})\033[0m"
elif [ -f "${cwd}/Gemfile" ]; then
    ruby_ver=$(ruby -v 2>/dev/null | sed -E 's/ruby ([^ ]+).*/\1/')
    [ -n "$ruby_ver" ] && lang_info=" \033[31m(ruby:${ruby_ver})\033[0m"
elif [ -f "${cwd}/mix.exs" ]; then
    elixir_ver=$(elixir -v 2>/dev/null | grep Elixir | sed -E 's/Elixir ([^ ]+).*/\1/')
    [ -n "$elixir_ver" ] && lang_info=" \033[35m(elixir:${elixir_ver})\033[0m"
fi

# Git 상태 (p10k 스타일)
git_info=""
if git -C "$cwd" --no-optional-locks rev-parse --git-dir > /dev/null 2>&1; then
    branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null)
    [ -z "$branch" ] && branch=$(git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
    if [ -n "$branch" ]; then
        status=$(git -C "$cwd" --no-optional-locks status --porcelain=v1 2>/dev/null)
        staged=0; unstaged=0; untracked=0
        while IFS= read -r line; do
            [ -z "$line" ] && continue
            x="${line:0:1}"; y="${line:1:1}"
            [[ "$x" == "?" ]] && ((untracked++)) && continue
            [[ "$x" != " " && "$x" != "?" ]] && ((staged++))
            [[ "$y" != " " && "$y" != "?" ]] && ((unstaged++))
        done <<< "$status"
        stash_count=$(git -C "$cwd" --no-optional-locks stash list 2>/dev/null | wc -l | tr -d ' ')
        ab=""
        counts=$(git -C "$cwd" --no-optional-locks rev-list --left-right --count HEAD...@{upstream} 2>/dev/null)
        if [ $? -eq 0 ] && [ -n "$counts" ]; then
            ahead=$(echo "$counts" | cut -f1); behind=$(echo "$counts" | cut -f2)
            [ "$ahead" -gt 0 ] 2>/dev/null && ab="${ab}\033[32m⇡${ahead}\033[0m"
            [ "$behind" -gt 0 ] 2>/dev/null && ab="${ab}\033[31m⇣${behind}\033[0m"
        else
            ab="\033[90m[no upstream]\033[0m"
        fi
        markers=""
        [ -n "$ab" ] && markers="${markers} ${ab}"
        [ "$stash_count" -gt 0 ] && markers="${markers} \033[37m*${stash_count}\033[0m"
        [ "$staged" -gt 0 ] && markers="${markers} \033[32m+${staged}\033[0m"
        [ "$unstaged" -gt 0 ] && markers="${markers} \033[33m!${unstaged}\033[0m"
        [ "$untracked" -gt 0 ] && markers="${markers} \033[34m?${untracked}\033[0m"
        git_info=" \033[90mon\033[0m \033[33m\xee\x82\xa0 ${branch}\033[0m${markers}"
    fi
fi

# Kubernetes context + namespace
kube_info=""
if command -v kubectl &> /dev/null; then
    ctx=$(kubectl config current-context 2>/dev/null)
    if [ -n "$ctx" ] && [ "$ctx" != "docker-desktop" ]; then
        ns=$(kubectl config view --minify --output 'jsonpath={..namespace}' 2>/dev/null)
        [ -z "$ns" ] && ns="default"
        ctx_short="$ctx"
        [ ${#ctx} -gt 20 ] && ctx_short=$(echo "$ctx" | rev | cut -d'/' -f1 | cut -d'_' -f1 | rev)
        kube_info=" \033[36m${ctx_short}/${ns}\033[0m"
    fi
fi

# 현재 모델 ID
model_id=$(echo "$input" | jq -r '.model.id // empty')

# Context window 토큰 사용률 (5단계 색상 + progress bar)
token_info=""
token_color="\033[90m"
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
if [ -n "$used_pct" ]; then
    used_pct_int=$(printf "%.0f" "$used_pct")
    if (( used_pct_int < 30 )); then token_color="\033[36m"
    elif (( used_pct_int < 50 )); then token_color="\033[32m"
    elif (( used_pct_int < 70 )); then token_color="\033[33m"
    elif (( used_pct_int < 80 )); then token_color="\033[38;5;208m"
    else token_color="\033[31m"; fi
    bar_width=15
    filled=$((used_pct_int * bar_width / 100)); empty=$((bar_width - filled))
    bar=""
    for ((i=1; i<=filled; i++)); do bar="${bar}▰"; done
    for ((i=1; i<=empty; i++)); do bar="${bar}▱"; done
    case "$model_id" in *opus*) ctx_size="1M" ;; *) ctx_size="200K" ;; esac
    token_info=" ${token_color}${bar} ${used_pct_int}% (${ctx_size})\033[0m"
fi

# 모델명
model_info=""
if [ -n "$model_id" ]; then
    case "$model_id" in *opus*) model_short="Opus" ;; *sonnet*) model_short="Sonnet" ;; *haiku*) model_short="Haiku" ;; *) model_short="$model_id" ;; esac
    model_info=" \033[96m${model_short}\033[0m"
fi

# 세션 경과 시간 (5단계 색상)
session_time=""
duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
if [ "$duration_ms" -gt 0 ] 2>/dev/null; then
    elapsed=$((duration_ms / 1000))
    if [ $elapsed -ge 3600 ]; then time_str="$((elapsed/3600))h$(((elapsed%3600)/60))m"
    elif [ $elapsed -ge 60 ]; then time_str="$((elapsed/60))m"
    else time_str="${elapsed}s"; fi
    if [ $elapsed -lt 1800 ]; then time_color="\033[36m"
    elif [ $elapsed -lt 3600 ]; then time_color="\033[32m"
    elif [ $elapsed -lt 7200 ]; then time_color="\033[33m"
    elif [ $elapsed -lt 10800 ]; then time_color="\033[38;5;208m"
    else time_color="\033[31m"; fi
    session_time=" ${time_color}${time_str}\033[0m"
fi

# 비용 (5단계 색상)
cost_info=""
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
if (( $(echo "$cost_usd > 0" | bc -l 2>/dev/null) )); then
    cost_display=$(printf "%.2f" "$cost_usd")
    if (( $(echo "$cost_usd < 5" | bc -l) )); then cost_color="\033[36m"
    elif (( $(echo "$cost_usd < 10" | bc -l) )); then cost_color="\033[32m"
    elif (( $(echo "$cost_usd < 20" | bc -l) )); then cost_color="\033[33m"
    elif (( $(echo "$cost_usd < 30" | bc -l) )); then cost_color="\033[38;5;208m"
    else cost_color="\033[31m"; fi
    cost_info=" ${cost_color}\$${cost_display}\033[0m"
fi

# Agent (teammate)
agent_info=""
agent_name=$(echo "$input" | jq -r '.agent.name // empty')
[ -n "$agent_name" ] && agent_info=" \033[95m@${agent_name}\033[0m"

# 코드 변경량
lines_info=""
lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
if [ "$lines_added" -gt 0 ] || [ "$lines_removed" -gt 0 ]; then
    lines_info=" \033[32m+${lines_added}\033[0m \033[31m-${lines_removed}\033[0m"
fi

# 200k 초과 경고 (Opus 1M에서는 숨김)
exceed_warn=""
exceeds_200k=$(echo "$input" | jq -r '.exceeds_200k_tokens // false')
if [ "$exceeds_200k" = "true" ]; then
    case "$model_id" in *opus*) ;; *) exceed_warn=" \033[31;1m[!200k]\033[0m" ;; esac
fi

# 최종 출력
sep="\033[90m |\033[0m"
output="\033[34m${display_path}\033[0m${git_info}${venv}${lang_info}"
right=""
[ -n "$kube_info" ] && right="${right}${sep}${kube_info}"
[ -n "$token_info" ] && right="${right}${sep}${token_info}${exceed_warn}"
[ -n "$model_info" ] && right="${right}${sep}${model_info}"
[ -n "$agent_info" ] && right="${right}${agent_info}"
[ -n "$lines_info" ] && right="${right}${sep}${lines_info}"
[ -n "$session_time" ] && right="${right}${sep}${session_time}"
[ -n "$cost_info" ] && right="${right}${sep}${cost_info}"
printf "%b%b" "$output" "$right"

Hooks + Worktree: WIP 자동 커밋과 병렬 작업

코드팩토리의 kimoring-ai-skills를 참고하여, PR 기반 워크플로우에 맞게 수정했다. 원본은 main에 직접 squash-merge하는 방식이지만, feature 브랜치 내 squash 후 PR 생성으로 변경하고, 세션 내 명시적 WIP 커밋(/wip)과 main 브랜치 차단을 추가했다.

Claude Code의 세션이란 claude 명령으로 CLI를 시작해서 /exit이나 Ctrl+C로 종료할 때까지의 구간이다. hooks는 이 세션의 시작과 종료에 셸 스크립트를 연결하는 시스템이다. 세션 시작 시 load-recent-changes.sh가 최근 커밋 10개와 CHANGELOG를 맥락으로 주입하고, 세션 종료 시 commit-session.shclaude -p 헤드리스 모드로 diff를 분석해 WIP: 한글 요약 형식의 커밋을 자동 생성한다.

이 hook의 진짜 가치는 Git worktree와 결합할 때 나타난다. Claude Code의 -w 옵션으로 독립된 작업 공간을 만들고, 세션을 열고 닫을 때마다 WIP 커밋이 쌓이며, 작업 완료 시 squash하여 깔끔한 PR을 만드는 구조다.

실제 작업 흐름

인증 모듈을 개발하는 시나리오로 전체 흐름을 따라가 본다.

세션 1: worktree 생성 + JWT 검증 로직 작업

claude -w feature-auth
# .claude/worktrees/feature-auth/ 디렉토리 생성
# worktree-feature-auth 브랜치 자동 생성, SessionStart hook이 최근 맥락 주입

Claude와 대화하며 JWT 토큰 검증 미들웨어를 구현한다. 오늘은 여기까지. /exit으로 세션을 종료하면 Stop hook이 WIP: JWT 토큰 검증 미들웨어 구현 같은 커밋을 자동 생성한다.

세션 2: 이어서 refreshToken 갱신 로직 추가

claude -w feature-auth --continue
# 같은 worktree에 재진입, SessionStart hook이 이전 WIP 히스토리를 맥락으로 주입

refreshToken 갱신 로직을 추가한다. /exit. Stop hook이 WIP: accessToken 만료 시 refreshToken 자동 갱신 추가를 커밋한다.

세션 3: 마무리 + squash + PR

claude -w feature-auth --continue
# 테스트 코드 작성, 에러 핸들링 보강 등 마무리 작업
# /merge-worktree 실행

/merge-worktree가 feature-auth 브랜치의 WIP 커밋 3개를 분석하고, git reset --soft로 하나의 커밋으로 squash한 뒤, feat: 사용자 인증 모듈 JWT 검증 및 토큰 갱신 구현 같은 최종 메시지를 작성하여 push하고 PR 생성을 제안한다. 같은 이름으로 -w를 다시 호출하면 기존 worktree에 재진입하므로, 매번 디렉토리를 이동할 필요가 없다.

세션을 닫지 않고도 /wip 커맨드로 명시적 체크포인트를 찍을 수 있다. 현재 세션의 Claude가 대화 맥락을 알고 있으므로 헤드리스 호출보다 더 정확한 커밋 메시지를 만든다. Stop hook은 세션 종료 시 안전망이고, /wip은 의도적 중간 저장이다. 단, main/master에서는 /wip이 차단된다.

WIP 커밋은 단순한 중간 저장이 아니다. 다음 세션에서 맥락을 이어받고, 여러 worktree 간 충돌 시 AI가 우선순위를 판단하며, squash 시 변경 의도를 빠짐없이 반영한 커밋 메시지를 만드는 데 쓰인다. 메인 브랜치는 깨끗하게 유지하면서, 뇌 용량만큼 병렬 작업을 스케일링할 수 있는 구조다.

Slash Commands

자주 반복하는 작업은 slash command로 만들어 /명령어 한 줄로 실행한다.

전역 커맨드 (~/.claude/commands/):

커맨드용도
/release버전 bump, tagging, GitHub Release 자동화
/copilot-reviewGitHub Copilot PR 리뷰를 분석하고 반영

/copilot-review는 PR 번호를 인자로 받아(없으면 현재 브랜치에서 자동 감지) Copilot이 남긴 코드 리뷰 코멘트를 분석하고, 사용자 확인 후 수정 사항을 반영하는 워크플로우다. Copilot Code Review와 Claude Code를 연결하는 브릿지 역할이다.

프로젝트 커맨드 (<project>/.claude/commands/):

프로젝트 디렉터리에 두면 해당 프로젝트에서만 노출된다. 예를 들어 /blog-stats는 NanoClaw 프로젝트에서만 사용하는 블로그 Analytics Engine 데이터 조회 커맨드다.

command 파일은 마크다운으로 작성된 프롬프트 템플릿이다. Claude가 이를 읽고 지시에 따라 실행한다. 복잡한 워크플로우도 한 파일에 정의할 수 있어서, skill과 함께 Claude Code 활용의 핵심 도구가 된다.

Skills 생태계

Skills는 Claude Code의 확장 기능이다. 호출 시에만 컨텍스트에 로드되어 토큰을 절약하면서도, 필요할 때 전문적인 지시를 제공한다. 현재 32개의 skill이 등록되어 있으며, npx 패키지와 커스텀 skill 두 가지 출처로 나뉜다.

npx 패키지로 설치하는 전역 skills

커뮤니티에서 관리하는 skill 패키지를 npx skills add로 설치한다. LangChain이나 Vercel 같은 프레임워크는 API가 자주 바뀌므로, 패키지 관리자가 skill을 지속적으로 업데이트하는 구조가 유리하다.

# LangChain/LangGraph/Deep Agents
npx skills add langchain-ai/langchain-skills --skill "*" --yes --global

# Vercel/React/Next.js
npx skills add vercel-labs/agent-skills --skill "*" --yes --global

# NestJS
npx skills add kadajett/agent-nestjs-skills --skill "*" --yes --global

npx로 설치된 skill은 17개다. LangChain/LangGraph 에이전트 구성(8개), Vercel/React 최적화(4개), Deep Agents(3개), NestJS(1개), Web Design(1개)으로 구성된다.

커스텀 Skills

npx 패키지로 해결되지 않는 개인 워크플로우는 직접 skill을 만들었다. ~/.claude/skills/ 디렉토리에 SKILL.md 파일을 두면 Claude가 자동으로 인식한다. 현재 15개의 커스텀 skill이 운영 중이다.

콘텐츠 파이프라인:

Skill용도
/vault-to-blogObsidian 노트를 멀티 모델 리뷰(Gemini + Claude 4명 Critic) 후 블로그 발행
/vault-noteObsidian vault에 기술 노트 작성
/gemini-browserPlaywright로 Gemini 웹에 접속하여 cover image 생성 → WebP 변환
/git-commitgit diff 분석 → 설명적 커밋 메시지 생성

실제 사용 시나리오로 핵심 skill 두 개를 설명한다.

/vault-to-blog는 가장 복잡한 skill이다. Obsidian vault에 메모 형태로 적어둔 기술 노트를 블로그 아티클로 변환하는 전체 파이프라인을 자동화한다. 변환 후에는 Gemini 2명 + Claude 2명, 총 4명의 Critic이 병렬로 초안을 평가한다. 각자 독자 관점, 기술 정확성, 구조/서사, 실용성을 10점 만점으로 채점하고, 별도의 Synthesis 중재자가 충돌하는 피드백(예: “축약하라” vs “더 추가하라”)을 정리하여 통합 수정 지시를 만든다. SKILL.md에서 Critic 구성의 핵심 구조를 발췌하면 이렇다.

# Critic 구성 (SKILL.md 발췌)
Critic A-1: Gemini — 독자 관점 (가독성, 흐름, 독자 피로도)
Critic A-2: Gemini — 기술 정확성 (사실 오류, 최신성, 누락된 trade-off)
Critic B-1: Claude — 구조/서사 (블로그 톤, 변환 규칙 준수)
Critic B-2: Claude — 실용성 (코드 검증, 따라하기 가능성)
수렴 조건: 평균 8.0 이상 AND 최저 6.0 이상 (Veto), 최대 2라운드

이 글 자체도 /vault-to-blog의 멀티 모델 리뷰를 거쳐 발행됐다. 실제로 Critic A-1이 “statusline 코드가 너무 길다”고 지적한 반면 B-2는 “hooks 코드가 빠졌다”고 요청해서 충돌이 발생했는데, Synthesis가 “이번 업데이트 범위는 Skills/Plugins이므로 둘 다 scope-out”으로 중재한 것이 자연스러운 결론이었다.

/gemini-browser는 MCP의 Gemini Image와 다른 접근이다. MCP가 API를 호출하는 반면, 이 skill은 Playwright로 실제 Gemini 웹 인터페이스를 자동화한다. Google 계정 로그인 세션을 로컬에 저장해두고, 프롬프트를 전달하면 생성된 이미지를 다운로드하여 WebP로 변환한다. API 할당량 제한 없이 사용할 수 있고, /vault-to-blog의 cover image 생성 단계에서 자동으로 호출된다.

개발 워크플로우:

Skill용도
/wip현재 변경사항으로 WIP 커밋 생성 (main 브랜치 차단)
/merge-worktreeworktree의 WIP 커밋을 squash → PR 생성
/code-reviewer코드 리뷰 베스트 프랙티스 체크
/webapp-testingPlaywright로 로컬 웹 앱 테스트 + 스크린샷
/mcp-builderMCP 서버 빌드 가이드 (Python FastMCP / TypeScript)

문서 처리:

Skill용도
/pdfPDF 텍스트 추출, 생성, 병합, 폼 처리
/xlsx스프레드시트 수식, 서식, 분석
/docxWord 문서 생성, 편집, 변경 추적
/pptx프레젠테이션 생성, 레이아웃, 노트
/internal-comms사내 커뮤니케이션 (상태 보고서, 업데이트 등) 작성

문서 처리 skill이 5개나 되는 이유는 엔터프라이즈 환경에서의 필요성 때문이다. 기획서, 주간 보고, 프레젠테이션을 CLI에서 바로 생성하거나 수정할 수 있다. 특히 /internal-comms는 회사 내부 형식에 맞춘 커뮤니케이션 작성을 도와준다.

Skills 동기화 문제

npx skill과 커스텀 skill의 동기화 방식이 다르다. npx는 ~/.agents/skills/에 실제 파일을 설치하고 ~/.claude/skills/에 심볼릭을 생성한다. Stow로 동기화되는 건 심볼릭뿐이고, 실제 skill 파일은 Git 관리 대상이 아니라 두 번째 PC에서는 심볼릭이 깨진다.

커스텀 skill은 ~/dotfiles/claude/.claude/skills/에 직접 저장되므로 Stow로 자연스럽게 동기화된다. npx skill만 별도 처리가 필요하다. setup-skills.sh 스크립트가 등록된 패키지 목록을 순회하며 npx skills add를 일괄 실행한다. 새 PC 세팅의 마지막 단계(stow claude 후)에서 이 스크립트를 돌리면 된다.

More: Skill이 많아지면 생기는 문제

32개 skill의 트레이드오프도 있다. 각 skill의 description이 시스템 프롬프트의 도구 선택 단계에 상주하므로, skill을 호출하지 않더라도 기본 토큰 소비가 증가한다. 체감상 skill이 적을 때보다 첫 응답이 느려진 경우가 있었다. 또한 skill이 많아질수록 트리거 충돌(의도한 skill 대신 다른 skill이 활성화)이 생긴다.

대응 방안은 두 가지다. 첫째, skill의 description을 정밀하게 작성하여 트리거 조건을 명확히 한다. skill-creator 플러그인이 이 작업을 도와준다. 둘째, 범용적이지 않은 skill은 전역이 아닌 프로젝트 레벨에 두어 해당 프로젝트에서만 로드되도록 한다.

GNU Stow 심볼릭 링크 관련 주의점도 하나 있다. Claude Code가 업데이트될 때 ~/.claude/ 내부의 캐시나 세션 파일이 심볼릭 링크를 무시하고 실제 파일로 덮어쓰는 경우가 드물게 발생한다. stow --restow claude로 심볼릭을 재생성하면 해결된다.

Memory 시스템

Claude Code의 memory는 세션 간 컨텍스트를 유지하는 파일 기반 기억 시스템이다. 프로젝트별로 .claude/projects/*/memory/ 디렉터리에 저장된다.

유형용도
user사용자 역할, 선호도, 지식 수준
feedback사용자 피드백과 교정 사항 (반복 실수 방지)
project진행 중인 작업, 목표, 마감일
reference외부 시스템 위치 정보

MEMORY.md가 인덱스 역할을 하고, 개별 .md 파일에 실제 내용이 저장된다. 중요한 원칙은 DB에서 조회 가능한 정보는 메모리에 중복 저장하지 않는다는 것이다. 예를 들어 스케줄 태스크 목록은 SQLite에 있으니 메모리에 따로 기록하지 않는다.

feedback 유형이 특히 유용하다. “지시를 바로 실행하지 말고 타당성을 먼저 검증하라”는 피드백을 저장해두면, 이후 세션에서도 Claude가 같은 실수를 반복하지 않는다.

MCP 서버

현재 연결된 MCP 서버는 두 가지다.

서버용도
Context7라이브러리/프레임워크 공식 문서 조회
Gemini Image이미지 생성 (블로그 cover image 등)

Context7은 WebSearch보다 우선 사용하도록 규칙을 정했다. import 구문이나 프레임워크 API 질문이 나오면 버전별 curated 문서를 바로 가져온다. Gemini Image는 Claude Code 자체에 이미지 생성 기능이 없기 때문에, MCP를 통해 Gemini의 이미지 생성 API를 연결한 것이다. 블로그 cover image나 다이어그램을 대화 중에 바로 생성할 수 있다.

MCP 서버 추가는 claude mcp add 명령어로 한다. scope에 따라 적용 범위가 달라진다.

# 전역 (모든 프로젝트에서 사용)
claude mcp add --scope user context7 -- npx -y @context7/mcp

# 프로젝트 (해당 프로젝트에서만 사용)
claude mcp add --scope project gemini-image -- npx -y gemini-image-mcp

--scope user~/.claude/에, --scope project<project>/.claude/에 설정이 저장된다. 미사용 MCP 서버는 연결만으로도 수천 토큰을 소비하므로, 쓰지 않는 서버는 비활성화하는 것이 중요하다.

Plugins

Plugins는 Claude Code의 공식 확장 시스템이다. MCP가 외부 도구를 연결하고, Skills가 프롬프트 기반 지시를 제공한다면, Plugins는 Claude Code 자체의 기능을 확장한다.

{
  "enabledPlugins": {
    "frontend-design@claude-plugins-official": true,
    "pyright-lsp@claude-plugins-official": true,
    "typescript-lsp@claude-plugins-official": true,
    "skill-creator@claude-plugins-official": true
  }
}
플러그인출처역할
pyright-lsp공식Python 타입 분석, go-to-definition, find-references
typescript-lsp공식TypeScript/JavaScript 언어 서버
frontend-design공식프론트엔드 UI 코드 생성 품질 향상
skill-creator공식새로운 skill 생성, eval 실행, 성능 벤치마크

LSP 플러그인은 이전에 claude lsp install 명령으로 별도 설치했지만, 현재는 enabledPlugins에서 관리하는 방식으로 통합됐다. settings.json에 한 줄 추가하면 활성화되고, dotfiles로 동기화되므로 두 번째 PC에서도 동일한 플러그인이 자동으로 켜진다.

skill-creator는 새로운 skill을 만들 때 유용하다. skill의 description이 트리거 정확도에 큰 영향을 미치는데, 이 플러그인이 description 최적화와 eval 실행을 도와준다. 커스텀 skill 15개를 운영하면서 체감한 것은, skill이 많아질수록 트리거 충돌(의도한 skill 대신 다른 skill이 활성화되는 문제)이 늘어난다는 점이다. skill-creator로 description을 정밀하게 다듬으면 이 문제를 줄일 수 있다.

커뮤니티 플러그인도 지원한다. extraKnownMarketplaces에 GitHub 레포를 등록하면 해당 레포의 플러그인을 설치할 수 있다.

LSP 통합

ENABLE_LSP_TOOL=1을 설정하면 Claude Code가 Language Server Protocol을 통해 코드를 분석한다.

기본 Claude Code는 코드 탐색 시 grepglob으로 파일을 검색하고 읽는다. 패턴 매칭이라 후보 파일을 여러 개 열어봐야 하고, 타입 정보는 알 수 없다. LSP를 활성화하면 go-to-definition, find-references, hover 등 언어 서버의 기능을 직접 사용한다.

기능grep 기반 (기본)LSP 기반
심볼 탐색정규식 매칭 → 후보 다수 읽기AST 기반 정확한 위치로 직접 이동
타입 추론불가interface, 제네릭 등 타입 정보 활용
리네이밍문자열 치환 (오검색 위험)타입 기반 안전한 리팩토링
토큰 소비높음 (수십 파일 읽기)낮음 (필요한 파일만)

아키텍처적으로 보면 MCP와 같은 맥락이다. 직접 파일이나 API를 다루는 대신, 중간 프로토콜을 통해 구조화된 정보를 얻는 패턴이다. 차이점은 LSP가 IDE 생태계(2016년, Microsoft)에서 이미 검증된 프로토콜이라는 것이다. Claude Code가 새로 만든 것이 아니라, 수십 개 언어 서버가 이미 존재하는 생태계를 그대로 차용했다.

병렬 에이전트(Teammate)와 함께 사용하면 효과가 배가된다. 여러 서브에이전트가 동일 언어 서버의 타입 정보를 공유하여 중복 탐색을 최소화할 수 있다(공식 벤치마크는 없지만, 구조적으로 타당하다).

공식 LSP 플러그인은 pyright-lsp(Python), typescript-lsp(TS/JS), jdtls-lsp(Java, JDK 17+ 필요), rust-lsp(Rust)가 있다. 커뮤니티에서는 Go, Kotlin, C#, Swift 등도 지원한다.

현재는 settings.json의 enabledPlugins에서 관리한다. 이전의 claude lsp install 방식도 여전히 작동하지만, 플러그인 시스템으로 통합되면서 설정 동기화가 더 간편해졌다.

{
  "enabledPlugins": {
    "pyright-lsp@claude-plugins-official": true,
    "typescript-lsp@claude-plugins-official": true
  }
}

tmux + iTerm2 통합

Claude Code는 장시간 세션이 많고, 프로젝트별로 여러 인스턴스를 띄우는 경우도 잦다. tmux로 세션을 관리하면 터미널을 닫아도 작업이 유지되고, iTerm2의 -CC 모드를 쓰면 tmux 세션이 네이티브 탭으로 표시되어 별도의 tmux 조작 없이 사용할 수 있다. tmux 자체에 대한 내용은 별도 주제이므로 여기서는 Claude Code와의 연동만 간략히 다룬다.

  • tmux -CC new-session -s nanoclaw: 프로젝트별 세션 생성, iTerm2 탭으로 표시
  • 세션 bury/attach: 백그라운드 유지 + 필요 시 복원
  • iTerm2 종료 후에도 세션 유지 → tmux -CC attach로 재접속

Teammate (멀티 에이전트)

CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1을 활성화하면 Agent tool로 서브에이전트를 생성해 병렬 작업을 수행할 수 있다.

서브에이전트 유형에는 general-purpose(범용), Explore(코드베이스 탐색), Plan(설계) 등이 있다. isolation: "worktree"를 지정하면 독립된 Git 브랜치에서 작업하여 메인 브랜치를 오염시키지 않는다.

teammateMode: "tmux"를 설정하면 각 서브에이전트가 별도의 tmux 패널에서 실행된다. 메인 에이전트와 서브에이전트의 작업 진행 상황을 동시에 볼 수 있어서, 어떤 에이전트가 어디서 막혀 있는지 바로 파악할 수 있다. iTerm2 + tmux -CC 모드와 결합하면 각 에이전트가 네이티브 탭으로 표시되어 더욱 편리하다.

비용 면에서 주의가 필요하다. 일반 대화 대비 약 7배의 토큰을 소비한다. 서브에이전트에 Sonnet이나 Haiku 모델을 지정하면 비용을 크게 줄일 수 있다.

정리

  • GNU Stow가 모든 것의 기반이다. 설정 파일들을 dotfiles 레포로 관리하고, stow 한 줄이면 새 Mac에서 환경이 복원된다.
  • 3계층 CLAUDE.md로 관심사를 분리했다. 전역(원칙) → 작업공간(UI/인프라) → 프로젝트(세부) 순으로 적용된다.
  • statusline으로 비용, 컨텍스트, 세션 시간을 실시간 모니터링한다. 색상 단계가 자연스러운 경고 역할을 한다.
  • hooks + worktree로 병렬 작업을 자동 관리한다. 세션 종료 시 WIP 자동 커밋, 작업 완료 시 squash + PR로 깔끔하게 정리한다.
  • 32개 skill(npx 17 + 커스텀 15)로 콘텐츠 파이프라인부터 문서 처리까지 자동화했다. 멀티 모델 리뷰, Gemini 브라우저 자동화, Copilot 리뷰 분석이 한 줄 커맨드로 끝난다.
  • Plugins로 Claude Code 자체 기능을 확장한다. LSP, 프론트엔드 디자인, skill 생성까지 settings.json 한 줄로 활성화된다.
  • memory 시스템으로 세션 간 컨텍스트를 유지한다. 특히 feedback 유형은 Claude의 반복 실수를 방지하는 데 효과적이다.
  • LSP + MCP + Plugins는 같은 철학이다. 직접 파일을 다루는 대신 중간 프로토콜로 구조화된 정보를 얻는다. LSP는 IDE 생태계를, MCP는 외부 도구를, Plugins는 Claude Code 자체를 확장한다.

이전 버전에서는 3계층 CLAUDE.md, statusline, hooks + worktree가 구성의 중심이었다. 이번 업데이트로 커스텀 skill 15개와 플러그인 시스템이 추가되면서, “반복 지시를 설정으로 옮기는 것”에서 한 단계 나아가 복잡한 워크플로우를 skill로 자동화하는 것이 핵심이 됐다. 블로그 발행이 4명의 AI Critic을 거치는 파이프라인이 된 것처럼, Claude Code는 단순한 코딩 도구를 넘어 개인 자동화 플랫폼으로 확장되고 있다.

이어서 읽기