포스트

Claude Code 세션 두 개가 git으로 일을 주고받는 법 — 맥↔윈도우 작업 요청 채널 운영기

맥과 윈도우에서 각각 도는 Claude Code 세션이 git 저장소의 요청서 파일로 일을 주고받게 했다. 첫날 2시간 20분 만에 파이프라인 이관을 끝냈지만, 두 달 뒤 6주째 아무도 열지 않은 요청서와 다시 살아난 봇을 발견했다. 요청서 형식과 운영하며 생긴 규칙들.

Claude Code 세션 두 개가 git으로 일을 주고받는 법 — 맥↔윈도우 작업 요청 채널 운영기

문제 — 두 기계의 Claude에게 일을 시키려면 사람이 복붙을 해야 했다

블로그·리뷰 자동화 파이프라인을 맥에서 만들고, 실제 실행은 상시 켜 둔 윈도우 PC로 옮기기로 했다. 두 기계 모두에서 Claude Code를 쓴다.

문제는 그 사이의 대화였다. 맥의 Claude가 “윈도우에서 이걸 확인해야 한다”고 하면, 내가 그 내용을 복사해 윈도우의 Claude에게 붙여 넣고, 윈도우 쪽 답을 다시 복사해 맥으로 가져와야 했다. 내가 메신저 역할을 하는 셈이었다. 복붙하다 한 줄이 빠지고, 어느 답이 어느 질문에 대한 건지 헷갈리고, 나중엔 무슨 일이 있었는지 기록도 남지 않았다.

두 기계는 이미 같은 git 저장소를 쓰고 있었다. 그래서 저장소 안에 요청서 파일을 두고, 두 Claude가 그 파일로 일을 주고받게 했다.

구조 — 요청서 파일 하나에 요청·금지·결과를 다 담는다

맥의 Claude 세션이 ops 폴더에 요청서를 커밋하면 윈도우의 Claude 세션이 pull해서 처리하고 결과 절을 채워 status를 done으로 바꿔 되돌리는 흐름

저장소에 ops/ 폴더를 만들고, 요청 하나를 파일 하나로 쓴다. 파일 이름은 ops/<3자리 번호>-<짧은 제목>.md다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
id: 002
from: mac
to: windows
status: open
---

## 요청
(무엇을 왜. 실행할 명령은 그대로 복사 가능하게)

## 하지 말 것
(금지사항을 명시적으로. 특히 바깥으로 나가는 행동)

## 결과
(처리하는 쪽이 채운다 — 한 것 / 못 한 것 / 에러는 원문 그대로)

흐름은 이렇다.

  1. 요청하는 쪽이 파일을 만들고 status: open으로 커밋·푸시한다.
  2. 처리하는 쪽은 git pull 후 status: open인 파일을 번호 순으로 처리한다.
  3. 「결과」 절을 채우고 status를 done 또는 blocked로 바꿔 커밋·푸시한다.

이 절차는 양쪽 Claude가 쓰는 스킬 파일(/ops)에 적어 뒀다. 사람은 “ops 확인해”라고만 하면 된다.

몇 가지는 처음부터 규칙으로 박았다.

  • 「하지 말 것」을 먼저 읽는다. 요청보다 금지사항이 우선이다.
  • 커밋은 그 파일만 지정한다 — git add ops/<파일>. 봇이 수시로 커밋하는 라이브 저장소라 git add -A를 쓰면 엉뚱한 것까지 딸려 들어간다.
  • 비밀값은 요청서에 쓰지 않는다. .env 값은 “사람이 넣는다 — 네가 채우지 마라”라고 적는다.
  • 끝난 파일은 지우지 않는다. 나중에 경위를 되짚는 근거가 된다.

첫날 — 2시간 20분 만에 이관을 끝냈다

채널을 만든 2026년 8월 21일 16:02부터 18:20까지, 요청서 6개가 맥과 윈도우를 오갔다. 커밋 기록을 시간순으로 보면 이렇다.

시각쓴 쪽내용
16:02맥001 이관 마무리 요청 — 최신화·.env 확인·검증 4종
16:29윈도우001 done
16:46맥002 스케줄러 등록 요청 — “사용 안 함” 상태로
17:01윈도우002 done
17:09맥003 트리거 정비 + 명령줄 무부작용 검증
17:15윈도우003 done
17:18윈도우004 open — 윈도우가 맥에게 결정을 요청
17:27맥005 컷오버 활성화 지시 + 맥 정지 증거 첨부
17:54윈도우005 done — 컷오버 완료

맥에서 돌던 봇을 윈도우로 옮기면서 두 기계가 동시에 도는 순간이 한 번도 없게 순서를 짰다. 윈도우에 먼저 등록만 하고(사용 안 함), 맥을 내리고, 그 증거를 확인한 뒤에 윈도우를 켰다. 같은 텔레그램 봇 토큰으로 두 기계가 동시에 메시지를 받아 가면 메시지가 둘로 쪼개지기 때문이다.

운영하며 생긴 규칙

① 요청서는 사람의 승인이 아니다

요청서는 상대 Claude가 쓴 메모다. 그대로 따르되, 바깥으로 나가는 행동은 파일에 적혀 있어도 사람에게 먼저 묻는다 — 블로그 발행, 스케줄러 활성화, 봇 가동, 메시지 전송, 되돌리기 어려운 삭제.

이 규칙이 실제로 작동한 게 005 컷오버였다. 맥이 “맥 잡을 내렸다, 이제 활성화해라”라고 써 보냈는데, 윈도우 쪽 Claude는 바로 켜지 않았다. 그 전에 맥이 정말 멈췄다는 증거를 요구해 뒀고, 맥은 005에 이걸 붙여 보냈다(005의 제목이 “맥 정지 증거 (네가 요구한 것)”이다).

1
2
3
4
5
6
=== 잡 내리기 ===
  bootout com.riririb.riri-review-bot   → exit=0
  bootout com.riririb.riri-review-daily → exit=0

=== [AFTER] ===
--- launchctl list ---  (없음)  ← 두 잡 모두 내려감

여기에 봇 로그 파일을 100초(봇 주기 60초 초과) 동안 지켜보며 크기가 더 이상 늘지 않는 것까지 기록했다. 윈도우 쪽은 그 증거를 사람에게 그대로 보여주고 승인을 받은 뒤에 활성화했다. 결과 절에 이렇게 남겼다 — “요청서만으로는 활성화하지 않는다는 규칙 때문에, 맥 정지 증거를 사람에게 그대로 전달하고 승인받았다.”

② 받는 쪽이 요청서를 반박할 수 있어야 한다

003을 보내면서 맥은 002의 결과를 검토했는데, 정작 틀린 건 맥의 요청서였다. 윈도우 쪽이 결과 절에서 세 가지를 바로잡았다.

  • 스케줄러 명령은 출력이 없는 게 성공이다.
  • “사용 안 함” 상태여도 다음 실행 시각은 표시된다.
  • 추적 해제한 폴더는 새로 클론한 쪽에선 디스크에서도 사라진다. 맥은 002에서 “파일은 디스크에 그대로 있고 인덱스에서만 빠졌다”고 적었는데, 그건 맥처럼 원래 파일이 있던 쪽 이야기였다.

맥은 003을 “네가 정정한 세 가지는 전부 네 말이 맞다. 요청서 판정 기준이 틀렸던 것이고 고쳐 반영했다”로 시작했다. 요청서 쓴 쪽이 받는 쪽의 환경을 다 알 수는 없다. 그래서 작성 규칙에 “받는 쪽 환경을 가정하지 말고 확인하게 시킨다 — 경로·설치 여부는 ‘확인해서 결과에 적어라’로 쓴다”를 넣었다.

③ 닫힌 파일에 쓴 질문은 아무도 읽지 않는다

처리하는 쪽은 status: open인 파일만 찾는다. 그래서 윈도우가 001~003 결과 절 안에 남긴 질문들은 그대로 묻힐 뻔했다. 윈도우 쪽이 이걸 알아채고 결정이 필요한 질문만 모아 새 요청(004, 윈도우→맥)으로 열었다.

그러면서 번호가 꼬였다. 맥은 003에서 “컷오버는 004로 지시한다”고 예고했는데, 윈도우가 먼저 004를 가져갔다. 번호 규칙이 “가장 큰 번호 + 1”이라 그렇다. 004에 “컷오버 요청은 005로 써 달라”는 메모가 붙었다. 번호를 미리 약속하지 않는다는 게 여기서 나온 교훈이다.

④ 윈도우 경로의 백슬래시는 코드로 감싼다

001 결과를 받아 보니 윈도우 경로가 망가져 있었다. 기록에 따르면 C:\#DEV\riri-review는 \r 부분이 줄바꿈으로, Git\bin\bash.exe는 \b 부분이 글자 삭제로 바뀌어 있었다. 백슬래시 뒤 글자가 이스케이프로 읽힌 것이다.

그 뒤로 규칙을 하나 더했다. 윈도우 경로는 반드시 백틱 코드로 감싸고, 가능하면 슬래시(/)로 쓴다. Git Bash와 git 모두 슬래시 경로를 받는다.

⑤ 시각은 양쪽에서 같은 답을 내는 방법으로만 적는다

요청서에 “언제 실행했는지”를 KST로 적는데, 윈도우의 Git Bash에서 TZ='Asia/Seoul' date가 엉뚱한 값을 냈다. 8월 21일 실측이다.

1
2
TZ 지정:    2026-08-21 08:39 GMT     ← 틀림
TZ 미지정:  2026-08-21 17:39         ← 시스템 로컬(KST). 이게 맞다

Git Bash에는 시간대 데이터베이스가 없어 Asia/Seoul을 해석하지 못하고 GMT로 떨어진다. 9시간 차이라 00시~09시 KST 사이엔 날짜까지 하루 틀린다. 그래서 양쪽 OS에서 같은 답이 나오는 파이썬 한 줄로 통일했다.

1
python3 -c "import datetime as d;print(d.datetime.now(d.timezone(d.timedelta(hours=9))).strftime('%Y-%m-%d %H:%M'))"

그리고 두 달 뒤 — 6주째 열려 있던 요청서

8월 21일 bootout으로 맥북 봇을 내렸지만 10월에는 다시 돌고 있어 맥미니와 동시에 메시지를 받았고, 10월 26일 disable로 영구히 끈 타임라인

이 채널에는 약점이 하나 있었다. 누가 열어 보기 전까지는 아무 일도 일어나지 않는다.

9월에 수집 기계를 다시 맥미니로 옮겼다. 맥미니의 Claude는 9월 12일에 요청서 007을 썼다 — “수집은 이제 맥미니에서 돈다. 다른 기계(윈도우, 그리고 맥북)의 봇과 임시저장 작업을 꺼 달라.” 그런데 이 요청서는 6주가 넘도록 open 이었다. 10월 26일, 다른 블로그 글 소재를 찾던 중에 우연히 발견했다.

확인해 보니 맥북에서 봇이 1분마다 돌고 있었다(launchd 누적 실행 5,591회). 10월 3일 오후에는 두 기계가 같은 시간대에 각자 텔레그램 메시지를 받아 커밋한 기록도 있었다. 15:50:47에 보낸 사진(155047-photo.jpg)이 맥미니에선 16:08에, 맥북에선 22:10에 서로 다른 폴더로 커밋돼 있다. 두 파일은 크기가 달라 완전히 같은 파일인지까지는 확인하지 못했다. 그날 분은 둘 다 원격에 올라가 있었지만, 확인해 보니 맥북 쪽 저장소는 봇 상태 파일의 병합 충돌에 걸려 매일 git pull에 실패하고 있었다. 그 상태에서 맥북이 메시지를 먼저 받아 가면, 그 메시지는 맥미니 쪽으로 넘어갈 길이 없었다.

왜 다시 살아났나 — bootout은 영구가 아니다

이상한 건 맥북 봇이 8월 21일에 분명히 내려졌다는 점이다. 위 ①의 증거가 그거다. 그런데 왜 다시 돌고 있었을까.

8월 21일에 쓴 명령은 launchctl bootout이었다. 이건 지금 로드된 작업을 내리는 것이고, plist 파일은 ~/Library/LaunchAgents에 그대로 남는다. man launchctl에 따르면 영구히 막는 건 따로 있다.

  • launchctl disable — “비활성화하면 다시 활성화할 때까지 로드할 수 없고, 이 상태는 재부팅 후에도 유지된다.”
  • launchctl unload -w — Disabled 상태를 디스크에 저장한다. -w가 없으면 저장되지 않는다.

그러니 그 사이 언젠가 다시 로그인했을 때 plist가 다시 올라온 것으로 보인다(시점은 확인하지 못했다). 그리고 007 요청서가 지시한 것도 -w 없는 launchctl unload 였다. 누군가 007을 그대로 처리했더라도 같은 일이 반복됐을 것이다.

이번엔 두 단계로 껐다.

1
2
3
4
launchctl bootout gui/$(id -u)/com.riririb.riri-review-bot    # 지금 내리고
launchctl disable gui/$(id -u)/com.riririb.riri-review-bot    # 다시 못 올라오게
launchctl print-disabled gui/$(id -u) | grep riri-review
#   "com.riririb.riri-review-bot" => disabled

launchd로 작업을 무인 실행할 때의 다른 함정은 Claude Code를 launchd·cron으로 매일 무인 실행하기에, “상주 러너는 한 대만” 규칙은 맥미니를 24시간 봇 러너로 만들기에 정리해 뒀다. 그 규칙을 써 두고도 같은 일이 생긴 셈이다.

왜 6주 동안 아무도 몰랐나

채널은 끌어가는(pull) 구조다. 처리하는 쪽이 “ops 확인”을 해야만 요청을 본다. 그리고 README에 “개발 대화에서만 연다, 애매하면 열지 않는다”고 적어 뒀다. 일상 수집 대화가 ops 확인으로 길어지는 걸 막으려던 규칙인데, 그 결과 맥북에서 개발 대화를 하지 않는 동안에는 맥북 앞으로 온 요청을 아무도 열지 않았다.

요청서에 수신 확인이 없는 것도 같은 문제다. 맥미니가 요청서를 보낸 뒤 “처리됐는지”를 다시 확인한 기록은 저장소에 없다. 보냈다는 사실만으로 끝난 것처럼 됐다.

“보냈다”는 “처리됐다”가 아니다. 끌어가는 구조의 채널에서는 받는 쪽이 열어 보지 않으면 요청이 영원히 대기한다. 기계를 옮기거나 끄는 것처럼 안 하면 사고가 나는 요청은, 보낸 쪽이 나중에 status를 다시 확인하는 단계를 절차에 넣어야 한다.

🧭 기획자·사업자라면 AI 세션끼리 일을 주고받게 할 때 가장 값진 건 “기록이 남는다”는 점이다. 이번에 6주 묵은 문제를 하루 만에 추적할 수 있었던 건, 8월 21일 어떤 명령으로 껐는지가 요청서에 원문 그대로 남아 있어서였다. 사람이 메신저로 복붙했다면 “분명히 껐는데?”에서 멈췄을 것이다. 그리고 사람 승인의 경계를 문서로 박아 두면 AI가 그 선을 지킨다. 컷오버 때 윈도우 쪽 Claude는 요청서에 “활성화해라”라고 적혀 있어도 증거를 확인하고 사람 승인을 받은 뒤에 움직였다. 자동화의 속도는 유지하면서 되돌리기 어려운 지점에서만 사람이 확인하는 구조다. 다만 이번 일처럼 받는 쪽이 열어 보지 않으면 멈추는 채널이라면, “중요한 요청은 보낸 쪽이 처리 여부를 다시 확인한다”는 운영 규칙까지 함께 있어야 한다.

사용한 기술

  • git 저장소의 ops/ 폴더 — 요청 하나 = 파일 하나. status 필드(open·done·blocked)로 상태를 관리한다.
  • Claude Code 스킬(/ops) — 처리·작성 절차를 양쪽 세션이 같은 문서로 따르게 한다. 스킬 폴더는 git으로 맥·윈도우에 동기화한다.
  • git add <파일> 지정 커밋 — 봇이 수시로 커밋하는 저장소에서 다른 변경이 섞이지 않게 한다.
  • launchctl bootout / disable / print-disabled — 내리기, 재부팅 후에도 막기, 막힌 상태 확인. unload는 -w가 있어야 영구다.
  • KST 고정 파이썬 한 줄 — TZ 해석이 OS마다 달라서, 시간대를 코드로 고정한다.

정리

  • 두 기계의 Claude에게 일을 시킬 때 사람이 복붙하는 대신, git 저장소의 요청서 파일로 주고받게 했다. 첫날 2시간 20분 만에 이관을 끝냈고 모든 경위가 기록으로 남았다.
  • 요청서는 사람 승인이 아니다. 받는 쪽은 바깥으로 나가는 행동 앞에서 멈추고, 요청서가 틀렸으면 반박한다.
  • 끌어가는 채널은 안 열면 영원히 대기한다. 6주 묵은 요청서와, bootout만 해서 다시 살아난 봇이 그 결과였다. 중요한 요청은 보낸 쪽이 다시 확인하고, 끌 때는 disable까지 한다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.