데몬은 살아있는데 로그인만 죽는다
한 달마다 풀리는 OAuth에 조용히 죽는 상주 데몬들 — 토큰 삽질 끝에 헬스체크와 텔레그램 봇으로 안전망을 깔았다.
하려던 것: 한 달마다 조용히 죽는 데몬들
7월부터 맥미니에 레포별로 Claude Code remote-control 데몬을 launchd로 상시 띄워두고, 폰이나 아이패드에서 붙어 쓰고 있다. 그런데 어느 날 아이패드 목록이 텅 비었다. 프로세스는 KeepAlive 덕에 전부 살아 있는데, 인증이 풀려서 서비스 등록만 죽은 stale 상태. 죽었으면 재시작이라도 됐을 텐데, 산 채로 조용히 무용지물이 돼 있었다.
“항상 로그인 방식으로 하니까 한 달에 한 번씩 로그인이 다 풀려서 짜증 난다.”
목표는 두 개였다. 로그인이 안 풀리게 하거나 풀리면 즉시 알 것, 그리고 복구를 폰에서 할 것.
어떻게 시켰나: 토큰 삽질과 진범 규명
.zshrc에 넣어둔 CLAUDE_CODE_OAUTH_TOKEN을 데몬이 쓰게 해달라고 했다. Claude가 실측해 보니 시작부터 허무했다. export가 안 붙어 있어 자식 프로세스가 못 보고, 애초에 launchd는 .zshrc를 읽지도 않는다.
그래도 토큰 고정이 되면 근본 해결이니 검증을 시켰다. 가짜 토큰을 물리니 Keychain이 멀쩡한데도 401 — 환경변수가 Keychain보다 우선한다는 건 확인. 그런데 진짜 토큰을 물리자 403이 떴다. 만료가 아니라 스코프 부족. setup-token으로 만든 토큰에는 Remote Control에 필요한 세션 스코프가 아예 없어서, 이 방식은 원천적으로 불가였다. Claude는 재시작 시 전부 403으로 깨질 footgun이라며 토큰 파일을 즉시 걷어내고, 래퍼에 unset까지 박았다.
대신 진범이 나왔다. 예전 로그인에는 refreshToken이 없어서 access token이 만료되는 순간 그대로 사망했던 것. 새로 로그인한 자격증명에는 refreshToken이 있어 자동 갱신이 되어야 한다 — 다만 첫 사이클이라 정말 안 풀리는지는 한 달 지나봐야 안다고 했다.
어디서 막혔나: 살아있다는데 안 보인다
그래서 안전망부터 깔았다. 15분마다 연결을 체크해서 상태가 바뀔 때만 텔레그램으로 알리는 감시 에이전트. 그런데 이걸 붙이고도 “정상”과 “보임”이 계속 어긋났다.
첫 번째. 공시 요약 서비스 레포가 로컬에선 Connected인데 앱 목록에 안 떴다. 로그를 뒤지니 워커 등록 404. 진범은 bridge-pointer 파일이었다. 서버에서 이미 삭제된 죽은 환경을 포인터가 계속 붙잡고 재개를 시도하다 404가 나던 것. 백업 후 포인터를 지우니 새 환경으로 바로 떴다. 덤으로 예전 강제 종료 때 남은 worktree 잔재에서 미커밋 작업까지 발견해 보존했다.
두 번째는 며칠 뒤였다. 봇은 전부 ✅라는데 앱에는 일부만 보여서 버그 아니냐고 따졌더니, 두 화면이 다른 걸 재고 있었다. 봇의 ✅는 로컬 프로세스와 서버로의 연결 소켓. 앱 목록은 “세션이 있는 환경”만 보여준다. 등록만 되고 세션을 한 번도 안 만든 레포는 카드 자체가 안 뜬다. 앱 목록 ≠ 등록 목록. 버그가 아니라 측정 대상이 달랐다.
결과: 폰 속의 관제실
상태 확인·재시작·재로그인까지 전부 텔레그램으로 되냐고 물었더니, 앞의 둘은 가능하고 재로그인만 브라우저 OAuth라 불가 판정이 났다. 그래서 전용 봇을 파이썬으로 만들게 했다. 표준 라이브러리만 쓴 단일 스크립트를 launchd 상주 롱폴링으로 돌리고, /status /restart /logs에 그날의 포인터 사고를 자동 복구하는 /fix까지. 내 chat_id에만 응답하고, 대상 레포는 plist 자동 탐지라 레포를 늘려도 봇 코드는 안 고친다. 기존 봇 토큰을 재사용하느라 같은 봇을 폴링하던 예전 알림 에이전트는 내려야 했다(폴러가 둘이면 409 충돌).
그 사이 상주 레포는 5개에서 11개까지 불었다. 이제 침대에서 /status로 11개 상태를 보고 /restart로 살린다. refreshToken이 진짜 한 달을 버티는지는 지켜보는 중이다.
배운 점
- KeepAlive는 프로세스만 지킨다. 인증이 풀리면 프로세스는 산 채로 서비스만 죽는다 — “떠 있음”과 “붙어 있음”은 다른 신호다.
- 이름이 같은 토큰이라고 권한이 같지 않다. 401(만료)과 403(스코프 부족)은 전혀 다른 문제였다.
- 대시보드마다 재는 게 다르다. 봇의 ✅, 앱의 카드 목록, ps의 프로세스 줄 수는 각각 다른 걸 세고 있었다.
- 알림은 상태가 바뀔 때만 보내게 한다. 주기적으로 오는 알림은 곧 배경 소음이 된다.
- 근본 해결이 맞는지 검증에 한 달이 걸린다면, 그동안을 버틸 안전망(알림 + 원격 복구)을 먼저 깐다.