[LazyClaude 심층 10] Project-scoped sandbox — $HOME 격리와 setup helper
글로벌과 프로젝트, 두 스코프를 한 토글로. .claude/settings.local.json은 gitignore
LazyClaude 심층 10편. 내 노트북 전체 와 이 프로젝트 는 다른 스코프다. 같은 UI에서 두 스코프를 다루되, 절대 섞이지 않게.
두 스코프
Claude Code 사용자는 두 영역을 동시에 관리한다:
| 스코프 | 위치 | 의미 |
|---|---|---|
| 🌐 Global | ~/.claude/... |
모든 프로젝트의 기본값 |
| 📁 Project | <cwd>/.claude/... |
이 프로젝트만의 오버라이드 |
같은 종류의 설정(CLAUDE.md / settings.json / skills / commands / hooks)이 두 곳에 독립적으로 존재한다. 한쪽 변경이 다른 쪽에 영향을 주면 사고다.
UI — 토글 + 프로젝트 피커
LazyClaude의 모든 설정 탭에 같은 컨트롤을 둠:
┌──────────────────────────────────────────┐
│ [ Global ] [ Project ] │
│ │
│ Project: [ /Users/o/cmblir.github.io ▾ ] │
└──────────────────────────────────────────┘
토글 한 번에 읽고/쓰는 경로 가 통째로 바뀐다. 사용자가 어느 스코프를 보고 있는지 항상 보이게.
14 endpoint, 한 가드
프로젝트 스코프를 위한 신규 endpoint 14개:
GET/PUT /api/project/claude-md # CLAUDE.md
GET/PUT /api/project/settings # .claude/settings.json
GET/PUT /api/project/settings-local # .claude/settings.local.json
GET/PUT /api/project/skill # .claude/skills/<id>/SKILL.md
GET/PUT /api/project/command # .claude/commands/**/*.md
... (총 14개)
각 endpoint는 쿼리 파라미터로 cwd를 받는다. 그리고 모든 endpoint가 같은 가드 를 거친다.
가드의 본체
import os
HOME = os.path.realpath(os.path.expanduser("~"))
def project_path(cwd: str, *parts) -> str:
real_cwd = os.path.realpath(os.path.expanduser(cwd))
if not real_cwd.startswith(HOME + os.sep):
raise PermissionError("project path escapes $HOME")
if not os.path.isdir(real_cwd):
raise FileNotFoundError(real_cwd)
target = os.path.realpath(os.path.join(real_cwd, *parts))
if not target.startswith(real_cwd + os.sep) and target != real_cwd:
raise PermissionError("traversal detected")
return target
3단계:
cwd가$HOME아래인가- 진짜 디렉터리인가
- 조합한 결과 path가 다시
cwd아래인가 (..트래버설 차단)
realpath로 두 번 푸는 게 핵심 — symlink로 우회되는 것을 막는다.
settings.local.json — 개인 오버라이드
.claude/settings.json은 팀이 공유 한다 (git에 들어감). 그러나 개인 환경 에서만 켜고 싶은 옵션이 있을 수 있다:
.claude/
├─ settings.json # team-shared, committed
└─ settings.local.json # personal, gitignored
settings.local.json은 자동으로 .gitignore에 들어가야 한다. LazyClaude는 PUT 시점에 .gitignore 검사:
def ensure_gitignored(cwd: str, rel: str):
gi = os.path.join(cwd, ".gitignore")
needed = rel
existing = ""
if os.path.exists(gi):
with open(gi) as f:
existing = f.read()
if needed not in existing.splitlines():
with open(gi, "a") as f:
sep = "" if (existing.endswith("\n") or not existing) else "\n"
f.write(sep + needed + "\n")
처음 settings.local.json을 저장하는 순간 .gitignore에 한 줄이 자동 추가됨. 사용자가 깜빡 잊을 수 없게.
권한 정규화 — 글로벌 파이프라인 재사용
설정 파일에는 permissions.allow / deny 같은 권한 키 가 있다. 글로벌과 프로젝트가 같은 정규화 규칙 을 거쳐야 한다:
- 동일 매처 통합 (
Edit+Edit을 한 번만) - 충돌 시 deny 우선
- 정렬 순서 안정화
LazyClaude는 글로벌 시점에 이미 만든 정규화 함수 를 그대로 프로젝트에 재사용. 같은 파이프라인이 두 스코프에서 동일한 결과를 보장.
Skill / Command — 디렉터리 스코프
Skills와 Commands는 파일 단위 가 아니라 디렉터리 단위:
.claude/skills/<skill-id>/SKILL.md
.claude/commands/<group>/<command>.md
스코프 토글이 <cwd>/.claude/skills/...로 root를 바꾼다. 같은 id의 글로벌 스킬과 프로젝트 스킬이 동시에 존재 — 어느 것이 적용되는지는 Claude Code의 우선순위가 결정한다 (project > user, 일반적으로).
사용 패턴 — 어디에 무엇
| 무엇 | Global이 더 좋음 | Project가 더 좋음 |
|---|---|---|
| Code style 규칙 | 공용 디폴트 (ko/en, 들여쓰기) | 이 프로젝트의 특수 규칙 |
| Skill | 범용 도구 | 이 도메인 전용 |
| Hook | 일반 가드레일 | 이 레포의 정책 |
| settings.json | 모든 프로젝트의 시작점 | 이 레포의 권한 |
| settings.local.json | (없음) | 개인 오버라이드 |
일반화 — 두 스코프를 다루는 UI 원칙
이 패턴은 다른 도구에서도 그대로:
- 스코프 인디케이터 항상 보이게 — 토글 + 현재 경로
- 읽기/쓰기 경로 동시 전환 — 한 컨트롤이 모든 endpoint에 영향
- 공용 가드 — 모든 endpoint가 같은 보안 함수 통과
- 자동 .gitignore — 개인 파일은 시키지 않아도 무시
- 정규화 재사용 — 한 스코프에서 만든 규칙이 다른 스코프에 자동 적용
한 줄 요약
두 스코프를 동시에 다루는 UI 의 핵심은 사용자가 어느 쪽에 있는지 잊지 않게 만드는 것.
이전 편: LazyClaude 심층 9 — Anthropic Labs
LazyClaude 심층 시리즈 진행 (10편)
- TTL + mtime 캐시
- 워크플로우 캔버스 최적화
- 첫 페인트 차단 풀기
- PWA + macOS .app 번들
- Hyper Agent
- Auto-Resume
- Multi-provider 워크플로우
- Hook Detective
- Computer Use · Memory · Advisor Lab
- Project-scoped sandbox