Claude Code 텔레그램 채널: 봇과 대화가 이어지는 원리
개발용 맥 한 대에 Claude Code를 늘 띄워 두고, 폰에서 일을 시키고 있습니다. 지금까지는 Claude 앱의 Code 탭(Remote Control)으로만 했는데, 이번에 텔레그램 봇을 하나 더 붙였습니다. 봇에게 「이 리포 상태 알려줘」라고 보내면 맥의 Claude Code가 받아서 일하고, 답이 다시 텔레그램으로 옵니다.
붙이고 나니 질문이 하나 남았습니다. 그 맥에는 공개 주소도, 바깥에 열어 둔 포트도 없습니다. 그런데 텔레그램에 보낸 메시지가 어떻게 맥의 터미널 세션 안까지 들어가는 걸까요. 플러그인 소스를 열어 보고 정리한 내용입니다.
폰 텔레그램 ──▶ 텔레그램 서버 ◀── 「새 메시지 있어?」 ── 봇 서버(Bun) ──▶ Claude Code 세션
(메시지 보관) ──▶ 답장 올리기 ──────── (플러그인) ◀── reply 도구맥으로 들어오는 연결이 하나도 없는 구조
핵심은 방향입니다. 텔레그램이 맥으로 메시지를 밀어 넣는 것이 아니라, 맥이 텔레그램 서버에 메시지를 가지러 갑니다.
플러그인은 봇 토큰으로 텔레그램 Bot API의 getUpdates를 계속 부릅니다. long polling이라는 방식입니다. 새 메시지가 없으면 텔레그램 서버가 요청을 바로 끝내지 않고 잠시 붙잡고 있다가, 메시지가 도착하는 순간 그 메시지를 실어 응답합니다. 그래서 폴링인데도 답이 늦지 않습니다.
이 구조 덕분에 맥 쪽에는 웹훅을 받을 공개 주소가 필요 없습니다. 방화벽에 구멍을 낼 것도 없습니다. 맥에서 텔레그램으로 나가는 HTTPS 연결뿐입니다.
대신 지켜야 할 규칙이 하나 생깁니다. 토큰 하나에 getUpdates를 부르는 쪽은 하나만 붙을 수 있습니다. 두 곳이 동시에 부르면 텔레그램이 한쪽에 409 Conflict를 돌려줍니다. 플러그인 소스에도 이 때문에 시작할 때 예전에 떠 있던 봇 프로세스를 먼저 정리한다는 주석이 있습니다. 같은 봇을 다른 서비스가 웹훅으로 받고 있다면 이 방식은 쓸 수 없습니다.
봇의 정체, Claude Code가 띄운 MCP 서버
텔레그램 플러그인은 Anthropic의 공식 플러그인 저장소에 있고, 본체는 server.ts 파일 하나입니다. Bun으로 돌아가는 MCP 서버입니다. Claude Code가 세션을 시작할 때 이 서버를 자식 프로세스로 띄웁니다.
보통의 MCP 서버는 Claude가 부를 때만 일합니다. 이 서버는 반대 방향도 씁니다. 텔레그램에서 메시지를 받으면 notifications/claude/channel이라는 알림으로 세션에 먼저 밀어 넣습니다. Claude 쪽에서 보면 대화창에 <channel>로 감싼 사용자 메시지가 하나 들어온 것과 같습니다.
이 알림은 세션을 채널 옵션으로 띄웠을 때만 받습니다.
claude --channels plugin:telegram@claude-plugins-official옵션 없이 띄운 세션에도 플러그인은 설치돼 있지만 메시지는 오지 않습니다. 제가 보기에는 아무 세션에나 바깥 메시지가 들어오면 안 되니, 받을 세션을 사람이 명시적으로 고르게 한 것입니다. 채널 기능은 아직 research preview 단계입니다.
답장은 플러그인이 Claude에게 주는 reply 도구로 합니다. Claude가 reply를 부르면 플러그인이 텔레그램의 sendMessage로 그 글을 올립니다. 메시지를 받는 순간 「입력 중…」 표시를 켜는 것도, 받은 메시지에 반응 이모지를 다는 것도 플러그인이 합니다.
허용 목록 밖의 메시지가 세션에 닿지 않는 이유
텔레그램 봇은 아이디만 알면 누구나 말을 걸 수 있습니다. 그런데 이 봇 뒤에는 파일을 고치고 명령을 실행하는 Claude Code가 있습니다. 그래서 플러그인은 메시지를 세션에 넘기기 전에 보낸 사람부터 확인합니다.
처음에는 짝 맺기(pairing) 방식입니다. 모르는 사람이 말을 걸면 봇이 6글자 코드를 답하고, 그 메시지는 버립니다. 세션에서 /telegram:access pair <코드>로 승인하면 보낸 사람의 숫자 ID가 허용 목록(access.json)에 들어갑니다. 아이디(username)가 아니라 숫자 ID를 쓰는 이유는 아이디는 바뀔 수 있고 숫자 ID는 바뀌지 않기 때문입니다.
저는 제 계정으로 짝 맺기를 끝내자마자 방식을 allowlist로 바꿨습니다. 이렇게 하면 목록에 없는 사람에게는 짝 맺기 코드도 보내지 않고 조용히 버립니다. 짝 맺기 상태로 두면 봇을 찾아낸 누구나 코드를 받을 수 있고, 그 코드를 승인할지는 결국 세션 안의 사람이 정합니다. 혼자 쓰는 봇이라면 열어 둘 이유가 없습니다.
허락이 필요한 작업은 텔레그램 버튼으로
Claude Code는 파일 수정이나 명령 실행처럼 허락이 필요한 작업 앞에서 멈추고 묻습니다. 터미널 앞에 사람이 없으면 이 질문이 그대로 걸려 있게 됩니다.
채널 플러그인은 이 질문도 텔레그램으로 옮겨 줍니다. 세션이 권한 요청을 플러그인에 넘기면 플러그인이 허용된 사람에게 승인·거절 버튼이 달린 메시지를 보냅니다. 버튼을 누르거나 yes abcde처럼 요청에 붙은 다섯 글자 코드와 함께 답하면, 그 결정이 세션으로 돌아가 작업이 이어집니다.
그래서 서버에서 미리 「전부 허용」으로 풀어 두지 않아도 폰으로 쓸 수 있습니다.
대화가 이어지는 범위는 세션 하나
텔레그램으로 보내는 메시지는 전부 같은 Claude Code 세션으로 들어갑니다. 그래서 앞에서 한 말이 맥락으로 이어집니다. 반대로 세션이 다시 뜨면 새 대화로 시작합니다. 맥을 재부팅했거나 세션이 죽어 자동으로 다시 떴다면 앞의 맥락은 없습니다.
텔레그램 Bot API에는 지난 메시지를 다시 가져오는 기능이 없습니다. 플러그인 README도 이 점을 분명히 적어 둡니다. 세션이 꺼져 있던 동안 보낸 메시지를 다시 뜬 세션이 받아 줄 거라고 기대하면 안 됩니다. 사진은 받는 순간 맥에 내려받아 두는데, 나중에는 가져올 방법이 없기 때문입니다.
음성 메시지는 글자로 바뀌지 않습니다. 플러그인은 음성 파일의 정보만 넘기고, 받아쓰기는 하지 않습니다. 말로 시키고 싶다면 폰 키보드의 받아쓰기로 입력해서 보내는 편이 확실합니다.
같은 맥에 Remote Control 세션도 따로 떠 있지만, 둘은 서로의 대화를 모릅니다. Remote Control은 Anthropic 서버를 거쳐 Claude 앱과 이어지고, 텔레그램 채널은 텔레그램 서버를 거칩니다. 같은 맥 안에서 각자 도는 세션 둘입니다.
붙이면서 걸린 두 가지
첫째는 설정 동기화였습니다. 저는 여러 기기의 Claude Code 설정을 한곳에서 맞추는 도구를 쓰고 있습니다. 플러그인을 설치하자 사용자 설정 파일(~/.claude/settings.json)의 enabledPlugins에 텔레그램이 켜졌고, 동기화 도구가 이것을 「올릴 변경」으로 잡았습니다. 그대로 올렸다면 Bun이 없는 다른 기기에서도 텔레그램 플러그인이 켜져 시작할 때마다 실패했을 겁니다. 기기마다 따로 켜는 플러그인 목록을 두고, 그 목록에 든 플러그인은 올리지 않게 고쳤습니다.
둘째는 늘 띄워 두는 방법이었습니다. 손으로 띄운 세션은 재부팅하면 사라지니 macOS의 launchd에 맡겼습니다. 그랬더니 텔레그램 세션은 멀쩡히 뜨는데, 같은 방식으로 띄운 Remote Control이 OAuth access token is invalid(401)로 실패했습니다. SSH로 들어가서 확인하면 로그인은 정상이었습니다.
원인은 인증 정보가 두 군데 있었다는 것입니다. SSH 세션에서는 로그인 키체인을 쓸 수 없어서 Claude Code가 ~/.claude/.credentials.json 파일을 씁니다. launchd가 띄운 프로세스는 로그인 키체인을 쓸 수 있으니 키체인을 읽었고, 거기에는 예전 로그인 정보가 남아 있었습니다. 지금 유효한 파일의 인증 정보를 키체인에 다시 넣고 나서야 두 세션이 같은 로그인으로 떴습니다.
하나는 아직 남았습니다. launchd의 사용자 에이전트는 그 사용자가 로그인해야 뜹니다. 그 맥은 자동 로그인이 꺼져 있어서, 재부팅한 뒤 누군가 화면에서 로그인하기 전까지는 봇이 답하지 않습니다.
봇을 처음 만들 때 BotFather가 아이디를 몇 번 거절했는데, 그 이야기는 BotFather "username is already taken": 텔레그램 봇 아이디 규칙과 해결에 따로 적었습니다.
우리 서비스에 적용할 계획이 있으신가요?
AI 챗봇·업무 자동화 살펴보기 →함께 읽기
- 텔레그램 알림 봇 만들기: BotFather 토큰부터 chat id까지예약해 둔 작업은 사람이 없는 시각에 서버가 혼자 처리합니다. 잘 끝나면 다행이지만, 중간에 막히면 그 작업은 조용히 대기 상태로 돌아갑니다. 까닭은 기록에 남는데, 기록을 열어 보기 전에는 아무도 모릅니다. 그래서 고객사 업무 시스템의 예약 작업에 텔레그램 알림을 붙였고, 그 알림을 받을 봇을 새로 만들기로 했습니다.
- BotFather "username is already taken": 텔레그램 봇 아이디 규칙과 해결개발용 맥에서 도는 Claude Code에 텔레그램 봇을 붙이려고 BotFather에게 /newbot을 보냈습니다. 이름을 넣고 아이디를 넣자 같은 답이 돌아왔습니다. 다른 아이디를 넣어도 또 같은 답이었습니다.
- code 명령: PATH에 없을 때 등록하는 두 가지 방법터미널에서 VS Code로 뭔가를 열려고 code .을 쳤는데 command not found: code가 떴습니다. VS Code는 이미 깔려 있었습니다. /Applications/Visual Studio Code.app에 앱 자체는 멀쩡히 있었고, 문제는 그 앱을 실행하는 code 명령이 PATH 어디에도 없다는 것…
- Claude에 MCP 커넥터를 직접 연결하는 방법Claude는 Gmail, Google Drive, Slack 같은 커넥터를 기본으로 제공합니다. 하지만 쓰고 싶은 서비스가 목록에 없다면 직접 등록할 수 있습니다. 커스텀 커넥터를 붙이면 대화 도중에 그 서비스의 기능을 바로 호출할 수 있습니다.
- 테스트용 LLM API 비용 비교: 로컬 모델에서 Gemini·Groq·Claude까지LLM 기능을 개발하면서 테스트는 몇 달째 로컬 모델로만 돌리고 있습니다. 대화, 툴을 부르는 에이전트, 문서에서 필드를 뽑는 구조화 추출, 이미지 인식, 요약까지 전부 자체 추론 서버 한 대에서 확인했습니다. 호출 비용이 0원이라 마음 놓고 몇백 번씩 돌릴 수 있다는 점이 가장 컸습니다. 그런데 기능이 늘수록 로컬 모…