Skip to content

필사 모드: AI 에이전트 하네스 해부 — 규칙이 스스로 지켜지게 만든 35개 파일

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

모델이 아니라 주변이 결과를 정한다

같은 모델을 쓰는데도 어떤 저장소에서는 에이전트가 git stash 로 남의 편집을 날리고, 어떤 저장소에서는 그런 일이 나지 않는다. 차이는 모델이 아니라 모델을 둘러싼 것, 즉 하네스(harness) 에 있다. 규칙을 어디에 적는지, 어떤 명령을 막는지, 커밋이 어떤 관문을 지나는지, 세션이 끝나도 무엇이 남는지.

이 글은 LabHub 저장소의 하네스를 파일 단위로 뜯어 본 기록이다. 이 저장소는 2026년 8월 20일 첫 커밋 뒤 3주 동안 AI 에이전트가 사람과 함께 일했고, 그 결과가 숫자로 남아 있다.

항목출처
전체 커밋1,491git rev-list --count origin/main
AI 공동 저작 커밋497 (Opus 354 · Fable 136 · Codex 7)커밋 본문의 Co-Authored-By
운영 배포 커밋350 (개발계 295)deploy(prod): 접두사
최근 7일 사람·AI 커밋(배포 제외)3779월 4일~11일
단위 시험 수1,697 → 2,3469월 7일 10:47 → 9월 11일 09:06 게이트 로그
이 Mac 에서 돈 게이트161회9월 7일 이후 게이트 로그 파일 수

마지막 줄이 이 글의 주제다. 게이트 161회 중 이 작업 세션의 로그에서 세어 보면 push 성공 127회, 게이트가 막은 것 41회, origin 이 앞서 나가 커밋을 다시 얹은 것 23회다. 하네스는 문서가 아니라 하루에도 수십 번 도는 기계다.

원칙 하나: 규칙은 한 곳에, 장치는 따로

하네스 전체는 2026년 9월 7일 커밋 하나(83821fa0, 35개 파일, 1,746줄)로 들어왔다. 커밋 메시지가 왜 만들었는지를 말한다.

규칙은 AGENTS.md 한 곳에 있었지만, 그것을 지키는 장치는 세션마다
스크래치패드에 다시 만들고 있었다(push.sh·gate.sh·bumpdigest.py).
세션이 끝나면 사라지고, 다음 세션은 같은 사고를 한 번 더 겪은 뒤에야
같은 도구를 다시 만들었다.

그래서 구조의 첫 원칙은 규칙의 원본은 하나 라는 것이다. 규칙은 AGENTS.md(707줄) 한 곳에 있고, Claude Code 가 읽는 CLAUDE.md 는 그 파일을 가리키는 포인터다. CLAUDE.md 의 문장을 그대로 옮긴다.

같은 내용을 두 곳에 나누어 적어 두면 반드시 한쪽만 낡은 상태로 남기 때문입니다.

.claude/README.md.claude/ 아래에 무엇을 두는지를 이렇게 정한다.

규칙의 원본은 AGENTS.md 다. 여기에는 Claude Code 만 읽는 장치를 둔다 —
규칙을 다시 적는 것이 아니라, 규칙이 지켜지도록 자동으로 끼어드는 것들이다.

다섯 층과 두 가지 더

README 는 하네스를 다섯 층으로 나눈다. 실제 파일 수를 세어 붙였다.

무엇파일하는 일
영구 지침AGENTS.md, CLAUDE.md2모든 세션이 먼저 읽는 규칙과 사고 기록
경로 규칙.claude/rules/*.md6특정 경로를 건드릴 때만 붙는 지침
스킬.claude/skills/*/SKILL.md5세 번 이상 반복한 절차
.claude/hooks/*.py2도구 호출 전후에 끼어드는 검사
서브에이전트.claude/agents/*.md3읽기만 하는 조사·검토 역할
슬래시 명령.claude/commands/*.md4스킬로 들어가는 얇은 입구
플러그인settings.jsonenabledPlugins13공식 마켓플레이스의 도구

무엇을 어느 층에 넣는지의 기준도 README 에 적혀 있다. 훅에 대한 문장이 가장 분명하다.

훅에 넣는 기준은 하나다 — 실제로 일어났고, 눈으로는 알아채기 어려웠던 것.
일반적인 모범 사례는 넣지 않는다. 경고가 흔해지면 아무도 읽지 않는다.

스킬에 대해서는 "세 번 이상 반복한 절차" 를 넣되, "순서가 어긋나면 사고가 나는 부분은 본문이 아니라 scripts/agent/ 의 스크립트로 뺀다" 고 한다. 플러그인 13개 중 commit-commands 는 일부러 뺐다. /commit 이 작업 트리에서 git commit 을 치는데, 이 저장소에서는 그것이 금지이기 때문이다.

경로 규칙: 그 파일을 열 때만 나타나는 지침

rules/ 의 파일 6개는 각각 paths: 를 갖고, 그 경로를 건드릴 때만 문맥에 들어온다. 전체 규칙을 매번 읽히면 아무도 읽지 않는다는 같은 원리다.

  • app-js.mdbackend/static/app.js 는 파일 전체의 SHA-256 이 보안 검사 스크립트에 박혀 있어 "한 글자만 달라져도 배포가 막히고, 막히는 자리의 메시지는 엉뚱하다(streamSSE 관련 문구가 뜬다)".
  • pipeline.md — "push 하면 Jenkins 가 매분 폴링해 승인 없이 운영까지 올린다(약 20분). 배포는 GitOps 로만."
  • migrations.md — "이미 적용된 마이그레이션은 절대 고치지 않는다."
  • generated.md — 손으로 고치면 다음 생성 때 사라지는 파일과 그 생성기의 표.
  • curriculum.md — "채점기는 양방향으로 시험한다 — 정답이 통과하는 것만 보면 절반이다."
  • tests.md — "시험의 문서 문자열에 왜 이 시험이 있는지, 어떤 사고가 있었는지를 적는다."

시험이 이 규칙들을 감시한다. paths: 가 실제 존재하는 파일과 맞는지, 본문이 AGENTS.mdscripts/ 를 다시 가리키는지를 tests/test_claude_harness.py 가 확인한다.

훅: 막는 것 넷, 묻는 것 둘

hooks/guard.py 는 도구가 실행되기 전에 표준 입력으로 호출 내용을 받고, 표준 출력으로 denyask 를 돌려준다. 안에 든 것은 정확히 네 가지이고, 각각 실제 사고에서 왔다.

GIT_STATE = re.compile(
    r"\bgit\s+(?:stash|checkout|switch|restore|reset|rebase|merge|pull|clean)\b")
GIT_OK = re.compile(r"\bgit\s+(?:worktree|stash\s+list)\b")
KUBECTL_WRITE = re.compile(
    r"\bkubectl\b[^|;&]*\s(?:apply|delete|patch|scale|edit|replace|annotate|label|cordon|drain)\b")
LOCKED = "backend/static/app.js"
  1. 작업 트리를 바꾸는 git 명령은 deny. 주석에 이유가 있다: "git stash 한 번이 다른 사람 넷의 편집을 되돌렸다."
  2. 클러스터 상태를 바꾸는 kubectl 은 deny. 읽기와 임시 파드만 허용한다.
  3. push.sh 를 거치지 않는 git pushask. 메시지에 근거를 붙인다: "오늘만 그 게이트가 CI 실패를 네 번 막았습니다."
  4. app.js 편집은 ask. 다이제스트 갱신을 잊으면 배포가 조용히 막히기 때문이다.

hooks/after_edit.py 는 파일을 고친 직후 그 파일 하나만 본다. Python 은 ast.parse, JavaScript 는 node --check, JSON 은 json.loads. 전체 시험은 2분이 넘어 편집마다 돌릴 수 없으니 "지금 이 파일이 스스로 성립하는가" 까지만 잡고, 나머지는 게이트에 맡긴다.

훅의 위험은 README 의 한 문장에 있다: "훅은 조용히 죽는다 — 고친 뒤 시험을 돌리지 않으면 막으려던 사고가 그대로 난다." 그래서 tests/test_claude_hooks.py 13개가 훅에 JSON 을 직접 먹여 deny·ask·통과를 확인한다.

서브에이전트: 읽기만 하는 역할

agents/ 의 세 역할은 모두 쓰기 도구가 없다.

이름도구역할하지 않는 것
safe-researcherRead, Grep, Glob, Bash코드·문서·클러스터를 읽고 요약Edit/Write, git 상태 변경, kubectl 쓰기
grader-reviewerRead, Grep, Glob, Bash채점기를 양방향으로 검토채점기를 고치지 않는다, 판정과 반례만
deploy-watcherRead, Bashpush 뒤 빌드·운영 반영을 지켜봄kubectl 쓰기, 재시도

grader-reviewer 의 정의 파일에 있는 문장이 이 역할이 왜 있는지 말한다: "'무조건 통과하는 채점기' 가 실제로 여러 개 있었고, 그것은 없는 것보다 나쁘다." 이 글의 .claude/ 조사도 safe-researcher 에게 맡겼다. 부모의 문맥을 아끼고, 조사 중에 실수로 무엇을 고칠 길을 없앤다.

시험은 이것도 강제한다. 에이전트가 셋 이상인지, tools 에 Edit·Write·NotebookEdit 가 없는지, 설명에 "하지 않는다" 류의 부정 표현이 있는지.

절차는 스크립트로: push.sh 와 gate.sh

하네스의 무게 중심은 scripts/agent/ 의 스크립트 8개다. 그중 둘이 핵심이다.

push.sh 는 커밋과 push 의 유일한 길이다. 작업 트리에서 커밋하지 않는다. origin/main 에서 새 worktree 를 뜨고, 내가 지목한 파일만 그 안으로 복사하고, 거기서 게이트를 돌린 뒤 커밋·push 한다. 다른 사람이 같은 파일을 고치고 있으면 파일을 통째로 복사하는 대신 APPLY="스크립트:경로" 로 내 변경만 적용하는 스크립트를 넘긴다. 게이트를 도는 4분 사이에 CI 가 배포 커밋을 올려 origin 이 앞서면, 새 origin/main 위에 내 커밋 하나를 cherry-pick 으로 다시 얹는다. 최대 세 번. 이 세션 로그에서 그 재시도가 23회 있었다.

worktree 경로가 실행마다 다른 이유도 주석에 있다: "고정 경로를 쓰다가 두 push 가 겹쳐 서로의 디렉터리를 지웠고, 그때 시험 로그가 통째로 거짓이 되었다."

gate.sh 는 CI 의 Verify 단계와 같은 것을 로컬에서 돌린다. 보안 검사, 커리큘럼 검사, 채점기 감사를 차례로 돌린 뒤 단위 시험 전부를 돌린다. 만든 이유는 머리 주석에 있다.

빌드 441~446 이 여섯 번 같은 자리에서 실패한 뒤에 만들었다. push 마다
시험을 손으로 골라 적고 있었고, 커리큘럼을 고치면서 test_i18n_catalog 를
고르지 않았다. 고르는 순간 빠뜨린다. 그래서 전부 돌리고, 로컬에만 있는
실패(설치 안 된 의존성·DB 없음)는 gate_baseline.txt 와 대조해 걸러 낸다.

기준선과의 대조는 comm -13 으로 한다. 예전에는 diff | grep 이었는데, set -o pipefail 아래에서 diff 의 종료 코드가 파이프라인 전체를 실패로 만들어 새 실패를 찾고도 그냥 지나갔다. 그렇게 test_ko_source_is_current 가 빌드 451~453 세 번 새어 나갔다. 이 사고는 시험으로 못 박혀 있다: gate.sh 가 comm -13 을 쓰고 git diff 외의 diff 를 쓰지 않는지를 검사한다.

node 시험에는 300초 감시 프로세스가 붙어 있다. node --test 는 기본 시간 제한이 없어 "activation.test.js 가 맥에서 두 번 멈춰 push 를 통째로 붙잡았다".

잠긴 파일과 배포의 증거

bumpdigest.pyapp.js 의 다이제스트를 갱신하되, 갱신할 때 세 가지를 함께 한다. 신뢰 경계의 개수(streamSSE, renderTrustedLessonMarkdown, innerHTML, eval( 등 11개 패턴)를 고치기 전 파일과 견주고, 달라진 것은 ALLOW 로 명시 승인해야 하며, 무엇을 왜 바꿨는지 적은 NOTE 를 이력 블록에 붙인다. 기대값을 숫자로 박아 두지 않는 이유는 "다른 사람의 정당한 변경 뒤에 거짓으로 막힌 적이 있다" 는 것이다. 시험 5개가 이 판정 로직에 임시 파일을 먹여 본다: 승인 없는 변화는 거부, 같은 이력 두 번 기록은 거부, 빈 NOTE 는 거부.

deploy_status.sh 는 "ArgoCD 의 Synced 초록불을 믿지 않는다". 두 앱 모두 selfHeal 이라 화면은 늘 초록이고, 실제로 사흘 묵은 이미지가 Synced 로 떠 있던 적이 있다. 그래서 네 가지를 각각 읽는다. Jenkins 마지막 빌드들의 결과와 시간, gitops 의 newTag 와 실제 Deployment 의 이미지 태그, ArgoCD 두 앱의 상태, 그리고 운영 사이트가 내주는 /app.js 가 그 커밋의 파일과 바이트 단위로 같은가. 정적 파일이 배포의 마지막 증거다.

하네스가 자기 자신을 시험한다

tests/test_claude_harness.py 의 21개 시험은 코드가 아니라 하네스의 구조를 검사한다. 몇 개만 옮긴다.

  • 스킬의 description 은 80자를 넘고, "사용한다" 와 함께 언제 쓰지 않는지 를 담아야 한다.
  • 스킬 본문에는 ## 왜, ## 절차, ## 출력 형식, ## 하지 말 것 네 절이 모두 있어야 한다.
  • 스킬이 언급하는 scripts/agent/* 는 실재해야 하고, .sh 는 실행 권한과 bash -n 을, .pycompile() 을 통과해야 한다.
  • .claude/scripts/agent/ 어디에도 /Users/, /private/tmp/claude 같은 기계 고유 경로가 없어야 한다.
  • push.shgit worktree add 가 있고 git stash 가 없어야 하며, git checkout 은 반드시 origin/main 과 함께여야 한다.
  • README 는 모든 스킬 디렉터리와 enabledPlugins 의 모든 플러그인을 언급해야 하고, commit-commands 는 거기 없어야 한다.

이 시험들 덕에 하네스는 도입 이후 구조가 바뀌지 않았다. .claude/scripts/agent/ 를 건드린 커밋은 도입 커밋과 기준선 두 줄을 정리한 커밋, 둘뿐이다.

세션 밖에 남는 것: 메모리

저장소 안의 하네스가 "규칙" 이라면, 저장소 밖에는 "경험" 이 남는다. 이 작업 환경의 메모리 디렉터리에는 파일 65개가 있다. 유형별로 프로젝트 사실 53개, 사용자 피드백 9개, 외부 참조 3개다. 각 파일은 사실 하나와 "왜", "어떻게 적용하는가" 를 담고, 색인 파일 한 줄이 세션 시작 때 읽힌다.

이번 주에 적힌 것 몇 개를 보면 무엇이 남는지 알 수 있다. 「게이트는 worktree 가 아니라 내 로컬 사본의 기준선을 읽는다」, 「만드는 것과 보이게 하는 것은 다른 일」, 「손으로 만든 Job 은 파드 라벨을 잃기 쉽다」, 「CronJob 실패는 ArgoCD 를 다음 성공까지 붙잡는다」. 전부 코드에는 적을 데가 없고, 문서에 적으면 낡는 종류의 사실이다.

이번 세션에서 실제로 겪은 것

하네스가 있어도 사고는 난다. 다만 사고가 막힌 자리에서 난다. 어제 검증기 한 줄을 고쳐 올리는 데 push 를 네 번 돌렸다.

  1. push.sh 가 첫 인자로 커밋 메시지 파일 을 받도록 바뀌어 있었다. 문자열을 주자 "커밋 메시지 파일이 없습니다" 로 끝났다.
  2. 게이트가 test_production_cli_…_fails_unprovisioned 를 새 실패로 잡았다. 내 변경과 무관한 환경 실패였다. 이 Mac 의 cryptography 50.0.1 이 잠금본 50.0.0 과 달라 다른 오류가 먼저 났다.
  3. 같은 실패가 또 잡혔다. 원인은 gate.sh 가 worktree 가 아니라 로컬 사본gate_baseline.txt 를 읽는다는 것이었고, 로컬 사본이 낡아 그 파일이 비어 있었다.
  4. 기준선을 맞추자 통과했고, 그 사이 CI 가 배포 커밋을 올려 "origin 이 앞서 있습니다 — 다시 얹습니다(1)" 를 거쳐 push 됐다.

네 번 중 한 번도 운영에 닿지 않았다. 게이트가 막은 41회가 전부 이런 것들이다. 그리고 3번의 교훈은 메모리 파일이 되어 다음 세션이 같은 자리에서 서지 않게 한다.

APPLY 스크립트에도 함정이 하나 있었다. 스크립트는 "이미 적용됐는가" 를 먼저 확인하는데, 그 확인의 열쇠를 원본에도 있는 문자열(resolve(demo.map(lang)))로 잡는 바람에 "이미 있습니다" 라며 아무것도 하지 않았다. 열쇠는 새로 들어가는 문자열이어야 한다.

한계

이 하네스는 완성이 아니다. 문서가 스스로 밝히는 한계 두 가지가 있다.

사람의 승인 단계가 없다. docs/CI-CD.md 의 문장이다: "main 에 밀린 것이 Verify 와 dev E2E 를 통과하면 같은 잡이 그대로 운영까지 간다. 운영을 지키는 것은 그 두 관문뿐이고, 그래서 관문에 무엇이 걸려 있는지가 중요하다." 승인 게이트는 fail-closed 설계의 목표로 적혀 있을 뿐 아직 돌지 않는다.

기준선은 기계마다 다르다. gate_baseline.txt 는 로컬 환경의 실패를 걸러 내는 파일인데, 저장소 안에 있으면서도 실제로는 기계마다 달라야 한다. 위의 3번 사고가 그 모순에서 왔다.

그리고 롤링 배포 중에는 두 파드가 다른 빌드를 내준다. 어제 브라우저가 옛 파드에서 받은 스크립트를 캐시에 남겨 새 기능이 잠시 안 보였다. 하네스는 커밋과 배포를 지키지만, 브라우저 캐시까지는 아직 지키지 않는다.

한 줄로

하네스는 규칙을 더 많이 적는 일이 아니라, 실제로 일어난 사고를 그 자리에서 자동으로 막는 장치를 하나씩 쌓는 일 이다. 규칙은 한 곳에, 장치는 도구 호출·커밋·배포의 길목에, 경험은 세션 밖 메모리에. 이 저장소에서는 그 세 층이 하루 수십 번 돌고, 그 결과가 게이트가 막은 41회로 남아 있다.

현재 단락 (1/98)

같은 모델을 쓰는데도 어떤 저장소에서는 에이전트가 `git stash` 로 남의 편집을 날리고, 어떤 저장소에서는 그런 일이 나지 않는다. 차이는 모델이 아니라 모델을 둘러싼 것,...

작성 글자: 0원문 글자: 7,192작성 단락: 0/98