당사(주식회사 Joinclass)는 launchd의 주기적 작업 17개와 Claude Code를 이용해 회사 운영을 거의 자동화하고 있습니다. 출판, 마케팅, 개발 업무를 각각 다른 Claude Code 세션에 맡기고 있기 때문에, 어느 시점부터 세션 간의 전달이 병목 현상이 되었습니다.
예를 들어, '개발 세션에서 수정한 버그를 마케팅 세션으로 전달하여 릴리스 노트를 작성하게 하는' 같은 작업입니다. 이전에는 제가 두 터미널을 오가며 내용을 복사/붙여넣기 했습니다. 자동화율 98%를 자랑하는 회사임에도 불구하고, 이 부분만 사람이 직접 처리하고 있었던 것입니다.
본문에서는 Claude Code의 크로스 세션 메시징(Cross-Session Messaging)(ListAgents / SendMessage)을 사용하여 세션 간 연결하는 방법과, 대화 세션이 없는 무인 작업(launchd)에서도 같은 작업을 수행하는 방법을 당사에서 실제로 구동 중인 스크립트를 기반으로 소개합니다.
| 경로 | 송신자 | 수신자 | 사용 기능 |
|---|
| A | 대화 세션 | 동일 머신의 다른 대화 세션 | ListAgents → SendMessage |
| B | 대화 세션 | 자신이 구동한 서브 에이전트 | Agent의 반환값 ID → SendMessage |
| C | launchd 작업 (무인) | 새로 구동할 headless 세션 | claude -p + 파일 전달 |
A와 B는 Claude Code에 내장된 기능입니다. C는 당사에서 작성한 셸 스크립트입니다.
보내는 쪽 세션에서 다음과 같이 자연어로 요청합니다.
> 현재 이 머신에서 구동 중인 다른 Claude 세션을 일람해줘
Claude가 ListAgents 도구를 호출하고, 수신자 목록을 반환합니다. 표시되는 것은 자신이 구동한 서브 에이전트, 동일 머신에서 구동 중인 다른 세션, (연결되어 있다면) 클라우드의 세션이며, 각각 종류별 레이블이 붙습니다. SendMessage의 목적지에는 이 목록에 나온 이름을 그대로 사용합니다.
> one-ceo-marketing 세션에 'v1.4.2에서 note 게시물의 줄 바꿈 깨짐을 수정했어.
수정 내용은 git log -1 을 참조해줘. 릴리스 노트 초안을 .company/departments/marketing/ 에 만들어줘'라고 보내
내부적으로는 다음과 같은 도구 호출이 이루어집니다.
{
"to": "one-ceo-marketing",
"message": "v1.4.2에서 note 게시물의 줄 바꿈 깨짐을 수정했어. 수정 내용은 git log -1 을 참조해줘. 릴리스 노트 초안을 .company/departments/marketing/ 에 만들어줘"
...
}
- 같은 이름의 세션이 두 개 있을 경우 전송할 수 없습니다. 목록 이름 뒤에 붙는
[ref]까지 포함하여 지정해야 합니다. 리포지토리명으로 세션 이름을 정하는 경우, 같은 리포지토리에서 터미널을 2개 열었을 때 이런 현상이 발생합니다.
- 클라우드 세션은 회신할 수 없습니다. 받을 수는 있지만, 다시 보내는 수단이 아직 없습니다. '끝나면 답장해줘'라고 써도 답장이 오지 않으므로, 결과는 상대방의 트랜스크립트를 보고 확인합니다.
- 본문은 파일 경로만으로 합니다. 당사의 CLAUDE.md에서는 '파일 내용은 전달하지 않고, 경로만 전달한다'고 규정하고 있습니다 (Thin Orchestrator 원칙). 내용을 본문에 붙이면 수신자의 컨텍스트도 그만큼 소모됩니다.
Agent 도구로 구동한 서브 에이전트는 작업이 끝난 후에도 ID로 호출할 수 있습니다. 새로 Agent를 호출하면 컨텍스트가 0부터 시작하지만, SendMessage를 사용하면 그때까지의 조사 내용을 이어서 요청할 수 있습니다.
> 아까 ai-ceo-publisher 에이전트에게,
제3장만 도표를 추가하도록 추가 지시해줘
이러한 사용 구분을 통해, 당사의 출판 플로우에서는 장별 수정 시마다 시장 조사를 처음부터 다시 할 필요가 없어졌습니다.
launchd에서 동작하는 작업에는 말을 걸 대화 세션이 없습니다. 그래서 '보내는 쪽은 파일에 작성하고, 받는 쪽은 claude -p로 새로운 세션을 구동하여 읽는다'는 형태로 하고 있습니다.
당사의 작업은 모두 다음 래퍼를 통해 claude를 호출합니다.
#!/bin/bash
# claude를 무인 실행할 때의 공통 래퍼(wrapper)입니다. 실제 비용을 기록합니다.
#
...
--output-format json
으로 받은 JSON을 record-cost.py에 전달하고, 응답 텍스트는 표준 출력에, 실제로 발생한 비용(total_cost_usd)은 cost-tracker.json에 기록합니다.
try:
d = json.loads(raw)
except json.JSONDecodeError:
...
래퍼 위에 수신자별 메일박스(mailbox)를 배치합니다. 다음 두 함수는 당사의 운영 경험을 바탕으로 정리한 최소 구성이며, 복사하여 붙여넣기만 하면 바로 작동합니다.
#!/bin/bash
set -euo pipefail
source "$(dirname "$0") /lib/claude-run.sh"
...
사용 방법은 다음과 같습니다.
# 보내는 쪽 (예: 야간 테스트 작업)
./mailbox.sh mailbox_send marketing "테스트가 3건 실패함. .company/departments/dev/error-log.md를 읽고 요약해줘"
# 받는 쪽 (launchd로 15분마다 실행)
...
비어 있을 때: 당사에서 측정했을 때, 단순히 "2+2"를 질문하는 것만으로도 1회 $0.27~$0.47이 발생했습니다. 내용은 Claude Code 측의 고정 캐시 생성(약 41,000 토큰) 때문이며, 자체 CLAUDE.md나 스킬은 5,800 토큰밖에 없습니다. claude를 실행하지 마세요. 줄일 수 있는 것은 호출 횟수뿐이므로, [ -e "$msg" ] || return 0가 가장 비용 절감에 효과적입니다. -
받는 쪽 작업이 작성 중인 파일을 읽지 않도록 하기 위함입니다. mv로 원자적으로(atomically) 쓰세요. -
인증 정보를 읽지 않기 때문에, 무인 실행에서는 "Not logged in"으로 반드시 실패합니다. 당사에서 실제로 이렇게 멈춘 적이 있어, 래퍼의 주석에 남겨두었습니다. --bare는 사용하지 마세요.
export MAILBOX_ROOT=/tmp/mbtest
./mailbox.sh mailbox_send test "OK라고만 응답해줘"
ls /tmp/mbtest/test/new # 1건 있음
...
new/가 비어 있는 상태에서 mailbox_drain을 실행하고, cost-tracker.json에 새로운 기록이 늘어나지 않는 것도 확인하면 안심할 수 있습니다.
가장 큰 실패 원인은 메시지에 권한 지시를 빠뜨린 것입니다. 당사에서는 이전에 AI가 만든 영업 메일을 검토 없이 전송하여, 경어법에 어색한 메일이 거래처에 전달된 적이 있습니다. 그 이후로 외부 영향을 주는 작업은 draft → 승인(approval) → 실행의 흐름을 반드시 거치고 있습니다. 세션 간 메시지에서도 받는 쪽의 --allowedTools를 제한하고, 본문에 "draft까지만"이라고 명시하는 습관을 들이고 있습니다. -
세션을 늘리기 전에 CLAUDE.md에 어느 수준까지 공통 규칙을 작성할지 결정해 두는 것이 중요합니다. CLAUDE.md가 1,000줄을 초과했을 때 AI의 움직임이 이상했던 경험이 있어, 현재는 200줄 이내로 유지하고 상세한 규칙은 .claude/agents/에 분리하여 관리하고 있습니다.
세션이 늘어나면, 인간의 역할은 작업 그 자체보다 **세션 간의 전달(message passing)**이 되기 쉽습니다. 대화 세션끼리는 ListAgents → SendMessage를 사용하고, 무인 작업에서는 claude -p와 파일 메일박스를 구분하여 사용하면, 전달을 위해 사람이 직접 손을 움직일 필요가 거의 없어집니다.
여러 에이전트에게 어떻게 업무를 분배하고 어떻게 연계할지는 『Claude Code 멀티에이전트 개발』에서 자세히 설명합니다. 서브 에이전트 설계나 부서별로 에이전트를 분리할 때의 사고방식도 정리되어 있습니다.