에이전트 하네스 엔지니어링: Claude Code를 외골격으로 설계하는 실전 아키텍처
에이전트 하네스 엔지니어링: Claude Code를 외골격으로 설계하는 실전 아키텍처

왜 똑같은 Claude Code를 써도 어떤 팀은 생산성이 3배가 되고 어떤 팀은 사고가 나는가
Claude Code를 처음 도입하는 팀이 흔히 빠지는 함정이 있습니다. "좋은 프롬프트만 쓰면 된다"는 가정입니다. 같은 CLI 도구를 써도 개발자마다 결과가 극명하게 갈립니다. 어떤 개발자는 PR을 반나절에 마무리하고, 어떤 개발자는 에이전트가 의도치 않은 파일을 덮어쓰거나 커밋 메시지 컨벤션을 무시하는 사고를 반복합니다.
한동안 운영하다 보면 결국 같은 결론에 도달합니다. 문제는 프롬프트가 아니라 하네스(harness) 였다는 것입니다. 프롬프트 엔지니어링이 모델에게 "무엇을 해달라"고 요청하는 기술이라면, 하네스 엔지니어링은 에이전트가 "무엇을 할 수 있고, 무엇을 절대 해선 안 되는지"를 시스템 수준에서 정의하는 기술입니다. 모델의 판단 능력에 의존하는 것이 아니라 실행 환경 자체를 설계하는 것입니다.
이 글은 Claude Code의 하네스를 구성하는 5개 레이어(프롬프트/도구/컨텍스트/권한/훅)를 체계적으로 설계하는 방법을 다룹니다. CLAUDE.md의 3계층 메모리 구조, settings.json의 권한 모델, 결정론적 훅 파이프라인, 슬래시 커맨드와 Skills, MCP 도구 등록 전략까지 프로덕션 코드베이스에서 직접 검증한 실전 아키텍처를 공유합니다.
1. 하네스란 무엇인가: 프롬프트 엔지니어링이 멈추는 곳에서 시작하는 것
Claude Code 공식 문서(https://code.claude.com/docs/en/how-claude-code-works)는 Claude Code를 "에이전트 하네스(agentic harness)"라고 명시합니다. 언어 모델(LM)은 텍스트를 생성하지만, 하네스는 그 텍스트가 현실 세계의 어느 부분에 닿을 수 있는지 결정합니다. 하네스는 모델과 세계 사이의 모든 것입니다.
프롬프트 엔지니어링의 한계는 명확합니다. 모델이 올바른 판단을 내리도록 유도할 수는 있지만, 잘못된 판단을 내렸을 때 그것을 막을 수 없습니다. rm -rf 명령어를 프롬프트로 막으려면 매 세션마다 "절대 파일을 일괄 삭제하지 마세요"라고 주입해야 합니다. 반면 훅(hook)으로 막으면 그 명령어는 에이전트의 판단과 무관하게 항상 차단됩니다.
에이전트 하네스는 다음 5대 제어 레이어로 구성됩니다.
| 레이어 | 구성 요소 | 제어 방식 | 적용 범위 |
|---|---|---|---|
| 프롬프트 | CLAUDE.md, Auto Memory | 확률적 (모델 의존) | 행동 양식·컨벤션 |
| 도구 | 빌트인 도구, MCP 서버 | 정책 기반 | 에이전트 능력 범위 |
| 컨텍스트 | 메모리 계층, Skills | 선택적 로딩 | 세션 지식 범위 |
| 권한 | settings.json, allow, deny | 화이트리스트/블랙리스트 | 실행 가능 액션 |
| 훅 | PreToolUse, PostToolUse, Stop | 결정론적 실행 | 불변 제약 조건 |
이 레이어를 모두 설계했을 때 비로소 하네스가 완성됩니다. 어느 하나만 공들여도 나머지 레이어의 구멍이 사고로 이어집니다. 프로덕션 코드베이스 운영에서 얻은 핵심 교훈은 이것입니다: 신뢰는 모델에게 위임하고, 안전은 하네스가 보장한다.
2. CLAUDE.md 설계: 프로젝트마다 반드시 넣어야 할 컨텍스트 4가지
CLAUDE.md는 Claude Code가 제공하는 가장 강력한 영속 컨텍스트 메커니즘입니다. 각 세션은 독립적으로 시작되지만, CLAUDE.md는 매 세션 시작 시 자동으로 컨텍스트 창에 주입됩니다. 공식 문서에 따르면 CLAUDE.md 파일은 현재 작업 디렉터리에서 루트까지 계층적으로 탐색하여 모두 이어붙여(concatenate) 로드됩니다.
3계층 메모리 구조
Global 계층 ~/.claude/CLAUDE.md
└─ 모든 프로젝트에 적용
(개인 코딩 스타일, 툴 단축키)
Project 계층 ./CLAUDE.md 또는 ./.claude/CLAUDE.md
└─ 팀 전체에 공유 (git 추적)
(아키텍처, 컨벤션, 워크플로)
Session/Local ./CLAUDE.local.md
└─ .gitignore 처리, 개인 전용
(로컬 샌드박스 URL, 테스트 데이터 경로)
더 구체적인 위치의 파일이 더 넓은 범위의 파일보다 우선합니다. 조직 전체 정책이 필요하다면 macOS 기준 /Library/Application Support/ClaudeCode/CLAUDE.md에 관리형 정책(Managed Policy)을 배포할 수 있습니다. Linux·WSL에서는 /etc/claude-code/CLAUDE.md가 해당 경로입니다.
CLAUDE.md의 권장 크기는 200줄 이하입니다. 파일이 길어질수록 컨텍스트를 소모하고 준수율이 떨어집니다. 200줄 안에서 반드시 포함시켜야 할 4가지 컨텍스트는 다음과 같습니다.
실전 CLAUDE.md 템플릿
# 프로젝트: waylog-api
## 빌드 & 테스트
- 빌드: `pnpm build`
- 테스트: `pnpm test` (커밋 전 반드시 실행)
- 린트: `pnpm lint` — ESLint + Prettier
- 타입 체크: `pnpm typecheck`
## 커밋 & 브랜치 규칙
- 커밋 메시지는 **반드시 한국어**로 작성한다
- 형식: `feat(scope): 설명`, `fix(scope): 설명`, `chore: 설명`
- 예시: `feat(auth): 소셜 로그인 카카오 연동 추가`
- **직접 커밋 금지**: 모든 변경은 PR을 통해 main에 반영한다
- `git push --force`는 절대 사용하지 않는다
## 코딩 컨벤션
- TypeScript strict 모드 활성화 — `as any` 사용 금지
- 함수형 컴포넌트 + React hooks 패턴 유지
- 네이밍: 컴포넌트 PascalCase, 함수/변수 camelCase
- API 핸들러는 `src/api/handlers/` 아래에 위치
- 에러 처리: 모든 async 함수는 try/catch 또는 Result 패턴
## 테스트 선행 원칙
- 새 기능 구현 전 테스트 케이스 먼저 작성 (TDD)
- 테스트 파일 위치: 구현 파일과 동일 디렉터리, `.test.ts` 접미사
- 단위 테스트: Vitest, E2E: Playwright
## Compact Instructions
위 4개 규칙은 컨텍스트 압축 후에도 반드시 유지한다.
커밋 금지, 테스트 선행, 한국어 커밋 메시지, strict TypeScript.
/init으로 초안 생성 후 다듬기 워크플로
새 프로젝트에서 CLAUDE.md를 처음 만들 때는 /init 커맨드로 시작합니다. Claude가 프로젝트 구조, 빌드 시스템, 테스트 프레임워크를 스캔해 초안을 자동 생성합니다. CLAUDE_CODE_NEW_INIT=1 환경변수를 설정하면 인터랙티브 멀티페이즈 플로우가 활성화되어 CLAUDE.md, Skills, Hooks를 한 번에 설정할 수 있습니다.
초안이 생성된 후에는 반드시 두 가지를 추가해야 합니다. 첫째, 자동 스캔으로는 알 수 없는 팀 고유의 약속(위 템플릿의 "커밋 금지" 규칙 등). 둘째, ## Compact Instructions 섹션으로, 컨텍스트 압축(compaction) 후에도 핵심 규칙이 살아남도록 명시합니다.
3. settings.json 권한 모델: allow·deny·hooks 3단계로 에이전트를 묶는 법
권한 모델은 하네스의 두 번째 레이어이자 가장 구체적인 경계선입니다. .claude/settings.json은 팀 전체가 공유하는 프로젝트 권한 정책이고, .claude/settings.local.json은 개인 전용으로 git에 추적하지 않습니다.
현행 Claude Code의 permissions 스키마는 allow, deny, ask 세 필드를 사용합니다. 과거 문서나 일부 예제에 등장하는 allowedTools는 구버전 표기이므로 주의하세요.
완전한 settings.json 예시
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"model": "claude-sonnet-4-6",
"permissions": {
"allow": [
"Read",
"Write",
"Edit",
"MultiEdit",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git add*)",
"Bash(git commit*)",
"Bash(pnpm*)",
"Bash(npm*)",
"Bash(npx*)",
"Bash(node*)",
"Bash(cat*)",
"Bash(ls*)",
"Bash(find*)",
"Bash(grep*)",
"WebSearch",
"WebFetch",
"mcp__filesystem__read_file",
"mcp__filesystem__list_directory",
"mcp__github__search_repositories",
"mcp__github__get_file_contents"
],
"deny": [
"Bash(rm*)",
"Bash(sudo*)",
"Bash(chmod*)",
"Bash(chown*)",
"Bash(curl * | bash*)",
"Bash(wget * | bash*)",
"Bash(git push --force*)",
"Bash(git reset --hard*)",
"Bash(> /dev/null*)",
"mcp__filesystem__write_file"
],
"ask": [
"Bash(git push*)"
]
},
"autoMemoryEnabled": true,
"claudeMdExcludes": []
}
allow 패턴 매칭 규칙
allow의 패턴은 세 가지 방식으로 동작합니다. "Read"처럼 도구 이름만 쓰면 해당 도구의 모든 호출을 허용합니다. "Bash(git status)"처럼 괄호 안에 명령어를 쓰면 정확히 그 명령어만 허용합니다. "Bash(pnpm*)" 형태의 와일드카드는 pnpm으로 시작하는 모든 Bash 호출을 허용합니다.
deny가 allow보다 항상 우선합니다. allow에 "Bash(*)" 와일드카드를 넣고 deny에 "Bash(rm*)" 을 넣으면, rm 계열 명령어는 allow 목록에 걸려도 deny에 의해 최종 차단됩니다. 이 우선순위 관계는 화이트리스트가 실수로 과도하게 넓어졌을 때의 안전망입니다.
ask 필드는 해당 패턴에 매칭되는 도구 호출 시 사용자에게 확인을 요청합니다. git push처럼 실행 여부를 명시적으로 결정하고 싶은 작업에 유용합니다.
팀 공유 vs 개인 로컬 분리 전략
.claude/settings.json은 git에 커밋하여 팀 전체가 동일한 권한 정책을 공유합니다. 개인 테스트 환경의 도구나 실험적 MCP 서버는 .claude/settings.local.json에 넣고 .gitignore에 추가합니다. 두 파일은 병합(merge)되며, local 파일의 allow 항목이 project 파일 위에 추가됩니다. 팀 deny 규칙은 반드시 project 파일에 두어야 개인 local 파일이 임의로 우회하지 못합니다.
4. Hooks 파이프라인: 에이전트가 절대 실수하지 못하도록 막는 결정론적 트리거
훅은 하네스의 핵심 결정론 레이어입니다. CLAUDE.md가 모델에게 "이렇게 해달라"고 요청하는 것과 달리, 훅은 모델의 의도와 무관하게 항상 실행됩니다. Claude가 아무리 타당한 이유로 rm -rf를 실행하려 해도, PreToolUse 훅이 exit code 2를 반환하면 그 명령어는 실행되지 않습니다.
훅 생명주기 4단계
[사용자 프롬프트 입력]
│
▼
┌─────────────────────┐
│ UserPromptSubmit │ ← 프롬프트 검증/수정
└──────────┬──────────┘
│ Claude 처리 (도구 선택)
▼
┌─────────────────────┐
│ PreToolUse │ ← 도구 실행 전 허용/차단
└──────────┬──────────┘
│ 도구 실행
▼
┌─────────────────────┐
│ PostToolUse │ ← 실행 후 후처리 (포맷팅, 로깅)
└──────────┬──────────┘
│ Claude 응답 완성
▼
┌─────────────────────┐
│ Stop │ ← 턴 종료 시 알림/검증
└─────────────────────┘
공식 문서(https://code.claude.com/docs/en/hooks)에 따르면 훅 핸들러는 command, http, mcp_tool, prompt, agent 다섯 가지 타입을 지원합니다. exit code 0은 성공, exit code 2는 블로킹 에러(stderr 내용이 Claude에게 전달됨), 그 외 non-zero는 비블로킹 에러입니다.
위 다이어그램은 자주 쓰이는 핵심 이벤트만 표시한 것입니다. 공식 문서에는 SessionStart, SessionEnd, SubagentStart, SubagentStop, PreCompact, PostCompact, FileChanged 등 더 많은 이벤트가 정의되어 있습니다.
실전 훅 1: rm -rf 계열 사전 차단 Python 스크립트
#!/usr/bin/env python3
"""
PreToolUse hook: 파괴적 셸 명령어를 실행 전 차단한다.
위치: .claude/hooks/block-destructive.py
"""
import sys
import json
import re
BLOCKED_PATTERNS = [
r'rms+-[a-zA-Z]*r[a-zA-Z]*s', # rm -r, rm -rf, rm -Rf 등
r'rms+--recursive',
r'dds+if=', # dd 디바이스 덮어쓰기
r'mkfs.', # 파일시스템 포맷
r'shred', # 안전 삭제
r'>s*/dev/(s?d[a-z]|nvme)', # 블록 디바이스 직접 쓰기
r'gits+pushs+(?!.*--force-with-lease).*--force', # force push (--force-with-lease 허용)
r'gits+resets+--hards+HEAD[~^]', # 커밋 되돌리기
]
def main():
try:
data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
sys.exit(0)
# Bash 도구 호출만 검사
if data.get("tool_name") != "Bash":
sys.exit(0)
command = data.get("tool_input", {}).get("command", "")
for pattern in BLOCKED_PATTERNS:
if re.search(pattern, command):
result = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
f"파괴적 명령어 차단: '{command[:60]}' — "
"팀 하네스 정책에 의해 실행이 거부되었습니다. "
"의도적인 삭제가 필요하다면 수동으로 실행하세요."
)
}
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()
원본의 --force 정규식 r'gits+pushs+.*--force(?!-with-lease)'는 부정 전방탐색 위치가 잘못되어 --force-with-lease도 차단하는 문제가 있었습니다. --force가 패턴 어디에든 존재하면 전방탐색이 그 자리에서만 작동하기 때문입니다. 위 예시처럼 (?!.*--force-with-lease).*--force 순서로 작성해야 의도대로 동작합니다.
실전 훅 2: TypeScript 파일 저장 시 prettier 자동 실행
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash -c 'FILE=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r \".file_path // .path // empty\"); if [[ \"$FILE\" == *.ts ]] || [[ \"$FILE\" == *.tsx ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi'"
}
]
}
]
}
}
실전 훅 3: 장시간 작업 완료 시 macOS 알림
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code 작업이 완료되었습니다.\" with title \"Claude Code\" sound name \"Glass\"'"
}
]
}
]
}
}
세 번째 훅은 Claude Code가 백그라운드에서 장시간 작업(리팩터링, 전체 테스트 실행 등)을 돌릴 때 특히 유용합니다. Stop 이벤트는 에이전트 턴이 완전히 종료될 때 발화하므로, 다른 창에서 작업하다가 완료 알림을 받을 수 있습니다.
결정론 원칙: 훅은 Claude의 텍스트 생성 이전/이후에 독립적으로 실행됩니다. 모델이 아무리 설득력 있는 이유로 파괴적 명령어를 정당화해도, PreToolUse 훅은 그 추론 내용을 보지 않습니다. 셸 명령어 패턴만 보고 차단합니다. 이 결정론이 하네스의 핵심 가치입니다.
5. 슬래시 커맨드와 Skills: 반복 작업을 에이전트 어휘로 만드는 설계
슬래시 커맨드(slash command)는 팀이 자주 반복하는 작업을 에이전트의 어휘(vocabulary)로 등록하는 메커니즘입니다. .claude/commands/ 디렉터리에 마크다운 파일을 두면 해당 파일명으로 슬래시 커맨드가 생성됩니다.
디렉터리 구조
.claude/
├── CLAUDE.md # 프로젝트 메모리
├── settings.json # 팀 공유 권한 정책
├── settings.local.json # 개인 전용 설정 (.gitignore)
├── hooks/
│ ├── block-destructive.py
│ └── notify-done.sh
└── commands/
├── review.md # /review — PR 코드 리뷰
├── test-gen.md # /test-gen — 테스트 자동 생성
├── refactor.md # /refactor — 리팩터링 가이드라인 적용
└── deploy-check.md # /deploy-check — 배포 전 체크리스트
커스텀 슬래시 커맨드 예시: /review.md 전체 내용
---
description: 현재 스테이징된 변경 사항을 팀 코딩 컨벤션 기준으로 코드 리뷰한다
allowed-tools:
- Read
- Bash(git diff*)
- Bash(git status)
- Bash(pnpm lint*)
- Bash(pnpm typecheck*)
---
# 코드 리뷰 커맨드
## 목표
현재 git stage 또는 지정된 파일에 대해 팀 기준의 코드 리뷰를 수행한다.
## 실행 절차
1. `git diff --staged` 또는 `git diff HEAD` 로 변경된 파일 목록 파악
2. 각 변경 파일에 대해 다음 관점에서 리뷰:
- TypeScript strict 위반 여부 (`as any`, `!` non-null assertion 남용)
- 함수 단일 책임 원칙 준수
- 에러 핸들링 누락 여부
- 테스트 커버리지 확인 (변경된 로직에 대응하는 테스트 존재 여부)
- CLAUDE.md의 컨벤션 위반 여부
3. `pnpm lint` 및 `pnpm typecheck` 실행 후 오류 보고
4. 마크다운 형식으로 리뷰 결과 출력:
- 필수 수정 (머지 차단)
- 권고 수정 (개선 권장)
- 긍정적 패턴 (유지 권장)
## 출력 형식
각 이슈는 파일명, 라인 번호, 문제 설명, 수정 제안을 포함한다.
이 커맨드를 /review로 호출하면, Claude는 해당 파일에 정의된 절차대로 동작합니다. allowed-tools 프론트매터는 이 커맨드 실행 중 허용되는 도구를 추가로 제한합니다. 즉, 리뷰 커맨드는 읽기와 린트만 할 수 있고 파일을 수정하지 않습니다.
Skills로 확장되는 커맨드 생태계
공식 문서 기준, 기존 .claude/commands/ 방식의 슬래시 커맨드는 더 넓은 개념인 Skills로 병합되었습니다. 정확히는 두 방식이 동일하게 동작하며, .claude/commands/deploy.md와 .claude/skills/deploy/SKILL.md는 둘 다 /deploy를 생성합니다.
Skills는 SKILL.md 파일로 정의하며, 슬래시 커맨드보다 구조화된 기능을 지원합니다. 주요 차이점은 다음과 같습니다.
- 지원 파일 디렉터리: 스크립트, 예제, 참고 문서를 같은 디렉터리에 묶을 수 있습니다.
context: fork: 격리된 서브에이전트 컨텍스트에서 실행할 수 있습니다.disable-model-invocation: Claude가 자동으로 호출하지 못하도록 제한할 수 있습니다.- 자동 발화:
description필드를 기반으로 Claude가 문맥에 맞게 자동으로 로드합니다.
기존 .claude/commands/ 방식은 하위 호환성이 유지되므로 기존 팀 설정을 즉시 마이그레이션할 필요는 없습니다. 새로 만드는 워크플로는 Skills 형식(~/.claude/skills/ 또는 .claude/skills/)을 권장하지만, 기존 커맨드 파일은 그대로 동작합니다.
6. MCP 연결과 도구 권한 화이트리스트: 컨텍스트 창을 지키는 선별적 서버 등록
MCP(Model Context Protocol)는 Claude Code가 외부 서비스(파일시스템, GitHub, 데이터베이스 등)에 연결되는 표준 프로토콜입니다. MCP의 개념과 원리에 대한 상세한 내용은 미래를 여는 열쇠: Model Context Protocol (MCP) 심층 가이드에서 이미 다뤘습니다. 여기서는 하네스 관점에서 MCP 도구를 선별적으로 등록하는 전략에 집중합니다.
MCP 도구의 lazy-load 원리
Claude Code는 MCP 도구를 기본적으로 lazy-load 방식으로 처리합니다. 세션 시작 시에는 도구 이름(description)만 컨텍스트에 적재하고, Claude가 실제로 특정 도구를 호출하려 할 때 전체 도구 스키마를 로딩합니다. 이 설계 덕분에 많은 MCP 서버를 등록해도 초기 컨텍스트 비용은 낮게 유지됩니다.
MCP 도구가 많아질 때의 컨텍스트 문제
그러나 MCP 서버 수가 많아지고 각 서버에 도구가 많아질수록, 도구 이름 목록 자체가 컨텍스트를 잠식합니다. 커뮤니티 보고에 따르면 MCP 도구가 수십 개를 넘어가면 사용 가능한 컨텍스트가 체감상 크게 줄어드는 경우가 있습니다. Claude가 도구를 검색(tool search)하는 방식으로 이를 완화할 수 있지만, 근본 해법은 필요한 MCP 서버만 등록하는 것입니다.
허용 MCP 서버 등록 예시
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/username/projects/waylog-api",
"/Users/username/projects/waylog-blog"
],
"env": {},
"description": "프로젝트 파일시스템 접근 — 읽기/목록만 허용",
"permissions": {
"allow": [
"mcp__filesystem__read_file",
"mcp__filesystem__read_multiple_files",
"mcp__filesystem__list_directory",
"mcp__filesystem__get_file_info"
]
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PAT}"
},
"description": "GitHub 리포지터리 검색 및 파일 조회",
"permissions": {
"allow": [
"mcp__github__search_repositories",
"mcp__github__get_file_contents",
"mcp__github__list_commits",
"mcp__github__get_pull_request"
]
}
},
"postgres": {
"command": "npx",
"args": ["-y", "@waylog/mcp-server-postgres"],
"env": {
"DATABASE_URL": "${DEV_DATABASE_URL}"
},
"description": "개발 DB 스키마 조회 전용 — SELECT 쿼리만 허용",
"permissions": {
"allow": [
"mcp__postgres__query",
"mcp__postgres__list_tables",
"mcp__postgres__describe_table"
]
}
}
}
}
각 MCP 서버에도 permissions.allow를 별도로 지정한 점에 주목하세요. filesystem 서버의 write_file 도구는 목록에 넣지 않았고, postgres 서버에는 SELECT 관련 도구만 허용했습니다. 이렇게 하면 settings.json 최상위 deny 규칙과 MCP 서버별 allow 목록이 이중으로 에이전트를 제한합니다.
RAG 기반 지식 베이스나 벡터 검색이 필요한 에이전트 설계에 관심 있다면 RAG 기반 AI 에이전트 완전 가이드를 참고하세요. MCP와 RAG를 결합한 아키텍처는 컨텍스트 창 효율을 극적으로 개선합니다.
7. 운영하며 발견한 하네스 안티패턴 5가지
실제 프로덕션 환경에서 Claude Code 하네스를 운영하다 보면 반복적으로 마주치는 안티패턴이 있습니다. 각각은 사소해 보이지만 쌓이면 에이전트 운영 품질을 눈에 띄게 떨어뜨립니다.
안티패턴 1: allow에 전역 와일드카드만 두는 위험
빠른 실험을 위해 "allow": ["Bash(*)"]처럼 전체 허용 패턴을 쓰는 경우가 있습니다. deny 규칙이 없으면 모든 셸 명령어가 허용됩니다. 개발 초기 단계에서 마찰을 줄이려는 의도지만, 에이전트가 패키지 설치, 환경 변수 출력, 네트워크 호출 등 의도치 않은 동작을 하는 문제로 이어집니다. 와일드카드를 쓸 거라면 반드시 deny 규칙을 먼저 정의하고, 팀 코드 리뷰에 settings.json 변경을 포함시켜야 합니다.
안티패턴 2: CLAUDE.md에 너무 많은 규칙을 쌓아 컨텍스트 낭비
팀 협업 과정에서 CLAUDE.md에 규칙이 계속 추가됩니다. "이번에 이런 실수가 있었으니 추가하자"는 선의에서 비롯되지만, 파일이 400줄을 넘어서면 두 가지 문제가 생깁니다. 컨텍스트를 과도하게 소모해 실제 코드 작업에 쓸 창이 줄어들고, 규칙이 많아질수록 모델이 모순된 규칙을 만나 임의로 하나를 선택하는 빈도가 늘어납니다. 200줄 상한을 팀 합의로 정하고, 특정 파일 타입에만 필요한 규칙은 .claude/rules/ 경로 스코프 파일로 분리하는 것이 효과적입니다.
안티패턴 3: 훅 오류를 무시하다 에이전트가 멈추는 상황
훅 스크립트에서 예외가 발생하면 exit code가 비정상(non-zero)으로 반환됩니다. 블로킹 훅(exit 2)은 에이전트 실행을 중단시키고, 비블로킹 훅(기타 non-zero)은 오류를 기록하지만 실행을 계속합니다. 문제는 훅 스크립트 자체의 버그(Python 의존성 미설치, jq 명령어 부재 등)로 인해 exit code 2가 반환되면 에이전트가 완전히 멈춘다는 점입니다. 모든 훅 스크립트에 set -e 대신 명시적 에러 핸들링을 사용하고, 훅이 실패할 경우 exit 0으로 폴백하는 패턴을 표준으로 채택하면 이 문제를 방지할 수 있습니다.
안티패턴 4: MCP 서버를 한꺼번에 다 켜는 실수
"언젠가 쓸 것 같아서" 수십 개의 MCP 서버를 등록해두는 패턴입니다. 앞서 설명했듯 MCP 도구가 많아지면 컨텍스트 창이 압박을 받습니다. 더 중요한 점은, 등록된 서버 중 하나라도 네트워크 오류나 인증 만료가 생기면 세션 시작이 지연되거나 실패한다는 것입니다. 현재 작업에 직접 필요한 서버만 활성화하는 "최소 등록 원칙"을 세우고, 환경별(개발/스테이징/프로덕션)로 다른 MCP 설정 파일을 유지하면 이 문제를 효과적으로 관리할 수 있습니다.
안티패턴 5: settings.json을 팀 공유 없이 개인 로컬에만 두는 고립 운영
가장 흔하고 가장 위험한 안티패턴입니다. 개발자 A는 촘촘한 deny 규칙을 갖추고 있지만, 새로 합류한 개발자 B는 기본 설정으로 Claude Code를 쓰다가 개발 DB 스키마를 drop하는 사고를 냅니다. settings.json을 git 리포지터리의 .claude/settings.json으로 관리하면 팀 전체가 동일한 최소 보호 기준을 갖추게 됩니다. 개인 편의를 위한 추가 허용 도구는 .claude/settings.local.json에 두고 .gitignore처리하면 됩니다. CI/CD 파이프라인에서 에이전트를 자동화할 때도 이 분리 전략이 필수입니다. CI 파이프라인 구성에 대한 자세한 내용은 GitHub Actions CI/CD 파이프라인 완벽 가이드와 AI 주도 개발: 실전 적용 전략을 참고하세요.
8. 결론: 하네스 구축 실무 체크리스트 5가지
"프롬프트 엔지니어링"에서 "하네스 엔지니어링"으로의 전환은 개발 도구를 다루는 패러다임의 변화입니다. 모델에게 올바른 판단을 기대하는 것에서, 시스템 수준에서 올바른 동작만 가능하도록 환경을 설계하는 것으로 이동하는 것입니다.
프로덕션 환경에서 Claude Code 하네스를 완성하려는 팀에게 다음 5가지를 체크리스트로 제안합니다.
-
CLAUDE.md를 200줄 이하로 유지하라: 팀 규칙, 커밋 컨벤션, 테스트 선행, 컨텍스트 언어 4가지만 넣고, 나머지는
.claude/rules/경로 스코프 파일로 분리한다./init으로 초안을 생성한 뒤 팀 약속을 추가하는 워크플로를 표준화한다. -
settings.json을 git으로 관리하라: 팀
deny규칙은 반드시.claude/settings.json에 두어 모든 팀원이 동일한 보호 기준을 공유한다.allow에 와일드카드를 쓸 거라면deny를 먼저 정의한다. 현행 스키마의 필드명은allow/deny/ask임을 기억한다. -
PreToolUse 훅으로 불변 제약을 걸어라:
rm -rf,git push --force,sudo등 에이전트가 절대 실행해서는 안 되는 명령어 패턴을 훅으로 차단한다. CLAUDE.md 규칙과 달리 훅은 모델 판단과 무관하게 항상 실행된다. -
MCP 서버를 최소 등록 원칙으로 운영하라: 현재 프로젝트에 필요한 서버만 활성화하고, 각 MCP 서버에도
permissions.allow를 명시해 이중 제한을 건다. 도구 수가 많아질수록 컨텍스트 창 사용량을 직접 측정해보고 운영 기준을 세운다. -
슬래시 커맨드로 팀 워크플로를 표준화하라: 코드 리뷰, 테스트 생성, 배포 전 체크 등 반복 작업을
.claude/commands/에 마크다운으로 정의한다. 커맨드 파일의allowed-tools프론트매터로 해당 커맨드 실행 중 도구를 추가 제한한다. 더 많은 기능이 필요하면 Skills 형식으로 마이그레이션한다.
하네스는 한 번 만들면 끝나는 게 아닙니다. 팀이 새 도구를 도입할 때마다, 새 개발자가 합류할 때마다, 에이전트가 예상치 못한 동작을 할 때마다 업데이트해야 합니다. 운영 경험에서 얻은 가장 중요한 교훈은 이것입니다: 하네스는 팀의 합의를 코드로 옮긴 것이다. 팀의 신뢰는 모델 능력이 아니라 하네스의 견고함에서 나옵니다.
MCP 프로토콜의 원리를 깊이 이해하고 싶다면 미래를 여는 열쇠: Model Context Protocol (MCP) 심층 가이드를, 에이전트에 장기 기억과 지식 검색을 붙이는 방법은 RAG 기반 AI 에이전트 완전 가이드를, 이 하네스를 CI/CD 파이프라인에 연결하는 방법은 GitHub Actions CI/CD 파이프라인 완벽 가이드와 AI 주도 개발: 실전 적용 전략에서 이어서 다룹니다.