Claude Code 세션 여러 개, 누가 나를 기다리는지 한눈에 보기

작성 · 수정


tmux 창마다 Claude Code를 하나씩 띄워 놓고 쓰다 보니, 어느 세션이 승인을 기다리는지 모르고 방치하는 일이 잦았다. 별도 계측 없이 Claude Code가 이미 남기는 파일만으로 세션 상태·마지막 요청·사용 한도를 한 화면에 모은 기록. 결과물은 로컬 웹 대시보드 하나와 메뉴바 아이템 하나다.

문제: 탭을 돌면서 “다 됐나?” 확인하기

Claude Code를 한 세션만 쓸 때는 아무 문제가 없다. 세 개, 네 개를 동시에 돌리기 시작하면 다른 종류의 일이 생긴다.

  • A 세션이 도구 실행 승인을 기다리는데 나는 B 세션을 보고 있다. 5분 뒤에야 발견한다.
  • C 세션은 진작 끝났는데 안 봐서 다음 지시가 늦어진다.
  • 어느 창이 어느 프로젝트였는지, 마지막에 뭘 시켰는지 헷갈려서 창을 하나씩 열어 본다.

문제의 본질은 세션의 상태가 세션 안에만 있다는 것이다. 창을 열어야 보인다. 그래서 창 밖으로 꺼내기로 했다.

발견: 계측이 필요 없다

처음엔 훅으로 상태를 기록하는 계측 레이어를 만들 생각이었다. 그런데 ~/.claude/sessions/ 를 열어보니 이미 다 있었다.

// ~/.claude/sessions/<pid>.json — Claude Code 가 실시간으로 갱신한다
{
  "sessionId": "0b3e18a4-…",
  "cwd": "/Users/me/workspace",
  "tmux": "projects:@2.%10",
  "status": "busy",
  "name": "workspace-7b",
  "statusUpdatedAt": 1788999269241
}

세션마다 파일 하나. status 가 idle / busy / waiting 으로 바뀌고 tmux 필드에 세션·윈도우·pane 주소까지 들어 있다. pid 가 살아 있는지로 on/off 를 판정하면 끝이다.

마지막 요청은 트랜스크립트에서 뽑는다. ~/.claude/projects/<cwd를 -로 치환>/<sessionId>.jsonl 의 꼬리에서 마지막 user 메시지를 찾으면 된다. 도구 출력이 길면 꼬리 400KB 안에 없을 수 있어서 400KB → 4MB → 전체 순으로 범위를 넓힌다.

그래서 스캐너는 파이썬 한 파일이다. 이 파일을 웹과 메뉴바가 같이 쓴다.

~/.claude/sessions/*.json ─┐
~/.claude/projects/*/*.jsonl ┼─▶ cc_sessions.py (scan) ─┬─▶ cc-web.py  → 127.0.0.1:7777
tmux list-panes ────────────┘                           └─▶ SwiftBar 플러그인 (2초)
                                                              ▲
훅(Stop/Notification) → ~/.claude/cc-state/<sid> ─────────────┘  done / waiting 보조 신호

상태 판정: “확인했다”의 기준

status 만으로는 부족한 경우가 둘 있다.

완료(done)와 유휴(idle)의 구분. Claude Code 입장에선 둘 다 idle 이다. 하지만 나한테는 “끝났는데 아직 안 봤다”와 “봤고 다음 지시를 생각 중이다”가 다르다. 그래서 Stop 훅에서 그 pane 이 지금 포커스돼 있는지(#{window_active}#{pane_active} 둘 다 1)를 본다. 보고 있었으면 idle, 아니면 done. 나중에 그 pane 으로 이동하면 tmux 의 pane-focus-in 훅이 done 을 idle 로 되돌린다. “확인했다”의 기준을 pane 포커스로 정한 것이다.

승인 대기(waiting). 최신 버전은 status 에 waiting 을 직접 써 주지만, 그 전 버전을 위해 Notification 훅도 받는다. 여기 함정이 하나 있다. Notification 훅은 승인 요청과 60초 무응답 알림 두 경우에 다 발화한다. 구분 없이 waiting 으로 쓰면 완료 표시가 1분 만에 덮여 사라진다. payload 의 message 에 permission 이 들어 있을 때만 waiting 으로 보고, 무응답 알림은 기존 상태를 건드리지 않는다.

tmux 상태바에도 점을 찍는데, 처음에 tmux set -w 로 윈도우 옵션에 쓰다가 한 윈도우에 Claude pane 이 둘이면 서로 덮어쓰는 걸 봤다. pane 옵션(set -p)으로 바꾸고, 윈도우 대표 상태는 pane 들을 훑어 대기 > 완료 > 처리중 > 유휴 우선순위로 집계한다. 집계할 때 #{@cc} 포맷은 윈도우 값을 상속해서 못 쓰고 tmux show -p -v @cc 를 써야 pane 값만 나온다.

네 벌 만들고 두 벌을 버렸다

같은 스캐너 위에 화면을 네 가지로 만들어 봤다.

  1. tmux 팝업 (prefix + Tab, curses) — 제일 먼저 만들었고 제일 먼저 버렸다. 팝업을 열려면 결국 터미널에 있어야 한다.
  2. 로컬 웹 (:7777) — 카드 목록, 마지막 응답 펼치기, 다음 프롬프트 이어서 보내기, 상태 전이 알림. 같은 와이파이의 폰에서도 열린다. PWA 로 Dock 에 박아 뒀다.
  3. 바탕화면 위젯 (Übersicht) — 만들고 몇 시간 뒤에 버렸다. 이유는 아래.
  4. 메뉴바 (SwiftBar) — 지금 제일 자주 보는 것.

위젯 얘기를 조금 하면, 처음엔 macOS 정식 위젯(WidgetKit)을 생각했다. 안 된다. 타임라인 갱신을 시스템이 통제해서 사실상 분 단위고, 위젯 확장은 샌드박스라 ~/.claude/sessions 를 앱 그룹 우회 없이는 못 읽는다. “지금 승인 대기 중” 을 보는 용도엔 맞지 않는다. Übersicht 는 HTML 위젯이라 웹 화면을 거의 그대로 얹을 수 있었는데, 클릭이 먹으려면 enableInteraction 을 켜야 하고 그러면 바탕화면 클릭을 가로챈다. 메뉴바가 생기자 존재 이유가 없어졌다.

결론은 웹 + 메뉴바. 웹은 조작이 필요할 때, 메뉴바는 힐끔 볼 때.

메뉴바: 점 세 개와 숫자 하나

SwiftBar 는 스크립트의 표준 출력이 곧 메뉴다. 파일명의 2s 가 갱신 주기다.

메뉴바에 보이는 글자 — 세션 세 개를 뜻하는 점 ○●○ 와 5시간 창 사용률 11%

●◉○ 82%          ← 메뉴바에 보이는 글자
---
0.1  onsha-4f                 busy  2m
     피드백을 받을 수 있는 창구가 있으면 좋겠네
1.1  credit-system-kotlin-5f  done  34s
     Git 차이 확인
2.0  workspace-7b             idle  7m
     메뉴바의 이 부분은 최소화 해줘
---
한도
5시간  ▓▓▓▓▓▓▓▓░░  82%  13:10
주간   ▓░░░░░░░░░   8%  9/17 07:00
Fable  ▓░░░░░░░░░  13%  9/17 07:00
---
토큰
오늘    72.1M   out 312k
7일      1.4B   out 5.4M

드롭다운 — 세션마다 tmux 주소·이름·상태·경과 시간 한 줄과 마지막 프롬프트 한 줄, 아래에 5시간·주간·모델별 한도 막대와 오늘·7일 토큰 누계, 맨 밑에 웹 대시보드 링크

세션마다 점 하나 — ◆ 승인 대기, ● 처리중, ◉ 완료, ○ 유휴. 종료된 세션은 안 그린다. 점 뒤 숫자는 5시간 창 사용률이다. 줄을 누르면 그 pane 으로 이동하고 터미널이 앞으로 나온다.

작은 함정 둘. 한글은 Menlo 에서 두 칸이라 f"{s:<6}" 로 맞추면 열이 어긋난다 — 표시 폭으로 채워야 한다. 그리고 색은 color=밝은바,어두운바 쌍으로 준다. 밝은 메뉴는 반투명 회색 바탕이라 중간 회색이 묻혀서, 유휴 줄은 생각보다 훨씬 진한 회색까지 내려야 읽힌다.

둘째 줄에는 pane 제목이 아니라 마지막 프롬프트를 쓴다. Claude Code 가 pane 에 붙이는 제목은 세션 주제로 한 번 정해지고 잘 안 바뀌어서, 지금 뭘 시켰는지 보려면 프롬프트가 맞다. 처음엔 제목을 썼다가 “예전 걸 그대로 물고 있다”는 걸 며칠 뒤에 알았다.

한도와 토큰

세션 상태만 보다가 욕심이 났다. 5시간 한도가 얼마나 남았는지, 오늘 토큰을 얼마나 썼는지도 같은 자리에 있으면 좋겠다.

한도는 Claude Code 의 /usage 명령이 치는 엔드포인트를 그대로 부른다. 로그인 토큰은 키체인의 Claude Code-credentials 항목에 JSON 으로 들어 있다.

r = subprocess.run(["security", "find-generic-password",
                    "-s", "Claude Code-credentials", "-w"], capture_output=True, text=True)
token = json.loads(r.stdout)["claudeAiOauth"]["accessToken"]
req = urllib.request.Request("https://api.anthropic.com/api/oauth/usage", headers={
    "Authorization": f"Bearer {token}",
    "anthropic-beta": "oauth-2025-04-20",
})

응답의 limits[] 에 5시간·주간·모델별 주간이 percent, severity(normal/warning/critical), resets_at 과 함께 온다. 문서화된 API 는 아니다. 그래서 60초 캐시를 두고 실패하면 Claude Code 가 ~/.claude.json 의 cachedUsageUtilization 에 마지막으로 받아둔 값으로 버틴다. 그때는 “(캐시)” 표시가 붙는다.

토큰은 트랜스크립트 합산이다. type: assistant 줄의 message.usage 에 input / output / cache_read / cache_creation 이 있다. 306MB 를 훑는 데 0.4초 — 줄마다 JSON 파싱을 하기 전에 b'"usage"' 바이트가 있는지만 먼저 보는 프리필터 덕이다. 스트리밍으로 같은 메시지가 여러 줄 남을 수 있어서 (requestId, message.id) 로 중복을 거른다. 이것도 60초 캐시.

한계도 있다. 트랜스크립트는 일정 기간 뒤 지워지므로 “전체 누계”는 못 낸다. 오늘과 7일만 보여준다.

실전에서 배운 것들

  • 닫기 버튼에는 가드가 있어야 한다. 웹에서 pane 닫기를 만들면서 살아 있는 세션이 있으면 상태와 무관하게 거부하게 했는데, 이 가드가 없던 시절에 세션 3개를 날렸다. 하나는 대시보드 자기 자신, 둘은 검증 스크립트의 grep -w 가 @0 을 잘못 집어서. 유휴 세션도 막는다 — 닫으면 세션이 끝나고 --resume 을 쳐야 하니까.
  • 정렬은 tmux 배치 순서로 고정한다. 상태로 재정렬하면 카드가 튀어서 어느 게 뭔지 헷갈린다. 상태는 색과 점으로만 알린다.
  • 실행 중인 세션에 프롬프트를 넣는 공식 경로는 없다. claude 서브커맨드에도 없고, 세션 간 메시징은 세션 안에서만 된다. tmux send-keys -l 이 유일한 경로다. 셸 pane 에 보내면 그 문장이 셸 명령이 되므로 세션 없는 pane 은 거부하고 승인 대기 중엔 화면이 선택지 메뉴라 글자가 그 선택을 눌러버리므로 역시 거부한다.
  • 입력 중엔 재렌더를 멈춘다. 2초마다 카드를 다시 그리는 구조에서 입력창에 포커스가 있을 때 목록을 다시 그리면 한글 조합이 깨진다.
  • GUI 앱이 부르는 스크립트는 PATH 가 빈약하다. launchd 도, SwiftBar 도, Übersicht 도 로그인 셸을 안 거친다. 직접 실행하면 멀쩡한데 자동 실행에서만 빈 결과가 나오면 /opt/homebrew/bin 이 없는 걸 먼저 의심한다.
  • 실행 중인 세션은 설정을 메모리에 들고 있다. 설정 디렉터리 경로를 바꿨더니 떠 있던 세션들의 상태줄이 조용히 사라지고 훅도 멎었다. 시작할 때 읽은 명령 경로를 그대로 쓰기 때문이다. 옛 경로로 심링크를 하나 놓아 두고 세션이 전부 재시작된 뒤 지운다.
  • 클라우드에 올리면 안 된다. 데이터가 로컬 파일이라 서버리스에서 못 읽고, 억지로 밀어올리면 프롬프트 전문이 외부에 쌓인다. 예쁜 주소가 필요하면 /etc/hosts 별칭이면 된다. 웹은 0.0.0.0 에 열려 있어 같은 와이파이에선 보인다는 것도 기억해 둬야 한다.

정리

BeforeAfter
승인 대기 발견탭을 돌다가 우연히메뉴바 ◆ + 웹 알림
완료 확인세션마다 열어서안 본 것만 ◉, 보면 자동으로 ○
마지막 요청기억에 의존카드·메뉴 둘째 줄
사용 한도/usage 를 쳐서메뉴바 숫자 하나
계측훅으로 직접 기록할 생각Claude Code 가 남기는 파일을 읽기만

결국 새로 만든 건 읽는 쪽뿐이다. Claude Code 는 세션 상태와 트랜스크립트와 사용량을 이미 파일로 남기고 있었고, 나는 그걸 창 밖으로 꺼냈을 뿐이다. 파이썬 파일 셋(스캐너·웹·한도)과 셸 스크립트 몇 개, SwiftBar 플러그인 하나. 전부 로컬이고 서버가 없다.

여러 세션을 돌리는 사람이라면 ~/.claude/sessions/ 부터 열어 보길 권한다. 생각보다 많은 게 이미 거기 있다.