하네스 설계 (+ omc)

김영준·2026년 6월 3일

ClaudeCode

목록 보기
7/8

개인 하네스 설계


1. 개념

1.1 배경 및 전체 구조

나는 Claude Code를 단순한 코드 생성 도구가 아니라, OMC(oh-my-claudecode)와 결합된 AI 개발 워크플로우의 일부로 사용한다.

  • Claude Code: 코드 읽기, 파일 수정, 명령 실행, hook, transcript 등 AI 개발 런타임 제공
  • OMC: 위 런타임 위에서 planner, architect, executor, reviewer, verifier 같은 역할별 agent와 /plan, /team, /ralph, /ultraqa 같은 workflow를 제공하는 multi-agent orchestration layer
  • 개인 하네스: OMC가 강제하지 않는 계획 승인 / 추적 가능한 로그 / 안전한 git 워크플로우를 강제로 보장하는 제어 계층

전체 흐름은 다음과 같다.

요청 → Claude Code → OMC → 개인 하네스 → 서버 구현 결과

1.2 Claude Code 동작 메커니즘

하네스 설계의 모든 결정은 Claude Code의 실제 동작 메커니즘을 정확히 이해한 위에서 내려졌다.

1.2.1 모델 응답의 구성

Claude(모델)는 매 턴마다 다음을 입력으로 받는다.

  • 시스템 프롬프트
  • 지금까지의 대화 전체
  • 사용 가능한 도구 목록(스키마 포함)
  • 사용자의 새 메시지

모델은 이걸 보고 응답 스트림을 만든다. 응답은 두 종류 블록으로 구성된다.

  • text 블록: 채팅에 보이는 글
  • tool_use 블록: "Edit 도구를 이런 인자로 호출해 달라"는 함수 호출

1.2.2 도구 호출 흐름

모델이 Edit(file_path=..., old_string=..., new_string=...) 같은 tool_use를 내뱉으면, Claude Code 런타임이 가로채서 다음 순서로 처리한다.

  1. PreToolUse hook 실행 — JSON으로 {tool_name, tool_input, session_id, transcript_path, cwd} 전달
    • exit 0 → 통과
    • exit 2 + stderr 메시지 → 차단 + 그 메시지를 모델에게 tool_result로 돌려줌
    • JSON 출력으로 입력 수정/우회/권한 결정 가능
  2. 통과되면 실제 도구 실행
  3. PostToolUse hook 실행tool_response까지 포함된 JSON 받음
  4. 결과를 모델에게 tool_result로 반환

tool_use가 없는 순수 text 응답이 나오면 턴 종료. 그 시점에 Stop hook 실행.

또한 사용자 입력이 들어올 때마다 UserPromptSubmit hook이 실행된다. 이 hook이 stdout으로 출력한 내용은 모델 컨텍스트에 추가 주입된다. 매 턴 정책 텍스트를 모델에 주입하는 채널.

세션이 시작될 때는 SessionStart hook이 실행된다. Claude Code를 새로 켤 때 자동으로 git 동기화 같은 초기 작업을 수행하는 채널.

1.2.3 주요 도구 분류

도구하는 일위험도
Read파일 읽기안전
Glob파일 경로 패턴 검색안전
Grep파일 내용 검색안전
Edit파일 일부 수정위험
Write파일 통째 쓰기위험
MultiEdit한 파일 여러 군데 수정위험
NotebookEdit주피터 노트북 셀 수정위험
Bash셸 명령 실행가변
WebFetch / WebSearch외부 자료 가져오기안전
Task / Agent서브에이전트 호출가변
Skill / ToolSearch메타 (스킬 호출, 도구 검색)안전
TodoWrite자기 할 일 관리안전

1.2.4 Plan Mode: EnterPlanMode 와 ExitPlanMode

Claude Code에는 빌트인 plan mode가 있다. plan mode는 두 개의 도구로 제어된다.

  • EnterPlanMode: 모델이 plan mode로 진입할 때 호출. 인자 없음. 호출 후 모델은 read-only 도구와 ExitPlanMode 외에는 호출하지 못한다.
  • ExitPlanMode(plan="..."): 모델이 계획을 인자로 넣어 호출하면, Claude Code UI가 승인 다이얼로그를 띄운다.
    • Accept → plan mode 해제, Edit/Write 등 가능
    • Reject → plan mode 유지

이 두 도구는 자연어 분류 없이 명시적으로 승인을 받을 수 있는 네이티브 채널이다.

1.3 문제 인식

OMC와 Claude Code 기본 기능만으로는 다음 여섯 가지가 보장되지 않는다.

1.3.1 계획 단계의 비강제성

OMC의 /plan은 호출했을 때만 실행된다. "AI에게 코드 변경 전 무조건 계획을 세우라"는 게이트는 없다. 팀원이 "@@ 기능 구현해줘"라고 한 줄 던지면 AI가 곧바로 코드를 수정한다.

1.3.2 도구 호출 우회 가능성 (Bash)

Edit/Write만 막아도 모델은 Bash로 우회해 파일을 변경할 수 있다.

echo 'new content' > auth.py
sed -i 's/old/new/g' *.py
cat > config.py << EOF ... EOF

따라서 Bash도 함께 통제해야 코드 수정 차단이 의미 있다.

1.3.3 자연어 승인 신호의 한계

"진행해", "ㅇㅋ", "approve" 같은 자연어를 승인 신호로 쓰면 다음 문제가 생긴다.

  • 언어 의존성 (다른 언어 사용 시 깨짐)
  • 오탐 (의도와 다른 문장 매칭)
  • AI 인젝션 위험 (AI 출력 텍스트에 "approve" 포함 가능)
  • 모호성 ("좋아 그렇게 해" 같은 판단 불가 표현)

/approve 같은 슬래시 커맨드는 명시적이지만 그걸 자발적으로 칠 사람은 어차피 계획 세울 사람이다. 정작 막아야 할 대상은 "@@해줘" 한 줄 던지고 결과만 기다리는 습관이라, 이 사람은 /approve를 안 친다.

1.3.4 로그의 추적성 부족

OMC의 .omc/sessions, .omc/state는 agent lifecycle 중심이라 "내가 언제 뭘 시켰고 어떤 파일이 바뀌었나"를 시간순으로 빠르게 보기 어렵다. Claude Code의 transcript_path는 모든 정보가 들어있지만 사람이 읽기엔 부담스럽다.

1.3.5 권한 시스템 이중 프롬프트

Claude Code는 Bash 같은 위험 도구에 매번 별도의 권한 다이얼로그를 띄운다. 우리가 plan을 승인했어도 Claude Code의 built-in 권한 시스템은 그걸 모르고 또 묻는다. plan 한 번 승인했는데 작업 30개 중 매번 "Do you want to proceed?"가 뜨면 plan 승인의 의미가 깎인다.

1.3.6 main 브랜치 직접 변경

git workflow에서 main에 직접 commit/push는 금기다. 하지만 AI는 그 규칙을 모르고 그냥 main에서 작업한다. 사용자가 PR 워크플로우를 쓰는데 AI가 main을 오염시키면 곤란하다.

1.4 핵심 설계 원칙

  1. 신호는 사용자 텍스트가 아니라 모델의 tool_use에서 받는다. 자연어 분류는 깨진다.
  2. 게이트는 PreToolUse hook 한 곳에 집중한다.
  3. 차단 정책은 블랙리스트로 간다.
  4. 승인은 Claude Code의 네이티브 메커니즘(EnterPlanMode + ExitPlanMode)을 활용한다.
  5. 승인 토큰은 한 턴에만 유효하다.
  6. 로그는 두 형태로 동시에 남긴다. JSONL과 사람이 읽기 좋은 마크다운.
  7. 하드 게이트(차단)와 소프트 유도(정책 주입)를 결합한다.
  8. 사용자에게 보여줄 plan은 채팅에 직접 출력하도록 강제한다.
  9. 하나의 plan 승인 = 턴 전체의 실행 위임. 토큰 발행 후엔 Claude Code의 네이티브 권한 프롬프트까지 우회한다.
  10. 보호 브랜치 정책은 토큰보다 상위 게이트다. main / master / develop에선 토큰이 있어도 코드 변경을 차단한다.

2. v1 구현

2.1 설계 결정의 출발점: 하드코딩이 아닌 강제

이 절은 v1의 모든 구현 결정 위에 깔린 출발점이다. 처음에는 "특정 단어를 승인 신호로 본다"는 자연어 규칙 기반 접근을 검토했지만, 그것이 본질적으로 깨지는 방식이라는 인식에서 v1의 방향이 정해졌다.

2.1.1 자연어 키워드 매칭의 한계

초기 검토안: 사용자가 채팅에 진행해, approve, ㅇㅋ, 좋아 같은 미리 정한 문구를 보내면 hook이 그 문구를 보고 승인 토큰을 발행한다.

이 방식은 하드코딩된 규칙이다. 그리고 하드코딩은 다음 시나리오에서 즉시 무너진다.

  • 언어 전환: 작업자가 갑자기 러시아어, 히브리어로 답하면 키워드 리스트에 없으니 승인 안 됨.
  • 변이형: "ㅇㅋㅋ", "ㄱㄱ", "Sure" 등 — 모든 표현을 다 잡으려면 리스트가 폭발함.
  • 오탐: "진행하기 전에 한 번 더 확인해줘"처럼 의도와 다른 문장에 진행이 들어가면 잘못 승인됨.
  • AI 인젝션: 모델이 출력한 텍스트에 approve가 포함된 경우, 사용자가 그걸 인용하면 의도와 무관하게 승인으로 처리됨.

자연어 표면을 보고 의미를 추론하는 규칙은 본질적으로 깨진다.

2.1.2 신호 위치 이동: 사용자 텍스트 → 모델의 tool_use

위 한계를 인정하면 결론은 명확하다. "승인 의도"를 사용자 텍스트에서 찾지 말고, 행동의 결과인 모델의 tool_use에서 찾자.

  • 사용자가 어떤 언어로 말하든 → AI는 결국 Edit, Write, Bash 같은 도구 호출을 생성해야 코드를 바꿀 수 있음.
  • 그 도구 호출은 구조화된 신호다. 언어 의존성 0, 변이형 0, 오탐 0.

2.1.3 결론: 도구 호출 단에서 강제

  • PreToolUse hook이 모든 tool_use를 가로챈다 (matcher "*" 등록).
  • hook은 자연어를 절대 해석하지 않는다. 도구 이름, 입력, 토큰 파일, 현재 git 브랜치만 본다.
  • 승인 신호도 ExitPlanMode의 결과에서 받는다.
  • 토큰은 한 턴 동안만 유효하고 턴이 끝나면 자동 폐기된다.

2.2 차단 게이트

2.2.1 PreToolUse hook 단일 게이트

  • 매처는 "*"로 등록 — AI가 어떤 도구를 호출하든 무조건 hook 경유.
  • 차단 시 exit 2 + stderr 메시지 → 모델에게 차단 사유와 다음 행동을 알림.

2.2.2 차단 대상 분류 — 순수 블랙리스트

v1은 화이트리스트를 두지 않는다. 화이트리스트로 가면 새 도구가 나올 때마다 패치해야 하는 지옥에 빠진다.

카테고리도구정책
변경 도구Edit, Write, MultiEdit, NotebookEdit보호 브랜치 아니고 토큰 있으면 통과
BashBash명령어 분류 후 분기 (2.3 참조)
그 외 모두Read, Glob, Grep, Agent, Skill 등기본 통과

2.3 Bash 차단 정책 (블랙리스트)

2.3.1 카테고리별 차단 패턴

config/blacklist.txt에 카테고리별 정규식.

  • 파일 시스템 변경: rm, rmdir, unlink, mv, cp, mkdir, touch, chmod, chown, ln
  • 출력 리다이렉션 (Edit/Write 우회로): >, >>, tee, heredoc
  • 인플레이스 편집기: sed -i, perl -i, gawk -i, ed, ex
  • Git 변경: git commit, git push, git reset --hard, git checkout -B, git checkout ., git clean -f, git rebase, git merge, git branch -D, git tag -d, git stash drop/clear, git filter-branch, git update-ref
  • 패키지 설치/제거: npm, yarn, pnpm, pip, poetry, cargo, go install, apt, brew, gem
  • DB / 마이그레이션: alembic, prisma migrate, knex migrate, manage.py migrate, psql/mysql -c 안에 INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/TRUNCATE
  • 시스템 파괴 / 권한 상승: sudo, su -, dd, mkfs, shred, format
  • 환경 변수 영구 변경: export, unset

2.3.2 우회 방지: 명령 체이닝 분해

git status && rm important.py 같은 명령은 첫 단어만 보면 git status라 통과되어버린다. 해결: ;, &&, ||, | 기준으로 segment 분해 후 각 segment를 따로 매칭.

2.3.3 우회 방지: 동적 실행 패턴 차단

bash -c "rm file" 한 줄로 블랙리스트가 무력화될 수 있다. 정적 분석 불가능 → 포함 자체를 차단.

  • eval, exec, source, . file
  • bash -c, sh -c, zsh -c
  • $(...) 명령 치환, 백틱 명령 치환
  • ... | bash, ... | sh, ... | zsh

2.4 승인 채널: EnterPlanMode + ExitPlanMode

2.4.1 흐름

  1. 사용자가 변경 의도를 담은 요청을 보냄 (언어 무관)
  2. UserPromptSubmit hook이 정책 텍스트를 stdout으로 출력 → 모델 컨텍스트 주입
  3. AI가 정책을 보고 EnterPlanMode() 호출 → 통과
  4. plan mode 안에서 Read/Glob/Grep으로 분석
  5. AI가 plan 본문을 채팅 텍스트로 출력
  6. AI가 ExitPlanMode(plan="...") 호출 → Claude Code가 승인 다이얼로그 표시
  7. 사용자가 plan을 채팅에서 읽고 Accept
  8. PostToolUse hook이 토큰 발행
  9. AI가 Edit/Write/Bash 실행 → 토큰 확인 → 통과
  10. Stop hook이 토큰 삭제

만약 AI가 정책 무시하고 곧장 Edit 시도하면 → PreToolUse가 차단 → AI가 회복 경로로 EnterPlanMode부터 다시 시작. 정책 주입(소프트)과 PreToolUse 차단(하드)이 이중 안전망.

2.4.2 자연어 분류 제거의 결과

hook은 어떤 사용자 텍스트도 해석하지 않는다. 사용자가 한국어, 영어, 러시아어, 이모지로 답하든 영향 없음.

2.5 정책 주입 (UserPromptSubmit hook)

차단만으로는 충분하지 않다. AI는 차단된 후 회복 경로를 모르거나, 채팅에서 명료화 질문부터 던지면서 plan mode를 한참 우회한다. 그래서 매 턴 모델에게 정책을 주입하는 소프트 유도 채널을 둔다.

2.5.1 메커니즘

UserPromptSubmit hook이 stdout으로 출력한 텍스트는 Claude Code가 모델 컨텍스트에 추가 주입한다.

2.5.2 정책 내용

  • 코드 수정 요청이면 첫 도구 호출이 반드시 EnterPlanMode
  • ExitPlanMode 호출 전 plan 본문을 채팅 텍스트로 출력 (구조 강제)
  • 사용자가 다이얼로그를 Accept하면 그 후 모든 도구 호출이 추가 질문 없이 통과
  • 채팅에서 명료화 질문 던지지 말고 plan 본문에 포함
  • 가짜 환경변수(OMC_SKIP_HOOKS 등)는 존재하지 않으니 제안 금지
  • 보호 브랜치(main/master/develop)에선 plan 첫 단계로 git checkout -b feature/<설명>을 포함
  • 막혔을 때 해답은 항상 EnterPlanMode

2.5.3 하드/소프트 이중 안전망

  • 하드 (PreToolUse 차단): AI가 정책을 무시해도 변경 시도가 막힌다.
  • 소프트 (UserPromptSubmit 정책 주입): AI가 차단 만나기 전에 능동적으로 plan mode로 들어가게 유도한다.

2.6 Plan 가시성 강제

ExitPlanMode 다이얼로그가 plan 본문을 충분히 표시하지 않는 환경에서 사용자는 plan을 못 보고 Accept하는 위험이 있다. 그래서 정책에 다음을 명시한다.

ExitPlanMode 호출 직전에 plan 전문을 채팅 텍스트로 먼저 출력하라.

2.6.1 plan 본문 구조 (강제)

  • ## What will change — 파일별 정확한 라인/메서드/import 단위 + Bash 명령 그대로 + SQL 본문 그대로
  • ## WARNING: DESTRUCTIVE — rm/delete/drop/truncate 같은 파괴적 작업
  • ## Risks — 구체적 위험
  • ## Rollback — 구체적 복구 명령

2.6.2 효과

  • 사용자가 다이얼로그가 뜨기 전에 plan을 읽을 수 있다
  • DESTRUCTIVE 섹션이 위험 작업을 한눈에 보여준다
  • Rollback 전략이 있어 문제가 생겨도 대응 가능

2.7 토큰 생명주기: 턴 단위

  • 생성: ExitPlanMode Accept 순간 (PostToolUse hook)
  • 만료: Stop hook 발사 시점 (AI가 응답 끝낼 때마다)
  • 세션 종료 시 SessionEnd hook이 이중 폐기

시나리오:
1. "@@ 기능 구현해줘" → plan → 승인 → 구현 → 턴 종료 (토큰 삭제)
2. "@@ 코드 리뷰해줘" → 새 턴, 토큰 없음. Read만 사용하므로 영향 없음
3. "$$ 다른 기능 구현해줘" → 새 턴, 차단 → 새 plan → 새 토큰 → 구현

2.8 권한 시스템 통합 (Claude Code 네이티브 프롬프트 우회)

v1 초반엔 사용자가 plan을 승인해도 Claude Code가 매 Bash 명령마다 "Do you want to proceed?" 다이얼로그를 띄웠다. 댓글 도메인 삭제 같은 작업은 Bash 호출이 30개 넘게 줄지어 나오기 때문에 사용자가 한 번에 끝까지 갈 수 없었다.

원인은 Claude Code 안에 권한 시스템이 두 겹이라는 것.

  1. PreToolUse hook (우리 하네스) — 토큰 발행 = 통과
  2. Claude Code built-in — 매 Bash 명령마다 별도 확인

문제는 2번이 1번의 결과를 모른다.

2.8.1 해결: PreToolUse JSON 출력

PreToolUse hook이 stdout으로 다음 JSON을 출력하면 Claude Code가 네이티브 프롬프트를 건너뛴다.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Harness: token_present"
  }
}

토큰 있을 때만 이 JSON을 출력하므로 안전망은 유지된다.

2.8.2 효과

"test_v16_dir 만들고 hello.txt 쓰고 rm으로 지워줘" 한 줄 요청 → plan 한 번 승인 후 mkdir → Write → rm 세 작업이 사용자 추가 입력 없이 연속 실행됨.

2.9 보호 브랜치 정책

코드 변경을 main / master / develop 같은 보호 브랜치에 직접 하면 안 되는 게 일반적인 git 워크플로우 규칙이다. 하지만 AI는 그걸 모르고 main에서 직접 commit/push한다.

2.9.1 규칙

protected_branches (기본값: main, master, develop)에 있는 브랜치에서:

  • Edit / Write / MultiEdit / NotebookEdit 시도 → 토큰 있어도 차단
  • 변경 카테고리 Bash → 토큰 있어도 차단
  • 단, git checkout -b / git switch -c항상 통과

2.9.2 흐름

사용자: "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가 메시지 보고 브랜치 만들고 재시도.

2.9.3 부수 효과: 자동 동기화 단순화

main에 직접 commit 못 하니 로컬 main은 항상 origin/main의 ancestor다. 이 보장 덕에 자동 동기화의 sync 로직이 단순해진다.

2.10 SessionStart 자동 동기화

매번 Claude Code 켤 때 사용자가 git pull 잊으면 옛날 main 위에서 작업하다 충돌난다. 이걸 SessionStart hook으로 자동화.

2.10.1 동작

Claude Code 세션 시작 시 자동으로:

  1. cwd가 git repo인지 확인 → 아니면 조용히 스킵
  2. origin remote / main 브랜치 자동 감지
  3. git fetch origin main (timeout 10초)
  4. 현재 main이면 git merge --ff-only origin/main, 다른 브랜치면 git branch -f main origin/main

보호 브랜치 정책 덕에 두 케이스 모두 항상 안전.

2.10.2 가시성 한계

Claude Code 2.1.100은 SessionStart hook의 stdout을 채팅에 표시하지 않는다 (모델 컨텍스트로만 주입). 대신 events.jsonl의 session_start_sync 이벤트 또는 git log -1 main vs git log -1 origin/main 비교로 검증.

2.10.3 안전 장치

  • network 실패 / detached HEAD / origin 없음 / non-git 폴더 → 모두 스킵
  • 사용자가 끄고 싶으면 auto_sync_main: false

2.11 인코딩 처리 (Windows)

v1 운영 중 발견된 결정적 버그. 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. 정책 텍스트는 모델에 도달하지 않는다.

2.11.1 해결

모든 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

2.11.2 install.ps1 인코딩

install 스크립트(.ps1) 자체도 PowerShell 5.x가 BOM 없는 UTF-8을 cp949로 잘못 읽는 문제가 있다. UTF-8 BOM으로 저장 + 스크립트 상단에서 [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() + chcp 65001 실행.

2.12 로그 구조

2.12.1 디렉토리 구조

~/.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 관련 뭘 건드렸지?" 같은 추적이 빠르려면 세션별 한 줄 요약이 한 파일에 모여 있어야 한다.

2.12.2 events.jsonl 스키마

각 줄은 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}

2.12.3 conversation.md 포맷

각 섹션: 시각 + 이벤트 종류 + 본문. 사용자 요청, 계획 승인, 파일 변경, 턴 종료가 시간순으로 정리됨.

2.12.4 index.md 한 줄 포맷

- 2026-06-03 14:21 / 회원가입 validation 수정 / auth/validators.py, tests/test_auth.py / 상세 링크

2.13 디렉토리 구조 (설치 후)

~/.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>/

2.14 Hook 별 책임

Hook받는 입력역할
SessionStartsession_id, cwdgit fetch + 로컬 main 동기화
UserPromptSubmit사용자 메시지요청 기록 + stdout으로 정책 텍스트 출력
PreToolUsetool_name, tool_input, cwd차단/통과 결정. 토큰 있을 때 JSON permissionDecision: allow 출력
PostToolUsetool_name, tool_input, tool_response결과 기록. ExitPlanMode Accept면 토큰 발행. Edit/Write 후 diff 캡처
Stopsession_id토큰 삭제, turn_end 이벤트 기록
SessionEndsession_id세션 요약, index.md 한 줄 추가

2.15 토큰 형식

state/<session_id>/approved 파일 하나로 단순화. 파일 존재 = 승인됨.

{
  "plan_id": "uuid",
  "approved_at": "2026-06-03T14:22:05+09:00",
  "plan_summary": "회원가입 validation 수정",
  "expected_files": []
}

2.16 사용 시나리오

2.16.1 일반 흐름 (소프트 유도)

  1. 사용자: "회원가입 validation 고쳐줘"
  2. UserPromptSubmit hook이 정책 주입
  3. AI: EnterPlanMode → 분석 → 채팅에 plan 출력 → ExitPlanMode → 다이얼로그
  4. 사용자: Accept → 토큰 발행
  5. AI 실행 → 추가 프롬프트 없이 통과
  6. Stop hook → 토큰 삭제
  7. SessionEnd → index.md 갱신

2.16.2 보호 브랜치 위 흐름

  1. 사용자: "X 기능 추가해줘" (현재 main)
  2. AI: plan 첫 단계로 git checkout -b feature/add-x 포함
  3. Accept → Bash(git checkout -b ...) → 새 브랜치
  4. Edit/Write 진행

2.16.3 하드 게이트 (정책 무시 시)

  1. AI가 곧장 Bash(rm -rf ...) 시도 → 차단
  2. AI가 메시지 보고 EnterPlanMode로 회복
  3. 이후 흐름은 2.16.1과 동일

2.16.4 추적 시나리오

"지난주 auth 관련 뭘 건드렸지?"

  1. ~/.harness/logs/index.md 열기
  2. grep auth로 검색
  3. 해당 세션 폴더의 conversation.md 확인
  4. diffs/에서 실제 변경 확인

2.17 검증 결과

2.17.1 격리 샌드박스 e2e 테스트

시나리오결과
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 자동 동기화

2.17.2 실사용 e2e 테스트 (Spring Boot 프로젝트)

"댓글 기능 삭제해줘" — 정책 + 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이 아닌 새 브랜치에 생성

2.18 운영 중 조정 포인트

  • 차단 패턴 추가/제거: ~/.harness/config/blacklist.txt
  • 보호 브랜치 추가/제거: settings.json의 protected_branches
  • 자동 동기화 끄기: auto_sync_main: false
  • 차단 메시지 문구: settings.json의 block_message_template
  • 정책 텍스트: ~/.harness/hooks/user_prompt_submit.pyPOLICY_TEXT
  • 변경 도구 목록: settings.json의 mutating_tools

2.19 v1 운영 중 발견한 이슈 (회고)

v1을 실사용에 투입하면서 드러난 이슈와 그에 대응한 패치들.

  • v1.1 — 도구 화이트리스트의 패치 지옥: 처음엔 always_allowed_tools에 안전 도구 리스트를 두고 그 외는 차단했는데, Claude Code가 추가한 새 도구(Agent / ToolSearch / Skill)가 막혀서 매번 리스트에 추가해야 했음. → v1.3에서 화이트리스트 제거.
  • v1.2 — EnterPlanMode vs ExitPlanMode 순환 블록: ExitPlanMode만 통과시키고 EnterPlanMode는 차단했더니, AI가 ExitPlanMode 호출하면 "not in plan mode" 에러 → EnterPlanMode 호출하면 하네스가 차단 → 무한 루프. → 두 도구 모두 기본 통과.
  • v1.2 — 가짜 환경변수 추천: AI가 막혔을 때 OMC_SKIP_HOOKS=pre_tool_use 같은 존재하지 않는 환경변수를 추천. 정책에 명시.
  • v1.4 — Plan 가시성 부재: ExitPlanMode 다이얼로그가 plan 본문을 안 보여주는 경우 사용자가 내용을 모르고 Accept하는 위험. → 정책에 "ExitPlanMode 호출 전에 plan 전문을 채팅에 출력하라" 추가.
  • v1.5 — Windows cp949 인코딩 사일런트 실패: 가장 결정적 버그. UserPromptSubmit hook의 stdout이 UnicodeEncodeError를 던지고 try/except가 삼켜서 정책이 모델에 한 번도 도달 안 함. → 모든 hook에 stdout/stderr UTF-8 reconfigure.
  • v1.6 — 권한 시스템 두 겹의 충돌: plan 승인했는데도 Claude Code가 매 Bash 명령마다 또 물었다. → PreToolUse가 JSON permissionDecision: allow 출력해 Claude Code 네이티브 프롬프트 우회.
  • v1.6 — plan 추상성: AI가 plan을 "comment 도메인 삭제" 수준의 한 줄로 제출. → POLICY_TEXT에 파일별 정확한 라인/메서드/명령/SQL까지 요구하는 구체적 포맷 명시.
  • v1.7 — main 보호 부재: AI가 main에서 직접 commit/push 시도. → 보호 브랜치 정책 도입. AI는 plan 첫 단계로 git checkout -b feature/<설명> 포함.
  • v1.7 — 매번 수동 git pull: SessionStart hook으로 자동 동기화. 보호 브랜치 덕에 sync 로직이 단순.
  • 사용자 UX 함정: 하네스가 plan을 강제해도 사용자가 plan을 안 읽고 Accept하면 의미 없음. 운영 수칙: "plan 본문 안 보이거나 부실하면 무조건 No".

2.20 v2 후보

  • 모호한 요청 시 명료화 질문 유도 강화 (plan에 [확인 필요] 마커)
  • Plan 범위 외 파일 변경 시 PostToolUse 경고/차단
  • 로그 보관 기간 자동 청소
  • 차단 통계 / 알림 대시보드
  • 다중 세션 동시 실행 시 글로벌 락
  • OMC planner agent와 우리 정책 흐름 통합
  • DESTRUCTIVE 작업 추가 확인 (키워드 타이핑)
  • 브랜치별 변경 가능 파일 제약 (feature/auth-* → src/auth/만)
  • Plan 첫 단계가 정말 브랜치 생성인지 PostToolUse가 검증
  • 현재 브랜치를 main 위로 자동 rebase 옵션

2.21 요약

요소결정
강제 방식자연어 매칭이 아니라 모델 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)를 거치면서 다음을 학습했다.

  • 화이트리스트는 패치 지옥을 만든다 → 블랙리스트로 통일
  • 하드 게이트만으로 부족하다 → 정책 주입 채널 추가
  • ExitPlanMode 다이얼로그를 신뢰할 수 없다 → 채팅에 plan을 따로 출력
  • Windows의 사일런트 인코딩 실패는 가장 잡기 어려운 버그다 → 인코딩 부트스트랩
  • 권한 시스템이 두 겹이면 plan 승인의 의미가 깎인다 → PreToolUse JSON으로 통합
  • AI는 git 워크플로우 규칙을 모른다 → 보호 브랜치 정책을 토큰보다 상위 게이트로

v2는 이 기반 위에서 시작한다.


3. 데모

<무분별 명령>

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

<무분별 삭제 막기>

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

<계획 세우기>

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

<한 번 승인하면 끝까지 위임>

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

<main 브랜치 보호>

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

<자동 동기화>

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

profile
개발의 신이 될거다

0개의 댓글