한 레포에만 살던 개발 규율을, Claude Code가 어디서든 따르게 만들기
한동안 저는 한 가지 사실을 당연하게 여기고 있었습니다. 제가 가장 오래 손대온 주력 레포에는, 그동안 시행착오로 다듬어온 개발 규율이 켜켜이 쌓여 있습니다. main에는 직접 손대지 않고 피처 브랜치에서 작업하고, 끝나면 PR을 올려 맥락 없는 에이전트에게 자체 코드 리뷰를 받고, 논리 단위마다 커밋하며 그때그때 빌드와 린트를 검증하고, 테스트는 운영 DB나 서버에 절대 닿지 않게 격리합니다. 이 규율을 Claude Code 세션에게 맡기면, 세션은 군말 없이 이 흐름을 따라줍니다. 그래서 저는 막연히 "내 에이전트는 규율을 안다"고 믿고 있었습니다.
그 믿음이 깨진 건, 다른 레포에서 같은 세션에게 작은 작업을 시켰을 때였습니다. 분명 같은 머신, 같은 에이전트였는데, 이번엔 워크트리도 만들지 않고 브랜치도 떼지 않은 채 main에 그냥 커밋해 버렸습니다. 잠깐 당황하다가, 곧 솔직히 인정할 수밖에 없었습니다. 규율을 아는 건 에이전트가 아니라 그 레포였다는 것을요. 이 글은 한 레포에만 갇혀 살던 개발 규율을, AI 코딩 에이전트가 어느 레포에서든 따르게 만들기까지의 기록입니다.
같은 AI 코딩 에이전트인데 왜 레포가 바뀌면 규율이 사라질까
처음엔 단순히 "다른 레포엔 지침을 안 적어둬서 그렇겠지" 하고 넘기려 했습니다. 그런데 막상 무엇이 빠졌는지 하나씩 따져보니, 문제가 그렇게 간단하지 않다는 생각이 들었습니다. 주력 레포에서 규율이 지켜지던 건 어느 한 장치 덕분이 아니라, 성격이 다른 세 가지 장치가 우연히 그 레포에서만 동시에 맞물려 있었기 때문이었습니다.
첫째는 강제 장치였습니다. main에 직접 커밋하거나 푸시하려 하면 그걸 막아서는 git hook이 있었는데, 이 hook이 그 레포의 설정에만 등록되어 있었습니다. 다른 레포에는 애초에 막아서는 손이 없었습니다.
둘째는 도구였습니다. 병렬 작업용 워크트리를 만들어주는 스킬이 있었지만, 그 스킬은 주력 레포의 포트 번호와 DB 구성, 디렉토리 구조를 전제로 짜여 있었습니다. 다른 레포에서 부르면 맞지 않는 셋업을 들고 와 오히려 엉켰습니다.
셋째는 권유였습니다. "이쯤이면 워크트리를 쓰는 게 좋겠다"고 부드럽게 안내하는 소프트 장치가 있었지만, 발동 문턱이 높게 잡혀 있었습니다. 브랜치가 여럿이고 세션이 동시에 여러 개 떠 있어야 울리도록 되어 있어서, 가끔 손대는 레포처럼 브랜치가 적고 단일 세션인 곳에서는 신호가 모자라 조용히 입을 다물었습니다.
막는 게 없고, 도구가 안 맞고, 권유도 울리지 않으니, 새 레포에서는 세 박자가 모두 빠진 채였습니다. 주력 레포에서만 우연히 삼박자가 맞아떨어져 있었던 것입니다.
이 진단이 이 글의 척추입니다. 제가 "규율"이라 부르던 것은 사실 하나의 원칙이 아니라, 한 레포에 결합돼 있던 세 기둥의 합이었습니다. 그러니 다른 레포로 옮기려면 지침 한 줄을 베껴 적는 걸로는 어림없고, 이 세 기둥을 레포에 묶이지 않는 형태로 끄집어내야 한다는 결론에 닿았습니다.
전부 강제할 것인가, 원하는 것만 켤 것인가
가장 먼저 부딪힌 결정은 의외로 철학적인 것이었습니다. 제 홈 디렉토리에는 git 레포가 수십 개 있는데, 그 면면이 제각각입니다. 진지하게 운영하는 것도 있지만, 코딩 테스트 풀이, 잠깐 배워보려고 만든 학습 레포, 버리기 직전의 프로토타입, 오픈소스를 포크해 들여다보던 레포도 한가득입니다. 여기서 "이제부터 모든 레포에 규율을 강제하겠다"고 정하면, 코테 한 문제 풀어보려고 만든 레포에서까지 피처 브랜치와 PR과 리뷰 루프를 거쳐야 합니다. 그건 누가 봐도 과잉이었습니다.
그래서 반대 방향을 택했습니다. 전부 강제(opt-out)가 아니라, 원하는 레포만 켜는 opt-in입니다. opt-out으로 가면 수십 개 레포를 일일이 예외 목록에 적어 빼줘야 하는데, 그 목록을 관리하는 일이 또 다른 잡무가 됩니다. opt-in은 반대로, 켜고 싶은 레포에만 표식을 하나 남기면 됩니다. 굳이 비유하자면 opt-out은 전원에게 일단 발송한 뒤 수신 거부를 받는 쪽이고, opt-in은 구독을 신청한 사람에게만 보내는 쪽입니다. 제 레포 구성에는 후자가 훨씬 정직한 그림이라는 생각이 들었습니다.
표식은 거창할 이유가 없어서, 레포 안에 빈 마커 파일 하나를 두는 것으로 정했습니다.
touch .claude/managed
이 파일이 있으면 "이 레포는 무거운 워크플로우를 적용해 달라"는 뜻이고, 없으면 그 레포는 어떤 강제에도 영향을 받지 않습니다. 미등록 레포는 100% 예전 그대로라는 점이 중요했습니다. 새 장치를 들이면서 가장 경계한 것이, 켜지 말아야 할 곳에서 멋대로 켜져 멀쩡한 작업을 방해하는 일이었기 때문입니다.
안전 규칙은 전부에게, 무거운 워크플로우는 등록한 레포에만
다만 opt-in으로 정하고 나니 한 가지가 걸렸습니다. 워크플로우는 선택이라 쳐도, 어떤 규칙은 레포 성격과 무관하게 항상 지켜져야 했습니다. 이를테면 테스트가 운영 DB나 운영 서버에 닿지 않게 하는 격리, 토큰이나 키를 커밋에 흘리지 않는 위생, 빌드 전 메모리를 살펴 머신을 무리하게 하지 않는 절제 같은 것들입니다. 이런 안전·품질 규칙은 코딩 테스트 레포에 적용돼도 손해 볼 일이 없습니다. 오히려 적용되는 편이 낫습니다.
그래서 규칙을 성격에 따라 두 층으로 갈랐습니다.
| 티어 | 적용 범위 | 담는 것 |
|---|---|---|
| A (안전·품질) | 모든 레포, 항상 | 운영 DB 격리, secret 위생, 타입 안전, 로컬 리소스 관리 |
| B (워크플로우) | 등록한 레포만 | 워크트리→PR→리뷰 루프, 완료의 정의, UI 변경 시 시각 검증 |
티어 A는 모든 세션이 자동으로 읽는 글로벌 지침에 두어, 어느 레포에서 세션을 열든 따라오게 했습니다. 티어 B는 앞서 만든 마커가 있는 레포에서만 발동하도록 묶었습니다. 안전은 기본값으로 깔리고, 무거운 워크플로우는 신청제가 된 셈입니다.
여기에 더해, 지침으로 부드럽게 안내하는 소프트 레이어와 hook으로 실제로 막아서는 하드 레이어를 함께 두었습니다. 하드 레이어인 차단 hook은 레포 종류를 가리지 않는 형태로 다시 짜서, 마커가 있는 레포에서만 main 직접 커밋·푸시·PR을 막고 피처 브랜치로 유도하도록 했습니다. 인프라성 레포나 혼자 쓰는 솔로 레포는 화이트리스트로 빼두었고, 무엇보다 hook이 상황을 확신하지 못하거나 내부 오류를 만나면 무조건 통과시키도록(fail-open) 했습니다. 가드레일이 정상 흐름을 막아서는 순간, 사람은 그 가드레일을 통째로 꺼버리게 마련입니다. 그래서 "애매하면 차단"이 아니라 "애매하면 통과"를 기본값으로 두었습니다.
워크트리 스킬도 손봤습니다. 주력 레포면 기존의 풀 셋업을 그대로 쓰되, 그 외 레포에서는 범용 경로로 빠지면서 기준 브랜치를 자동으로 찾도록 바꿨습니다. main이라는 이름을 하드코딩해두면 기본 브랜치가 다른 레포에서 곧장 어긋나기 때문입니다. 마지막으로 등록을 한 번에 처리하는 /enroll 명령을 두어, 마커를 만들고 그 레포의 고유한 사정만 담은 지침 파일 골격을 깔되, 공통 원칙은 글로벌을 참조하게 했습니다. 같은 규칙을 레포마다 복붙해두면 결국 서로 어긋나기 때문에, 중복은 0으로 두는 것을 원칙으로 삼았습니다.
내가 만든 차단 장치를, 내가 의심해야 했다
여기까지는 그럴듯한 설계였습니다. 그런데 막상 차단 hook을 다 짜놓고 나니, 한 가지 불편한 생각이 들었습니다. 이 hook은 "안전을 지키는 장치"인데, 정작 이 장치가 안전한지는 누가 확인하는가. 가드레일을 만들어놓고 그게 잘 도는지를 제 손으로만 확인하면, 결국 제가 미리 떠올린 시나리오 안에서만 도는 셈입니다. 그래서 이 hook을, 맥락을 전혀 모르는 여러 관점의 리뷰 에이전트에게 적대적으로 뜯어보게 했습니다. 우회는 가능한가, 과하게 막지는 않는가, 다른 하네스 장치와 충돌하지는 않는가, 빠진 경우는 없는가. 결과는 솔직히 뜨끔했습니다. 결함이 둘 나왔습니다.
우회 — 미끼 하나로 차단을 빠져나간다
첫 번째는 우회였습니다. 제 hook은 "이 명령이 어느 디렉토리에서 실행되는가"를 보고 그 레포가 등록 대상인지 판정했는데, 명령에서 디렉토리를 읽어낼 때 가장 왼쪽에 매치되는 경로를 그 명령의 실행 위치로 단정하고 있었습니다. 문제는 명령이 한 줄에 여럿 이어질 수 있다는 점이었습니다. 앞에 아무 해가 없는 미끼 명령 하나를 다른 경로로 붙이면, hook은 그 미끼 경로를 보고 "여긴 등록 레포가 아니네" 하고 통과시켰습니다. 그리고 그 뒤에 이어진 진짜 커밋은 등록 레포의 main에 그대로 들어갔습니다.
git -C ~/some-other-repo log; git commit -m "..."
리뷰 에이전트가 이 패턴을 실제로 돌려보니, 커밋이 등록 레포 main에 멀쩡히 쌓였습니다. 막혔어야 할 일이 막히지 않은 것입니다. 적대적으로 보지 않았다면 저는 이걸 끝내 몰랐을 것입니다.
과차단 — 읽기만 하는 명령까지 막아선다
두 번째는 정반대였습니다. hook이 명령 전체를 하나의 문자열로 보고 그 안에 위험한 패턴이 들어 있는지만 훑다 보니, 실제로는 아무것도 바꾸지 않는 명령까지 같이 걸렸습니다. 이를테면 git commit이라는 글자가 들어간 read-only 명령, 즉 무언가를 검색하기 위해 그 문자열을 인자로 넘기는 명령까지 차단당했습니다. 공교롭게도, 이 결함은 검증하던 에이전트 자신이 진단을 위해 돌린 검색 명령이 hook에 막히면서 그 자리에서 실증됐습니다. 막아야 할 건 통과시키고, 막지 말아야 할 건 막고 있던 셈입니다.
두 결함은 방향이 정반대였지만 뿌리는 하나였습니다. hook이 명령을 "위험한 문자열이 보이느냐"로만 읽고 있었다는 것입니다. 그러나 명령의 본질은 문자열의 생김새가 아니라 "이 명령이 실제로 무엇을, 어디서 하느냐"에 있습니다. 그래서 차단 로직을 다시 썼습니다. 따옴표를 인식하는 토크나이저로 명령을 제대로 쪼개고, 세미콜론·파이프·괄호를 기준으로 명령을 세그먼트로 나눈 뒤, 실제로 무언가를 바꾸는 명령이 진짜로 실행될 디렉토리만 골라 평가하도록 바꿨습니다. 그러자 미끼를 붙여도 진짜 커밋의 실행 위치를 정확히 짚어 막을 수 있었고, 문자열로만 등장하는 read-only 명령은 더 이상 걸리지 않았습니다.
한 가지 덧붙일 교훈이 있었습니다. 이 토크나이저를 처음부터 새로 짜지 않았다는 점입니다. 이미 다른 hook에서 같은 문제를 겪고 검증해둔 토크나이징 패턴이 있어서, 그것을 가져와 재사용했습니다. 가드레일을 적대적으로 검증하라는 교훈만큼이나, 이미 검증된 부품을 다시 쓰라는 것도 이번에 새삼 배운 것이었습니다. 마지막으로, 고친 로직이 다시는 같은 구멍을 내지 않도록 결정적인 테스트 매트릭스를 만들어 두었습니다. 우회 8가지, 과차단 10가지, 디렉토리 이동 추적, 실제 레포에서의 동작까지 합쳐 서른여섯 경우를 두고, 전부 통과하는 것을 확인했습니다.
만든 규율이, 만든 사람의 실수를 잡았다
설계도 하고 검증도 했으니, 마지막은 직접 써보는 일이었습니다. 그런데 마침 이 모든 변경을 정리해 문서로 남기는 작업 자체가, 방금 만든 워크플로우를 거쳐야 하는 일이었습니다. 그래서 새로 만든 규율로 그 정리 작업을 처리했습니다. 일종의 도그푸딩이었던 셈인데, 여기서 두 번, 제가 만든 장치에 제가 잡혔습니다.
하나는 충돌이었습니다. 제가 문서를 다듬는 사이, 다른 세션이 올린 커밋으로 원격 main이 앞서가 있었습니다. 예전 같으면 모르고 그대로 밀어붙였을 텐데, PR을 머지하기 전 검사하는 게이트가 이 어긋남을 잡아냈습니다. 덕분에 리베이스로 어긋남을 풀고, 앞서간 변경에 맞춰 제 작업을 다시 정렬했습니다. 다른 하나는 더 뜨끔했습니다. 리뷰 루프가, 제가 코드를 확인하지 않고 기억에 의존해 적어둔 사실 하나가 틀렸다는 것을 짚어냈습니다. 어떤 값을 저장할 때 쓰는 키의 이름을, 실제 코드와 다르게 적어둔 것이었습니다. 리뷰 에이전트가 그 문장을 코드와 대조해 어긋남을 알려주었고, 저는 코드를 다시 열어 확인한 뒤 정정했습니다.
제가 부주의로 흘려보낸 것을, 제가 만든 리뷰 자동화가 막아 세웠습니다. 규율을 잘 만들었는지를 가장 정직하게 알려준 건, 결국 그 규율이 제 실수 위에서 작동하는 순간이었습니다.
규칙이 사는 세 개의 집
일을 끝내고 정리해보니, 제 규칙은 결국 세 곳에 나뉘어 살게 되었습니다. 글로벌 지침에는 모든 레포에 자동으로 적용되는 공통 방법론이 살고, 등록 마커는 무거운 워크플로우를 켜는 opt-in 스위치 역할을 하며, 레포별 지침에는 그 레포만의 고유한 사정만 담겨 공통 원칙은 글로벌을 참조합니다. 덕분에 새 레포는 안전 규칙을 셋업 없이 자동으로 물려받고, 무거운 워크플로우가 필요해지면 마커 한 줄로 켜면 됩니다.
돌이켜 생각해보면, 이번 일에서 제가 얻은 것은 새 hook 몇 개가 아니었습니다. 한 프로젝트 안에서만 잘 돌던 규율은 그 자체로는 다른 곳으로 옮겨지지 않으며, 옮기려면 레포에 묶이지 않는 글로벌 레이어로 끄집어내야 한다는 것. 성격이 제각각인 레포 집합에는 전부 강제하기보다 원하는 것만 켜는 편이 현실적이라는 것. 그리고 무엇보다, 제가 만든 가드레일은 반드시 적대적으로 의심해봐야 비로소 우회와 과차단을 함께 막을 수 있다는 것이었습니다. 차단의 기준은 "위험한 글자가 보이느냐"가 아니라 "이 명령이 실제로 무엇을 어디서 하느냐"에 있어야 했습니다.
무언가를 강제하는 장치를 만들 때, 그 장치를 가장 먼저 의심해야 할 사람은 그것을 만든 자신이라는 생각이 들었습니다. 그리고 다 만든 규율은 책장에 올려두지 말고 자기 작업에 직접 돌려봐야 한다는 것도요. 그래야 그 규율이, 언젠가 자신의 부주의까지 조용히 막아주는 날이 옵니다.
관련 글
사람 손 없이 PR→머지→배포: AI 에이전트 자동 연쇄를 설계하며 배운 것
코드 리뷰·머지 게이트·배포는 다 자동화했는데, 사슬의 진입점만 수동 호출로 남아 있었습니다. PR 생성 직후 자동 트리거, 보고가 아닌 GitHub를 직접 조회하는 머지 게이트, 배포 보류 게이팅까지 — 자동 연쇄를 직접 설계하며 겪은 시행착오를 정리했습니다.
Claude Code 라우팅 매트릭스를 글로벌 CLAUDE.md 와 SKILL.md 에 박은 회고
Claude Code sub-agent 라우팅 매트릭스를 글로벌 CLAUDE.md 와 review-loop SKILL.md 두 곳에 박은 회고입니다. 같은 매트릭스를 인라인 복제하지 않은 이유와 hook 까지 가지 않은 단계적 결정 근거를 함께 적었습니다.
Claude Code 하네스: 1인 개발자가 AI 에이전트 팀을 만드는 법
1인 개발자가 Claude Code로 전문가 AI 팀을 구성한 경험. 에이전트, 스킬, 오케스트레이터로 이루어진 하네스 구조와 모노레포 풀스택 개발 자동화 사례를 공유합니다.