
이 글에서 다루는 내용
Claude Code를 PC 앞에 앉아서만 쓰고 계신가요? 출근길 지하철이나 외출 중에 텔레그램 메시지 한 줄로 Claude Code에 명령을 전달할 수 있다면, 워크플로 전체가 바뀝니다. 이 글에서는 Telegram Bot을 게이트웨이로 두고 WSL Ubuntu 위에서 동작하는 Claude Code CLI를 원격 호출하는 브리지 서버를 Python으로 직접 구축합니다. MCP 서버 방식의 한계를 피하고 Max 구독 쿼터를 그대로 활용하는 실전 구성이며, 아키텍처 설계·구현·세션 관리·보안 필터·systemd 상주화·트러블슈팅까지 한 편에 담았습니다.
1. 왜 Telegram Bot + CLI 브리지 방식인가
1-1. 세 가지 원격 접근 방식 비교
Claude Code에 원격 접근하는 방법은 크게 세 가지입니다.
| 방식 | 장점 | 단점 |
|---|---|---|
| Telegram MCP Server | 설정이 간단 | Claude가 실행 중이어야 동작, 외부 트리거 불가 |
| Telegram Bot + CLI 브리지 | 원격 트리거 가능, Max 구독 쿼터 그대로 사용 | 직접 구현 필요 |
| OpenClaw Telegram Skill | 빠른 셋업 | OpenClaw에 종속 |
1-2. 이 글이 채택한 선택
두 번째 방식, 즉 "텔레그램 메시지 → 봇 서버 → claude CLI 실행 → 결과 반환" 구조는 가장 유연하고 확장성이 높습니다. 특히 WSL에서 이미 Claude Code를 사용 중이라면 추가 인증 없이 바로 연결됩니다.
ℹ️ Anthropic 공식 Remote Control과 이 구성은 상호 배타적이지 않습니다. Remote Control은 Claude.ai 앱에서 편하게 이어받는 용도, 이 브리지는 외부에서 아예 트리거를 걸고 싶을 때의 보완재로 생각하면 됩니다.
2. 전체 아키텍처
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Telegram │───▶│ Bot Server │───▶│ Claude CLI │
│ (Mobile) │ │ (WSL Ubuntu) │ │ (subprocess) │
└─────────────┘ │ │ └─────────────────┘
▲ │ - 인증 필터 │ │
│ │ - 세션 관리 │ │
│ │ - 명령 검증 │ ▼
│ │ │ ┌─────────────────┐
└────────────│ 결과 포맷팅 │◀───│ Claude 응답 │
└──────────────────┘ └─────────────────┘
핵심 컴포넌트는 세 가지입니다.
- python-telegram-bot: 텔레그램 API 클라이언트 (비동기 지원).
- subprocess:
claude -p를 비동기로 호출. - 세션 딕셔너리: 사용자별
session-id를 JSON으로 영속화해 대화 컨텍스트 유지.
3. 사전 준비
3-1. Claude Code CLI 설치 확인
WSL Ubuntu에서 이미 Claude Code를 사용 중이라면 이 단계는 건너뜁니다.
$ claude --version
2.0.x (Claude Code)
$ which claude
/home/your-user/.npm-global/bin/claude
설치되어 있지 않다면 Claude Code 설치 및 활용 완벽 가이드를, WSL 자체가 아직이면 WSL2 설치와 Ubuntu 초기 세팅 가이드를 먼저 진행합니다.
3-2. Telegram Bot 토큰 발급
- 텔레그램에서
@BotFather검색 후 대화 시작. /newbot명령 입력.- 봇 표시 이름과 유저네임(
_bot으로 끝나야 함) 지정. - 발급된 HTTP API 토큰 복사 (형식:
8123456789:AAE...).
3-3. 본인 Telegram User ID 확인
보안상 본인 계정만 봇을 쓰도록 화이트리스트를 구성해야 합니다. @userinfobot에 /start를 보내면 숫자 ID가 반환됩니다.
Id: 123456789
First: Sehyun
Username: @your_username
3-4. Python 환경 준비
$ mkdir ~/claude-telegram-bridge && cd ~/claude-telegram-bridge
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install python-telegram-bot==21.6 python-dotenv
4. 프로젝트 구조
~/claude-telegram-bridge/
├── venv/
├── .env # 토큰 및 허용 사용자 ID
├── bot.py # 메인 봇 서버
├── claude_runner.py # Claude CLI 호출 래퍼
├── security.py # 명령 필터링
└── sessions.json # 세션 ID 영속화
5. 단계별 구현
5-1. 환경 변수 설정 (.env)
TELEGRAM_BOT_TOKEN=8123456789:AAEXXXXXXXXXXXXXXXXXXXXXXXX
ALLOWED_USER_IDS=123456789
CLAUDE_BIN=/home/your-user/.npm-global/bin/claude
WORKDIR=/home/your-user/projects
⚠️
.env는 절대 Git에 커밋하지 않습니다..gitignore에 반드시 추가합니다.
5-2. Claude CLI 호출 래퍼 (claude_runner.py)
import asyncio
import os
import json
from pathlib import Path
CLAUDE_BIN = os.getenv("CLAUDE_BIN", "claude")
WORKDIR = os.getenv("WORKDIR", str(Path.home()))
SESSION_FILE = Path(__file__).parent / "sessions.json"
def _load_sessions() -> dict:
if SESSION_FILE.exists():
return json.loads(SESSION_FILE.read_text())
return {}
def _save_sessions(sessions: dict) -> None:
SESSION_FILE.write_text(json.dumps(sessions, indent=2))
async def run_claude(user_id: int, prompt: str, timeout: int = 180) -> str:
"""Claude CLI를 비동기로 호출하고 결과 반환. 사용자별 session-id로 컨텍스트 유지."""
sessions = _load_sessions()
session_id = sessions.get(str(user_id))
cmd = [CLAUDE_BIN, "-p", prompt, "--output-format", "json"]
if session_id:
cmd.extend(["--resume", session_id])
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
cwd=WORKDIR,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
return "응답 시간 초과 (180초)."
if proc.returncode != 0:
return f"Claude 실행 실패: {stderr.decode()[:500]}"
try:
result = json.loads(stdout.decode())
new_session_id = result.get("session_id")
if new_session_id:
sessions[str(user_id)] = new_session_id
_save_sessions(sessions)
return result.get("result", "(빈 응답)")
except json.JSONDecodeError:
return stdout.decode()[:4000]
def reset_session(user_id: int) -> None:
sessions = _load_sessions()
sessions.pop(str(user_id), None)
_save_sessions(sessions)
핵심 포인트 네 가지입니다.
--output-format json으로session_id를 안정적으로 추출.--resume <id>로 이전 대화 이어감.asyncio.create_subprocess_exec로 블로킹 없이 여러 요청 동시 처리.- 타임아웃 180초: 리팩터링 같은 긴 작업도 수용.
5-3. 보안 필터 (security.py)
import re
DANGEROUS_PATTERNS = [
r"\brm\s+-rf\s+/",
r"\bsudo\b",
r"\b(curl|wget)\s+.*\|\s*sh",
r"\b:\(\)\s*\{", # fork bomb
r"\bmkfs\b",
r"\bdd\s+if=.*of=/dev/",
]
def is_prompt_safe(prompt: str) -> tuple[bool, str]:
"""프롬프트 단계에서 명백한 위험 패턴 1차 필터링."""
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, prompt, re.IGNORECASE):
return False, f"차단된 패턴: {pattern}"
return True, ""
5-4. 메인 봇 서버 (bot.py)
import os
import logging
from dotenv import load_dotenv
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import (
Application, CommandHandler, MessageHandler,
ContextTypes, filters,
)
from claude_runner import run_claude, reset_session
from security import is_prompt_safe
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
ALLOWED_USERS = {
int(uid.strip())
for uid in os.getenv("ALLOWED_USER_IDS", "").split(",")
if uid.strip()
}
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
def authorized(func):
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if user_id not in ALLOWED_USERS:
logger.warning(f"차단된 사용자 접근: {user_id}")
await update.message.reply_text("접근 권한이 없습니다.")
return
return await func(update, context)
return wrapper
@authorized
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Claude Code Bridge\n\n"
"메시지를 그대로 보내면 Claude가 실행합니다.\n"
"/reset - 대화 컨텍스트 초기화\n"
"/status - 현재 세션 상태 확인"
)
@authorized
async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
reset_session(update.effective_user.id)
await update.message.reply_text("세션이 초기화되었습니다.")
@authorized
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
prompt = update.message.text
user_id = update.effective_user.id
safe, reason = is_prompt_safe(prompt)
if not safe:
await update.message.reply_text(f"차단됨: {reason}")
return
await context.bot.send_chat_action(
chat_id=update.effective_chat.id, action=ChatAction.TYPING,
)
logger.info(f"[user {user_id}] {prompt[:80]}")
result = await run_claude(user_id, prompt)
for i in range(0, len(result), 4000):
await update.message.reply_text(result[i:i + 4000], parse_mode=None)
def main():
if not TOKEN:
raise RuntimeError("TELEGRAM_BOT_TOKEN이 설정되지 않았습니다.")
if not ALLOWED_USERS:
raise RuntimeError("ALLOWED_USER_IDS가 비어 있습니다.")
app = Application.builder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("reset", reset))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
logger.info("Bot is running...")
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
6. 실행 및 테스트
$ source venv/bin/activate
$ python bot.py
2026-04-20 15:30:12 - __main__ - INFO - Bot is running...
텔레그램에서 방금 만든 봇을 찾아 /start를 보내고, 이어서 실제 프롬프트를 전송합니다.
예상 대화 보기
Me: ~/projects/devpilot-lab 프로젝트에서 README.md를 요약해 줘
Bot: (3초 대기) DevPilot Lab은 IT 기술 블로그를 위한 정적 사이트 프로젝트입니다. 주요 카테고리는 AI & LLM, 개발환경 & 도구…
Me: 방금 본 README에서 "개발환경" 섹션을 더 자세히 알려 줘
Bot: (이전 컨텍스트 자동 유지) "개발환경 & 도구" 섹션은 VS Code, WSL, Docker 설정 가이드로 구성되어 있으며…
두 번째 메시지에서 --resume이 동작해 컨텍스트가 유지된 것을 확인할 수 있습니다.
7. 백그라운드 서비스로 등록 (systemd)
PC가 켜진 동안 항상 동작하도록 systemd 유닛을 등록합니다. WSL2는 /etc/wsl.conf에 systemd=true가 활성화되어 있어야 합니다.
$ sudo nano /etc/systemd/system/claude-telegram-bridge.service
[Unit]
Description=Claude Code Telegram Bridge
After=network.target
[Service]
Type=simple
User=your-user
WorkingDirectory=/home/your-user/claude-telegram-bridge
ExecStart=/home/your-user/claude-telegram-bridge/venv/bin/python bot.py
Restart=on-failure
RestartSec=5
StandardOutput=append:/home/your-user/claude-telegram-bridge/bot.log
StandardError=append:/home/your-user/claude-telegram-bridge/bot.err.log
[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now claude-telegram-bridge
$ sudo systemctl status claude-telegram-bridge
8. 보안 체크리스트
텔레그램으로 Claude Code를 원격 제어한다는 것은 사실상 메신저에 셸 접근 권한을 여는 것과 같습니다. 배포 전 아래 항목을 반드시 확인합니다.
| 항목 | 수준 |
|---|---|
ALLOWED_USER_IDS에 본인 ID만 등록 |
필수 |
.env를 .gitignore에 추가 |
필수 |
봇 토큰 유출 시 @BotFather에서 즉시 재발급 |
필수 |
| 위험 셸 패턴 필터링 적용 | 필수 |
WORKDIR을 프로젝트 루트로 제한 (홈 전체 금지) |
필수 |
| 파괴적 작업(rm, DB drop 등)에 인라인 키보드 재확인 | 권장 |
| 로그에 프롬프트 전문을 남기지 않기 (민감 정보 보호) | 권장 |
트러블슈팅
문제 1: subprocess에서 claude: command not found
systemd는 사용자 셸의 PATH를 상속하지 않습니다. .env의 CLAUDE_BIN에 절대 경로를 넣습니다.
$ which claude
/home/your-user/.nvm/versions/node/v22.0.0/bin/claude
# 이 경로를 CLAUDE_BIN에 그대로 복사
문제 2: Invalid API key / Please run /login
WSL에서 Claude Code가 OAuth 인증되어 있지 않은 상태입니다. 봇을 실행하는 동일 사용자 계정으로 먼저 로그인합니다.
$ claude # 대화형 모드로 실행 후 /login
헤드리스 환경에서는 claude setup-token으로 장기 OAuth 토큰을 발급받아 CLAUDE_CODE_OAUTH_TOKEN 환경변수로 주입합니다. ANTHROPIC_API_KEY와 동시에 사용하면 안 됩니다.
문제 3: 세션이 계속 초기화됨
sessions.json의 파일 권한과 systemd WorkingDirectory를 확인합니다.
$ ls -la ~/claude-telegram-bridge/sessions.json
$ sudo journalctl -u claude-telegram-bridge -n 50
문제 4: 응답이 너무 길어서 텔레그램이 거부
텔레그램 메시지 길이 제한은 4096자입니다. 코드에서 4000자 단위로 분할하고 있지만, 코드 블록(```)이 분할선에 걸리면 렌더링이 깨집니다. Markdown 파싱을 끄거나 긴 결과는 파일 첨부로 전환합니다.
if len(result) > 4000:
from io import BytesIO
bio = BytesIO(result.encode())
bio.name = "claude_result.txt"
await update.message.reply_document(document=bio)
return
문제 5: 타임아웃이 자주 발생
대규모 리팩터링이나 웹 검색이 포함된 프롬프트는 3분을 넘길 수 있습니다. claude_runner.py의 timeout을 600초로 늘리거나 프롬프트를 쪼개 보냅니다.
확장 아이디어
- 음성 입력: 텔레그램 음성 메시지 → Whisper STT → Claude 전달.
- Cron 예약: 매일 아침 "어제 커밋 요약해 줘" 자동 실행.
- 다중 프로젝트:
/project <name>명령으로WORKDIR전환. - 승인 플로우: 파일 쓰기/삭제 시 인라인 키보드로 Yes/No 재확인.
- MCP 확장: Claude Code에 MCP 서버 연결을 같이 붙여 도구 세트 확장.
마무리
이 구성의 진짜 강점은 Max 구독의 Claude Code 쿼터를 그대로 활용하면서 모바일 접근성을 얻는다는 점입니다. 별도 API 키 비용도, 외부 서비스 의존도 없습니다. WSL이 돌아가는 PC 한 대만 있으면 언제 어디서든 Claude Code가 주머니 안에 들어옵니다.
- 관련 글: Claude Code 설치 및 활용 완벽 가이드
- 관련 글: WSL2 설치와 Ubuntu 초기 세팅 가이드
- 공식 라이브러리: python-telegram-bot
- 공식 문서: Telegram Bot API
'AI & LLM' 카테고리의 다른 글
| 클로드 토큰 절약 방법 - 비용 레버 8가지 실전 정리 (1) | 2026.04.21 |
|---|---|
| MCP 서버 연동으로 Claude Code 확장하기 (1) | 2026.04.21 |
| 클로드 Remote Control 사용 방법 - 폰에서 내 PC의 Claude Code 이어 받기 (1) | 2026.04.20 |
| Claude Design 첫 사용기 (1) | 2026.04.20 |
| Claude Code 설치 및 활용 완벽 가이드 (0) | 2026.04.18 |