-
멀티프로젝트 Claude Code 설정 운영기 3편 - Edit는 줘도 commit은 못 준다AI 엔지니어링 2026. 8. 25. 07:40728x90반응형
무심결의 commit 하나가 다음 세션의 토대가 됐다
여러 프로젝트에 Claude Code 설정을 얹어 8주쯤 운영했을 때 일이다. 한 세션에서 에이전트가 기능 구현을 끝내더니, 확인을 받지도 않고 그대로
commit까지 해버렸다. 나는 그걸 눈치채지 못한 채 세션을 닫았고, 다음 세션은 그 커밋을 이미 확정된 토대로 삼아 그 위에 또 빌드를 쌓았다. 되돌리려 했을 땐 히스토리 두 겹을 함께 풀어야 했다.문제는 에이전트가 무능해서가 아니었다. 나는 "파일을 고쳐도 좋다"는 권한과 "그 변경을 git 히스토리에 남겨도 좋다"는 권한을 한 덩어리로 묶어 통째로 넘겨준 상태였다. 이 둘은 위험도가 완전히 다르다. 편집은 되돌리기 쉽지만, 커밋은 사건이 된다.
그날 이후 나는 에이전트 권한을 "역할" 단위가 아니라 "동작(primitive)" 단위로 다시 그었다. 이 글은 2026년 7월 현재까지 굴려 본 그 경계에 대한 기록이다.
매크로 경계 안쪽에 마이크로 경계가 있다
이 시리즈 앞부분과, 따로 쓴 "에이전트는 기본 OFF" 이야기에서 다룬 건 매크로 경계였다. 어떤 에이전트를 자동으로 부를 것인가, 언제 서브에이전트를 활성화할 것인가. 즉 누구를 부르느냐의 문제.
이 글은 한 겹 안쪽, 마이크로 경계다. 같은 에이전트에게도 어떤 primitive는 거부하는가. 여기서 primitive란 에이전트가 실행하는 최소 동작 단위를 말한다. 파일
Edit/Write,branch생성,commit,push,PR생성이 각각 하나의 primitive다. 매크로가 "이 방에 누구를 들일까"라면, 마이크로는 "들인 사람에게 어떤 서랍은 잠가 둘까"에 가깝다.이 구분이 중요한 이유는, 같은
@builder(구현 전용 역할)에게도Edit열은 열어 주고commit열은 닫아야 하기 때문이다. 권한은 주체가 아니라 동작에서 갈린다.되돌릴 수 있는 일과, 히스토리에 박히는 일
경계를 긋기 전에 동작을 두 부류로 나눴다.
Reversible 동작은 working tree만 바꾸는 일이다. 파일 편집, 새 파일 생성 같은 것.
git checkout이나git reset으로 흔적 없이 되돌릴 수 있다. 에이전트가 여기서 실수해도 비용은 낮다.Publishing 동작은 히스토리·원격·리뷰 표면에 흔적을 남기는 일이다.
branch생성,commit,push,PR/tag/release. 되돌리려면 force push나 revert 같은, 그 자체로 또 하나의 "사건"인 조치가 필요하다. 팀이 얽혀 있으면 남의 작업까지 건드린다.규칙은 한 줄로 정리된다. 에이전트에게 reversible은 위임하되, publishing은 절대 위임하지 않는다. 그리고 그 경계의 핵심 열쇠인
branch생성권은 오직 명시적 publishing command만 갖는다.branch 생성권은 커맨드만 갖는다
이 경계를 글로벌
CLAUDE.md의 "Workflow Rule" 섹션에 문장으로 못 박았다. 회사·프로젝트 식별 정보를 걷어내고 핵심만 옮기면 이렇다.## Workflow Rule (권한 경계) - 사용자 요청이나 활성화된 publishing command 게이트 없이는 코드를 바꾸거나 commit하지 않는다. - branch 생성은 `/pr`·`/sub-pr`·`/hotfix-pr`·`/worktree done`에만 예약한다. (`/worktree start`는 detached-HEAD worktree만 만들고 branch는 만들지 않는다.) 그 외 어떤 맥락도 branch를 만들지 않으며, "구현을 승인함"은 결코 "branch 생성을 승인함"이 아니다. - 구현 전용 역할(@builder)은 write는 하되 commit/push/PR은 못 한다. write는 항상 single-threaded로 실행한다. - Skills는 권한 경계가 아니다(재사용 지침 모듈일 뿐). MCP write/mutate 도구도 @builder급이며 publishing gate를 넘지 않는다.말로 풀면,
/feature안에서 에이전트가 소스를 실컷 고쳐도 그 편집은 전부 reversible 구역에 머문다. 히스토리로 넘어가는 문(branch·commit·push·PR)은 내가/pr같은 커맨드를 직접 부를 때만 열린다. 아래 매트릭스가 그 그림이다.
같은 주체라도 Edit/Write 열은 통과하고 branch·commit·push 열은 막힌다. 게이트를 여는 건 오직 publishing command다.
진짜 구멍은 allow 설정이 아니라 대화의 관성이었다
처음엔 이 경계를
settings.local.json의 permissions로 잡으려 했다.Bash(git commit:*)를 넉넉히 allow에 넣어 두고 에이전트를 믿는 식이었다. 앞의 사고가 바로 여기서 났다.그래서 git mutate primitive를 기본 deny로 내리고, publishing command만 게이트를 여는 구조로 바꿨다. 실제 설정은 커맨드가 게이트를 소유하는 형태지만, 핵심을 어느 설정에나 붙일 수 있는 최소 형태로 옮기면 PreToolUse hook 하나로 재현된다.
#!/usr/bin/env bash # .claude/hooks/deny-agent-publishing.sh # 에이전트의 git publishing primitive를 기본 거부한다. # publishing command만 PUBLISH_GATE=1을 세팅해 이 게이트를 연다. payload="$(cat)" # hook은 stdin으로 tool 입력(JSON)을 받는다 cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // ""')" # 읽기 전용 git은 항상 통과 (status/log/diff/show ...) if printf '%s' "$cmd" | grep -Eq '^[[:space:]]*git[[:space:]]+(status|log|diff|show|rev-parse)'; then exit 0 fi # 되돌리기 어려운 primitive: branch 생성·commit·push·tag, 그리고 PR 생성/ready mutate='git[[:space:]]+(commit|push|tag)|git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)|gh[[:space:]]+pr[[:space:]]+(create|ready)' if printf '%s' "$cmd" | grep -Eq "$mutate"; then if [ "${PUBLISH_GATE:-0}" != "1" ]; then printf '%s' '{"decision":"deny","reason":"publishing primitive는 /pr·/sub-pr·/hotfix-pr·/worktree done 안에서만 허용됩니다. 구현 승인은 branch/commit 승인이 아닙니다."}' exit 0 fi fi exit 0 # 그 외에는 통과Edge case 몇 개를 일부러 남겨 뒀다.
git status·git log같은 읽기는 그대로 통과시켜야 에이전트가 상태를 파악할 수 있다. 막는 건commit·push·tag와 새 branch(checkout -b/switch -c), 그리고gh pr create뿐이다. publishing command는 자기 트랜잭션 안에서만PUBLISH_GATE=1을 세팅하므로 정상 발행 흐름은 막히지 않는다.그런데 hook을 붙이고 나서 깨달은 진짜 함정은 따로 있었다. 위험한 건 명시적 allow 설정이 아니라 대화의 관성이었다. 에이전트가 "이제 커밋할까요?"라고 물으면 나는 반사적으로 "네, 그렇게 진행"이라고 답한다. 이 자연어 수락을 command 호출로 취급하는 순간, 게이트가 스르륵 열린다. 에이전트가 스스로 꺼낸 선택지에 대한 짧은 동의("ok", "ㅇㅇ", "진행")는 발행 권한과 아무 상관이 없어야 한다.
그래서 규칙에 두 문장을 더 박았다. 제안 수락은 command 호출이 아니다. 게이트는 내가 소유 command를 직접 부를 때만 열린다. 그리고 한 번의 승인은 다음 reversible 한 단계만 연다. 하나의 "네"가 branch→commit→push→PR로 연쇄하지 않는다(one approval, one gate).
예외를 뚫되, 번지지 않게 — MCP write 도구
외부 MCP write 도구가 들어오면서 경계가 한 겹 더 정교해졌다. 원칙은 그대로다. MCP write/mutate 도구는 기본적으로
@builder급 권한이라, 파일은 건드려도 publishing gate는 넘지 못한다. 외부 도구는 신뢰할 수 없는 것으로 간주하고 시작한다.여기에 딱 하나, 선언적 예외를 뚫었다. registry allowlist에 등록된 프로젝트에 한해, 외부 이슈 트래커(예: Jira류)의 create/edit/transition 호출을 플랜 lifecycle 이벤트에 묶인 자동 side-effect로 허용했다. 플랜을 등록하면 티켓이 열리고, 작업이 진행 상태로 넘어가면 담당자가 붙고, 정리 단계에서 리뷰로 전이되는 식이다. 상태 머신이 작업 흐름을 그대로 따라간다.
중요한 건 예외의 모양이 아니라 예외를 가둔 방식이다. 이 side-effect는 commit/push/PR로는 절대 확장되지 않는다. 예외를 열 때 "여기까지만"을 같은 문장 안에 못 박아 둔 것이다. 예외는 방치하면 번진다. 다음 사람이(또는 다음 세션이) "이것도 자동인데 저건 왜 안 돼?"라며 경계를 밀어붙이기 때문이다. 그래서 예외를 둘 때는 그 예외가 닿지 않는 곳을 함께 명문화한다. 이게 이 시리즈를 관통하는 permission-boundary 설계의 핵심이다.
결국 이것도 인터페이스 설계다
정리하면 이렇다. 에이전트에게 Edit/Write는 위임하되 branch·commit·push는 위임하지 마라. 히스토리로 넘어가는 문은 명시적 publishing command만 열고, 자연어 수락은 그 문의 열쇠가 아니다. 예외를 둘 거라면, 그 예외가 닿지 않는 경계를 같은 자리에서 문장으로 고정하라.
지금 당신의 에이전트 설정을 열어 딱 하나만 확인하라. 에이전트가
commit이나push를 스스로 실행할 수 있는가. 할 수 있다면, 그 primitive를 오늘 기본 deny로 내려 두라.반응형'AI 엔지니어링' 카테고리의 다른 글
멀티프로젝트 Claude Code 설정 운영기 2편 - gitignore한 CLAUDE.local.md를 git add -f로 강제 추적한 이유 (0) 2026.08.18 멀티프로젝트 Claude Code 설정 운영기 1편 - 여러 프로젝트 .claude/를 한 저장소가 소유하게 만들기 (0) 2026.08.11 프런트엔드 AX 설계기 11편 — 설정도 코드처럼 부채가 쌓인다 (1) 2026.08.04 프런트엔드 AX 설계기 10편 - 단가가 아니라 실패비용으로 모델을 고른다 (0) 2026.07.28 프런트엔드 AX 설계기 9편 - 외부 이슈 트래커 write 도구를 권한 경계 안에 가두기 (1) 2026.07.24