나는 Claude Code를 단순한 코드 생성 도구가 아니라, OMC(oh-my-claudecode)와 결합된 AI 개발 워크플로우의 일부로 사용한다.
/plan, /team, /ralph, /ultraqa 같은 workflow를 제공하는 multi-agent orchestration layer전체 흐름은 다음과 같다.
요청 → Claude Code → OMC → 개인 하네스 → 서버 구현 결과
하네스 설계의 모든 결정은 Claude Code의 실제 동작 메커니즘을 정확히 이해한 위에서 내려졌다.
Claude(모델)는 매 턴마다 다음을 입력으로 받는다.
모델은 이걸 보고 응답 스트림을 만든다. 응답은 두 종류 블록으로 구성된다.
모델이 Edit(file_path=..., old_string=..., new_string=...) 같은 tool_use를 내뱉으면, Claude Code 런타임이 가로채서 다음 순서로 처리한다.
{tool_name, tool_input, session_id, transcript_path, cwd} 전달tool_response까지 포함된 JSON 받음tool_use가 없는 순수 text 응답이 나오면 턴 종료. 그 시점에 Stop hook 실행.
또한 사용자 입력이 들어올 때마다 UserPromptSubmit hook이 실행된다. 이 hook이 stdout으로 출력한 내용은 모델 컨텍스트에 추가 주입된다. 매 턴 정책 텍스트를 모델에 주입하는 채널.
세션이 시작될 때는 SessionStart hook이 실행된다. Claude Code를 새로 켤 때 자동으로 git 동기화 같은 초기 작업을 수행하는 채널.
| 도구 | 하는 일 | 위험도 |
|---|---|---|
| Read | 파일 읽기 | 안전 |
| Glob | 파일 경로 패턴 검색 | 안전 |
| Grep | 파일 내용 검색 | 안전 |
| Edit | 파일 일부 수정 | 위험 |
| Write | 파일 통째 쓰기 | 위험 |
| MultiEdit | 한 파일 여러 군데 수정 | 위험 |
| NotebookEdit | 주피터 노트북 셀 수정 | 위험 |
| Bash | 셸 명령 실행 | 가변 |
| WebFetch / WebSearch | 외부 자료 가져오기 | 안전 |
| Task / Agent | 서브에이전트 호출 | 가변 |
| Skill / ToolSearch | 메타 (스킬 호출, 도구 검색) | 안전 |
| TodoWrite | 자기 할 일 관리 | 안전 |
Claude Code에는 빌트인 plan mode가 있다. plan mode는 두 개의 도구로 제어된다.
EnterPlanMode: 모델이 plan mode로 진입할 때 호출. 인자 없음. 호출 후 모델은 read-only 도구와 ExitPlanMode 외에는 호출하지 못한다.ExitPlanMode(plan="..."): 모델이 계획을 인자로 넣어 호출하면, Claude Code UI가 승인 다이얼로그를 띄운다.이 두 도구는 자연어 분류 없이 명시적으로 승인을 받을 수 있는 네이티브 채널이다.
OMC와 Claude Code 기본 기능만으로는 다음 여섯 가지가 보장되지 않는다.
OMC의 /plan은 호출했을 때만 실행된다. "AI에게 코드 변경 전 무조건 계획을 세우라"는 게이트는 없다. 팀원이 "@@ 기능 구현해줘"라고 한 줄 던지면 AI가 곧바로 코드를 수정한다.
Edit/Write만 막아도 모델은 Bash로 우회해 파일을 변경할 수 있다.
echo 'new content' > auth.py
sed -i 's/old/new/g' *.py
cat > config.py << EOF ... EOF
따라서 Bash도 함께 통제해야 코드 수정 차단이 의미 있다.
"진행해", "ㅇㅋ", "approve" 같은 자연어를 승인 신호로 쓰면 다음 문제가 생긴다.
/approve 같은 슬래시 커맨드는 명시적이지만 그걸 자발적으로 칠 사람은 어차피 계획 세울 사람이다. 정작 막아야 할 대상은 "@@해줘" 한 줄 던지고 결과만 기다리는 습관이라, 이 사람은 /approve를 안 친다.
OMC의 .omc/sessions, .omc/state는 agent lifecycle 중심이라 "내가 언제 뭘 시켰고 어떤 파일이 바뀌었나"를 시간순으로 빠르게 보기 어렵다. Claude Code의 transcript_path는 모든 정보가 들어있지만 사람이 읽기엔 부담스럽다.
Claude Code는 Bash 같은 위험 도구에 매번 별도의 권한 다이얼로그를 띄운다. 우리가 plan을 승인했어도 Claude Code의 built-in 권한 시스템은 그걸 모르고 또 묻는다. plan 한 번 승인했는데 작업 30개 중 매번 "Do you want to proceed?"가 뜨면 plan 승인의 의미가 깎인다.
git workflow에서 main에 직접 commit/push는 금기다. 하지만 AI는 그 규칙을 모르고 그냥 main에서 작업한다. 사용자가 PR 워크플로우를 쓰는데 AI가 main을 오염시키면 곤란하다.
이 절은 v1의 모든 구현 결정 위에 깔린 출발점이다. 처음에는 "특정 단어를 승인 신호로 본다"는 자연어 규칙 기반 접근을 검토했지만, 그것이 본질적으로 깨지는 방식이라는 인식에서 v1의 방향이 정해졌다.
초기 검토안: 사용자가 채팅에 진행해, approve, ㅇㅋ, 좋아 같은 미리 정한 문구를 보내면 hook이 그 문구를 보고 승인 토큰을 발행한다.
이 방식은 하드코딩된 규칙이다. 그리고 하드코딩은 다음 시나리오에서 즉시 무너진다.
진행이 들어가면 잘못 승인됨.approve가 포함된 경우, 사용자가 그걸 인용하면 의도와 무관하게 승인으로 처리됨.자연어 표면을 보고 의미를 추론하는 규칙은 본질적으로 깨진다.
위 한계를 인정하면 결론은 명확하다. "승인 의도"를 사용자 텍스트에서 찾지 말고, 행동의 결과인 모델의 tool_use에서 찾자.
Edit, Write, Bash 같은 도구 호출을 생성해야 코드를 바꿀 수 있음."*" 등록).ExitPlanMode의 결과에서 받는다."*"로 등록 — AI가 어떤 도구를 호출하든 무조건 hook 경유.v1은 화이트리스트를 두지 않는다. 화이트리스트로 가면 새 도구가 나올 때마다 패치해야 하는 지옥에 빠진다.
| 카테고리 | 도구 | 정책 |
|---|---|---|
| 변경 도구 | Edit, Write, MultiEdit, NotebookEdit | 보호 브랜치 아니고 토큰 있으면 통과 |
| Bash | Bash | 명령어 분류 후 분기 (2.3 참조) |
| 그 외 모두 | Read, Glob, Grep, Agent, Skill 등 | 기본 통과 |
config/blacklist.txt에 카테고리별 정규식.
>, >>, tee, heredocgit status && rm important.py 같은 명령은 첫 단어만 보면 git status라 통과되어버린다. 해결: ;, &&, ||, | 기준으로 segment 분해 후 각 segment를 따로 매칭.
bash -c "rm file" 한 줄로 블랙리스트가 무력화될 수 있다. 정적 분석 불가능 → 포함 자체를 차단.
. file$(...) 명령 치환, 백틱 명령 치환... | bash, ... | sh, ... | zshEnterPlanMode() 호출 → 통과ExitPlanMode(plan="...") 호출 → Claude Code가 승인 다이얼로그 표시만약 AI가 정책 무시하고 곧장 Edit 시도하면 → PreToolUse가 차단 → AI가 회복 경로로 EnterPlanMode부터 다시 시작. 정책 주입(소프트)과 PreToolUse 차단(하드)이 이중 안전망.
hook은 어떤 사용자 텍스트도 해석하지 않는다. 사용자가 한국어, 영어, 러시아어, 이모지로 답하든 영향 없음.
차단만으로는 충분하지 않다. AI는 차단된 후 회복 경로를 모르거나, 채팅에서 명료화 질문부터 던지면서 plan mode를 한참 우회한다. 그래서 매 턴 모델에게 정책을 주입하는 소프트 유도 채널을 둔다.
UserPromptSubmit hook이 stdout으로 출력한 텍스트는 Claude Code가 모델 컨텍스트에 추가 주입한다.
EnterPlanModeOMC_SKIP_HOOKS 등)는 존재하지 않으니 제안 금지git checkout -b feature/<설명>을 포함ExitPlanMode 다이얼로그가 plan 본문을 충분히 표시하지 않는 환경에서 사용자는 plan을 못 보고 Accept하는 위험이 있다. 그래서 정책에 다음을 명시한다.
ExitPlanMode 호출 직전에 plan 전문을 채팅 텍스트로 먼저 출력하라.
시나리오:
1. "@@ 기능 구현해줘" → plan → 승인 → 구현 → 턴 종료 (토큰 삭제)
2. "@@ 코드 리뷰해줘" → 새 턴, 토큰 없음. Read만 사용하므로 영향 없음
3. "$$ 다른 기능 구현해줘" → 새 턴, 차단 → 새 plan → 새 토큰 → 구현
v1 초반엔 사용자가 plan을 승인해도 Claude Code가 매 Bash 명령마다 "Do you want to proceed?" 다이얼로그를 띄웠다. 댓글 도메인 삭제 같은 작업은 Bash 호출이 30개 넘게 줄지어 나오기 때문에 사용자가 한 번에 끝까지 갈 수 없었다.
원인은 Claude Code 안에 권한 시스템이 두 겹이라는 것.
문제는 2번이 1번의 결과를 모른다.
PreToolUse hook이 stdout으로 다음 JSON을 출력하면 Claude Code가 네이티브 프롬프트를 건너뛴다.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Harness: token_present"
}
}
토큰 있을 때만 이 JSON을 출력하므로 안전망은 유지된다.
"test_v16_dir 만들고 hello.txt 쓰고 rm으로 지워줘" 한 줄 요청 → plan 한 번 승인 후 mkdir → Write → rm 세 작업이 사용자 추가 입력 없이 연속 실행됨.
코드 변경을 main / master / develop 같은 보호 브랜치에 직접 하면 안 되는 게 일반적인 git 워크플로우 규칙이다. 하지만 AI는 그걸 모르고 main에서 직접 commit/push한다.
protected_branches (기본값: main, master, develop)에 있는 브랜치에서:
git checkout -b / git switch -c → 항상 통과사용자: "X 기능 추가해줘" (현재 main 브랜치)
|
v
AI: EnterPlanMode → plan 작성
- 첫 단계: git checkout -b feature/add-x ← 정책이 강제
- 그 다음: 실제 코드 변경
|
v
사용자: Accept
|
v
AI: Bash(git checkout -b feature/add-x) → 통과 (브랜치 생성은 safe)
|
v
이제 feature 브랜치 위
|
v
AI: Edit / Write → 통과 (토큰 + 보호 브랜치 벗어남)
만약 AI가 정책 무시하고 main에서 곧장 Edit 시도하면 → hook이 차단 메시지로 회복 경로 안내 → AI가 메시지 보고 브랜치 만들고 재시도.
main에 직접 commit 못 하니 로컬 main은 항상 origin/main의 ancestor다. 이 보장 덕에 자동 동기화의 sync 로직이 단순해진다.
매번 Claude Code 켤 때 사용자가 git pull 잊으면 옛날 main 위에서 작업하다 충돌난다. 이걸 SessionStart hook으로 자동화.
Claude Code 세션 시작 시 자동으로:
git fetch origin main (timeout 10초)git merge --ff-only origin/main, 다른 브랜치면 git branch -f main origin/main보호 브랜치 정책 덕에 두 케이스 모두 항상 안전.
Claude Code 2.1.100은 SessionStart hook의 stdout을 채팅에 표시하지 않는다 (모델 컨텍스트로만 주입). 대신 events.jsonl의 session_start_sync 이벤트 또는 git log -1 main vs git log -1 origin/main 비교로 검증.
auto_sync_main: falsev1 운영 중 발견된 결정적 버그. PowerShell 5.x와 python script.py로 실행되는 hook 프로세스의 sys.stdout/sys.stderr는 기본적으로 시스템 코드페이지(한국어 Windows에서는 cp949)로 설정된다. 정책 텍스트에 들어있는 ★, ↓, em-dash(—), 한글 같은 cp949에 없는 문자를 출력하면 UnicodeEncodeError가 발생한다.
문제는 hook의 try/except가 이 예외를 조용히 삼키고 exit 0을 반환한다는 것. 결과: stdout 0 바이트, stderr 0 바이트, exit 0. 정책 텍스트는 모델에 도달하지 않는다.
모든 hook 스크립트 상단에 다음을 둔다.
import io
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
try:
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer, encoding="utf-8",
errors="replace", line_buffering=True,
)
except Exception:
pass
try:
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
try:
sys.stderr = io.TextIOWrapper(
sys.stderr.buffer, encoding="utf-8",
errors="replace", line_buffering=True,
)
except Exception:
pass
install 스크립트(.ps1) 자체도 PowerShell 5.x가 BOM 없는 UTF-8을 cp949로 잘못 읽는 문제가 있다. UTF-8 BOM으로 저장 + 스크립트 상단에서 [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() + chcp 65001 실행.
~/.harness/logs/
├── YYYY-MM-DD/
│ └── <session_id>/
│ ├── events.jsonl # 모든 이벤트(타임스탬프)
│ ├── conversation.md # 사람이 읽는 대화 + plan + diff 요약
│ ├── changes.json # {파일경로: [변경 시각, hash, 줄수 변화]}
│ └── diffs/<n>.patch # PostToolUse 에서 떨어지는 diff
└── index.md # 세션 인덱스
핵심은 index.md다. "지난주에 auth 관련 뭘 건드렸지?" 같은 추적이 빠르려면 세션별 한 줄 요약이 한 파일에 모여 있어야 한다.
각 줄은 ISO 8601 타임스탬프, session_id, event_type, payload를 포함한 단일 JSON 객체.
{"ts":"2026-06-03T14:21:30+09:00","type":"user_prompt","content":"회원가입 validation 고쳐줘"}
{"ts":"2026-06-03T14:21:45+09:00","type":"tool_use_pre","tool":"Read","decision":"allow"}
{"ts":"2026-06-03T14:22:05+09:00","type":"plan_approved","plan_summary":"..."}
{"ts":"2026-06-03T14:22:10+09:00","type":"tool_use_pre","tool":"Edit","decision":"allow","reason":"token_present"}
{"ts":"2026-06-03T14:22:30+09:00","type":"turn_end","tokens_invalidated":1}
각 섹션: 시각 + 이벤트 종류 + 본문. 사용자 요청, 계획 승인, 파일 변경, 턴 종료가 시간순으로 정리됨.
- 2026-06-03 14:21 / 회원가입 validation 수정 / auth/validators.py, tests/test_auth.py / 상세 링크
~/.harness/
├── lib/
│ ├── paths.py
│ ├── logger.py
│ ├── token.py
│ ├── bash_classifier.py
│ └── git_utils.py
├── hooks/
│ ├── _bootstrap.py
│ ├── session_start.py
│ ├── user_prompt_submit.py
│ ├── pre_tool_use.py
│ ├── post_tool_use.py
│ ├── stop.py
│ └── session_end.py
├── config/
│ ├── blacklist.txt
│ └── settings.json
├── state/
│ └── <session_id>/
└── logs/
├── index.md
└── YYYY-MM-DD/<session_id>/
| Hook | 받는 입력 | 역할 |
|---|---|---|
| SessionStart | session_id, cwd | git fetch + 로컬 main 동기화 |
| UserPromptSubmit | 사용자 메시지 | 요청 기록 + stdout으로 정책 텍스트 출력 |
| PreToolUse | tool_name, tool_input, cwd | 차단/통과 결정. 토큰 있을 때 JSON permissionDecision: allow 출력 |
| PostToolUse | tool_name, tool_input, tool_response | 결과 기록. ExitPlanMode Accept면 토큰 발행. Edit/Write 후 diff 캡처 |
| Stop | session_id | 토큰 삭제, turn_end 이벤트 기록 |
| SessionEnd | session_id | 세션 요약, index.md 한 줄 추가 |
state/<session_id>/approved 파일 하나로 단순화. 파일 존재 = 승인됨.
{
"plan_id": "uuid",
"approved_at": "2026-06-03T14:22:05+09:00",
"plan_summary": "회원가입 validation 수정",
"expected_files": []
}
git checkout -b feature/add-x 포함"지난주 auth 관련 뭘 건드렸지?"
~/.harness/logs/index.md 열기| 시나리오 | 결과 |
|---|---|
| Read/Glob/Grep 통과 | ✅ |
| Edit 토큰 없이 차단 | ✅ |
| Bash ls -la 통과 | ✅ |
| Bash rm 차단 | ✅ |
| Bash bash -c 차단 | ✅ |
| Bash 체이닝 분해 차단 | ✅ |
| ExitPlanMode Accept → 토큰 발행 | ✅ |
| Stop hook → 토큰 폐기 | ✅ |
| Edit 후 diff 캡처 | ✅ |
| main + Edit (토큰X) → 차단 | ✅ |
| main + Edit (토큰O) → 차단 (브랜치가 토큰보다 상위) | ✅ |
| main + git checkout -b → 통과 (safe Bash) | ✅ |
| feature 브랜치 + Edit (토큰O) → 통과 | ✅ |
| develop + Edit (토큰O) → 차단 | ✅ |
| SessionStart 자동 동기화 | ✅ |
"댓글 기능 삭제해줘" — 정책 + plan 강제
| 단계 | 결과 |
|---|---|
| 정책 주입 stdout 출력 | ✅ |
| AI EnterPlanMode 호출 | ✅ |
| 채팅에 plan 4섹션 출력 | ✅ |
| ExitPlanMode 다이얼로그 | ✅ |
"test_v16_dir 만들고 hello.txt 쓰고 rm으로 지워줘" — 권한 통합
| 단계 | 결과 |
|---|---|
| PreToolUse JSON allow 출력 | ✅ |
| plan에 mkdir/Write/rm 구체 명시 | ✅ |
| Accept 후 세 작업 연속 실행 (추가 프롬프트 X) | ✅ |
"test_branch_protection_v17.txt 만들어줘" (main 브랜치) — 보호 브랜치
| 단계 | 결과 |
|---|---|
| AI가 main이 보호 브랜치임을 인지 | ✅ |
| plan 첫 단계 = git checkout -b ... | ✅ |
| Accept 후 브랜치 생성 + 파일 생성 연속 실행 | ✅ |
| 파일이 main이 아닌 새 브랜치에 생성 | ✅ |
~/.harness/config/blacklist.txtprotected_branchesauto_sync_main: falseblock_message_template~/.harness/hooks/user_prompt_submit.py의 POLICY_TEXTmutating_toolsv1을 실사용에 투입하면서 드러난 이슈와 그에 대응한 패치들.
always_allowed_tools에 안전 도구 리스트를 두고 그 외는 차단했는데, Claude Code가 추가한 새 도구(Agent / ToolSearch / Skill)가 막혀서 매번 리스트에 추가해야 했음. → v1.3에서 화이트리스트 제거.OMC_SKIP_HOOKS=pre_tool_use 같은 존재하지 않는 환경변수를 추천. 정책에 명시.permissionDecision: allow 출력해 Claude Code 네이티브 프롬프트 우회.git checkout -b feature/<설명> 포함.[확인 필요] 마커)| 요소 | 결정 |
|---|---|
| 강제 방식 | 자연어 매칭이 아니라 모델 tool_use 단에서 강제 |
| 차단 게이트 | PreToolUse 한 곳, 매처 "*", 순수 블랙리스트 |
| Bash 우회 방지 | 명령 체이닝 분해 + 동적 실행 키워드 자체 차단 |
| 승인 채널 | EnterPlanMode → ExitPlanMode 네이티브 다이얼로그 |
| 정책 주입 | UserPromptSubmit hook stdout → 모델 컨텍스트 |
| Plan 가시성 | What will change / WARNING / Risks / Rollback 4섹션 강제 |
| 토큰 수명 | 한 턴 한정 |
| 권한 통합 | PreToolUse JSON permissionDecision: allow → Claude Code 네이티브 프롬프트 우회 |
| 보호 브랜치 | main/master/develop에서 mutating 시도 차단 (토큰 무관) |
| 자동 동기화 | SessionStart에서 git fetch + 로컬 main 동기화 |
| 로그 | events.jsonl + conversation.md + changes.json + diffs/ + index.md |
| 하네스 위치 | 전역 ~/.harness/ |
| 인코딩 | 모든 hook이 UTF-8 reconfigure |
| 이중 안전망 | 하드(차단) + 소프트(정책 주입) |
이 설계의 핵심은 신호 위치 이동이다. 사용자의 자연어 텍스트가 아니라 모델의 tool_use에서 신호를 받고, 승인도 자연어가 아니라 ExitPlanMode 네이티브 다이얼로그에서 받는다.
v1 최종본은 일곱 차례 패치(v1.1~v1.7)를 거치면서 다음을 학습했다.
v2는 이 기반 위에서 시작한다.

정책 없는 상태에서 AI에게 작업 시키면 분석 없이 바로 명령을 마구 실행한다.


하네스가 위험한 Bash 명령(rm -rf ...)을 PreToolUse hook에서 잡고, AI에게 "plan mode로 진입하라"는 메시지를 돌려준다.

AI가 차단 메시지 또는 정책 주입을 받고 EnterPlanMode로 진입해 plan을 작성한다.




plan 승인 후 Bash + Write + Bash 연속 작업이 사용자 추가 입력 없이 흘러간다. plan 한 번 본 게 곧 전체 위임이라는 게 v1.6 의 핵심.



main 브랜치 상태에서 작업 요청 시 AI 가 알아서 plan 첫 단계로 git checkout -b feature/... 를 넣는다. Accept 후 브랜치 생성과 파일 작업이 한 번에 흘러간다.


Claude Code 세션 시작 시 SessionStart hook 이 자동으로 git fetch + 로컬 main 업데이트. PowerShell 에서 git log -1 main 과 git log -1 origin/main 의 해시가 같은 게 동기화 증거.