내가 Claude Code용 모바일 승인 시스템을 만든 방법, 이제 드디어 책상에서 떠날 수 있다
Source: Dev.to
모바일 승인 시스템을 직접 구축한 이유 (Claude 코드용)
배경
회사에서 Claude를 사용해 코드를 자동으로 생성하고 리뷰하는 워크플로우가 있었습니다.
하지만 승인 단계가 데스크톱 웹 UI에만 국한돼 있어, 휴대폰으로는 승인할 수 없었습니다.
그 결과, 작은 작업이라도 책상에 앉아 있어야 했고, 생산성이 크게 떨어졌습니다.
목표
- 모바일에서 바로 Claude가 만든 코드를 승인/거절할 수 있는 UI 제공
- 푸시 알림을 통해 새로운 PR이 생겼을 때 즉시 알림 받기
- 기존 CI/CD 파이프라인과 최소한의 코드 변경으로 통합
전체 아키텍처
Claude (AI) → GitHub Repo → GitHub Actions →
└─> Webhook → Firebase Cloud Functions →
├─> FCM (Firebase Cloud Messaging) → Mobile App (React Native)
└─> GitHub API (Approve/Request changes)
- Claude가 PR을 생성하면 GitHub에 푸시됩니다.
- GitHub Actions가 PR 이벤트를 감지하고 Webhook을 트리거합니다.
- Webhook은 Firebase Cloud Functions 로 전달되어
- FCM을 통해 모바일 디바이스에 푸시 알림을 보냅니다.
- 승인/거절 요청을 처리할 수 있는 REST 엔드포인트를 제공합니다.
- 사용자는 모바일 앱에서 알림을 확인하고, 버튼을 눌러 승인 또는 거절을 전송합니다.
- Cloud Function이 GitHub API를 호출해 실제 승인/거절 작업을 수행합니다.
주요 구현 단계
1️⃣ GitHub Actions 설정
name: Notify on PR
on:
pull_request:
types: [opened, reopened, ready_for_review]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send webhook
uses: distributhor/workflow-webhook@v1
with:
webhook_url: ${{ secrets.FIREBASE_WEBHOOK_URL }}
payload: |
{
"pr_number": "${{ github.event.pull_request.number }}",
"title": "${{ github.event.pull_request.title }}",
"url": "${{ github.event.pull_request.html_url }}",
"author": "${{ github.event.pull_request.user.login }}"
}
FIREBASE_WEBHOOK_URL은 Firebase Cloud Functions 의 HTTP 트리거 URL입니다.- PR이 열리면 자동으로 webhook이 전송됩니다.
2️⃣ Firebase Cloud Functions 구현
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const fetch = require('node-fetch');
admin.initializeApp();
exports.handleGitHubWebhook = functions.https.onRequest(async (req, res) => {
const { pr_number, title, url, author } = req.body;
// 1️⃣ FCM 푸시 전송
const message = {
notification: {
title: `새 PR #${pr_number}`,
body: `${title} — ${author}`
},
data: {
pr_number: pr_number.toString(),
url
},
token: await getDeviceToken() // 디바이스 토큰을 DB에서 조회
};
await admin.messaging().send(message);
// 2️⃣ 승인/거절 엔드포인트 반환
res.status(200).send('Notification sent');
});
exports.approvePR = functions.https.onRequest(async (req, res) => {
const { pr_number, decision } = req.body; // decision: "approve" | "reject"
const githubToken = functions.config().github.token;
const apiUrl = `https://api.github.com/repos/your-org/your-repo/pulls/${pr_number}/${decision}`;
const response = await fetch(apiUrl, {
method: 'PUT',
headers: {
Authorization: `token ${githubToken}`,
Accept: 'application/vnd.github.v3+json'
}
});
if (response.ok) {
res.status(200).send('Success');
} else {
const err = await response.text();
res.status(500).send(err);
}
});
handleGitHubWebhook은 PR 정보를 받아 FCM 푸시를 보냅니다.approvePR은 모바일 앱에서 전송된 승인/거절 요청을 받아 GitHub API 를 호출합니다.
3️⃣ React Native 모바일 앱
import React, { useEffect, useState } from 'react';
import { View, Text, Button, Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import axios from 'axios';
export default function App() {
const [pr, setPr] = useState<any>(null);
useEffect(() => {
// 1️⃣ 푸시 권한 요청
messaging()
.requestPermission()
.then(() => console.log('Permission granted'));
// 2️⃣ 푸시 수신 핸들러
const unsubscribe = messaging().onMessage(async remoteMessage => {
const { pr_number, url } = remoteMessage.data;
setPr({ number: pr_number, url });
});
return unsubscribe;
}, []);
const handleDecision = async (decision: 'approve' | 'reject') => {
if (!pr) return;
try {
await axios.post('https://us-central1-your-project.cloudfunctions.net/approvePR', {
pr_number: pr.number,
decision,
});
Alert.alert('Success', `PR #${pr.number} ${decision}d`);
setPr(null);
} catch (e) {
Alert.alert('Error', e.message);
}
};
if (!pr) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>새 PR 알림을 기다리는 중...</Text>
</View>
);
}
return (
<View style={{ padding: 20 }}>
<Text>PR #{pr.number}</Text>
<Button title="Approve" onPress={() => handleDecision('approve')} />
<Button title="Reject" onPress={() => handleDecision('reject')} />
</View>
);
}
onMessage로 실시간 푸시를 받아 상태에 저장합니다.- 사용자가 Approve 혹은 Reject 버튼을 누르면 Cloud Function 으로 요청을 전송합니다.
배포 및 테스트
- Firebase 프로젝트에 Functions 와 Cloud Messaging 을 활성화
firebase deploy --only functions로 함수 배포- 모바일 디바이스에 Expo 혹은 React Native CLI 로 앱을 설치
- GitHub 레포에 PR을 열어 푸시 알림이 정상적으로 오는지 확인
- 승인/거절 버튼을 눌러 GitHub 에 실제로 상태가 반영되는지 검증
얻은 교훈
- 푸시 알림을 활용하면 사소한 승인 작업도 손쉽게 모바일에서 처리할 수 있다.
- Cloud Functions 를 중간에 두면 보안(GitHub 토큰 관리)과 확장성을 동시에 확보할 수 있다.
- UI/UX 를 최소화하고 버튼 하나만 제공하면, 사용자는 거의 무의식적으로 작업을 완료한다.
마무리
이제 나는 책상에 앉아 있을 필요 없이, 커피를 마시면서도 Claude가 만든 코드를 바로 승인할 수 있습니다.
작은 자동화가 일상적인 마찰을 크게 줄여 주었고, 팀 전체의 피드백 루프가 눈에 띄게 빨라졌습니다.
Tip: 같은 패턴을 다른 팀(예: 디자인 리뷰, 문서 승인)에도 적용하면 전사적인 효율성을 높일 수 있습니다.
모든 AI‑코딩‑에이전트 사용자가 직면하는 문제
커피를 들고 돌아왔을 때 Claude Code가 15분 동안 멈춰 있었습니다.
AI 코딩 에이전트가 자율성을 얻으며 — 코드를 작성하고, 빌드를 실행하고, 파일을 수정하는 — 인간의 감독이 중요해집니다. 에이전트가 계속 작업하도록 하고 싶지만, 동시에 무엇을 하고 있는지 알고 싶습니다. 권한 프롬프트가 체크포인트이면서 동시에 병목 현상이 됩니다.
- allowlist에 명령을 추가하면 프롬프트가 줄어들지만, 알 수 없는 명령과 파일 쓰기를 무차별적으로 승인하는 것은 위험합니다.
- 터미널에 계속 붙어 있는 것도 현실적이지 않습니다.
그래서 저는 claude‑push 를 만들었습니다 — Claude Code용 비동기 인간‑인‑루프 승인 레이어입니다. PermissionRequest 훅과 ntfy.sh 를 이용해 Allow/Deny 푸시 알림을 바로 휴대폰으로 보냅니다. 오픈 소스이며, 설정은 3분이면 충분합니다.
딜레마: 책상에 머물 것인가, 모두 허용할 것인가
Claude Code에 코드 생성 및 리팩터링을 위임하면, 다음과 같은 프롬프트를 보게 됩니다:
Claude wants to run: rm -rf dist && npm run build
Allow? (y/n)
이 메시지가 뜰 때마다 터미널로 돌아가 y를 눌러야 합니다. 집중 중이거나(또는 커피를 마시러 나갔을 때) 놓치면 Claude Code가 대기 상태가 됩니다.
| 접근 방식 | 장점 | 단점 |
|---|---|---|
| 터미널에서 수동 승인 | 안전함 | 책상을 떠날 수 없음 |
| 모든 것을 허용 목록에 추가 | 편리함 | 알 수 없는 명령이 통과함 |
| 모바일 푸시 알림 | 책상에서 멀리 떨어져도 안전 | 초기 설정 필요 |
claude‑push가 옵션 3을 현실로 만듭니다.
선행 기술
konsti‑web/claude_push 은(는) 동일한 문제를 해결했지만 Windows + PowerShell + 키스트로크 인젝션에 의존합니다 — macOS나 Linux에서는 작동하지 않습니다. 저도 같은 어려움을 겪었기 때문에 Bash + PermissionRequest hooks 를 사용해 처음부터 개념을 다시 만들었습니다.
How It Works: PermissionRequest Hook + ntfy.sh
Claude Code는 Hooks 시스템을 제공하여 특정 이벤트에 대해 외부 스크립트를 실행할 수 있습니다. PermissionRequest 이벤트에 훅을 등록하면 권한 프롬프트를 가로채어 직접 결정값을 반환할 수 있습니다.
Claude Code requests permission
→ Hook script fires
→ Sends notification with Allow/Deny buttons to ntfy.sh
→ Phone receives the push notification
→ You tap Allow or Deny
→ Response received via ntfy.sh SSE
→ Hook returns allow/deny JSON
→ Claude Code continues or stops
ntfy.sh는 무료 HTTP 기반 푸시 알림 서비스입니다. 계정이 필요 없으며, 토픽 이름만 알면 알림을 보내고 받을 수 있습니다. Pushover나 LINE Notify와 달리 curl 명령 하나로 알림을 전송할 수 있어 훅 스크립트에 최적화된 선택입니다.
구현 세부 사항
후크 스크립트는 약 60줄의 Bash로 구성됩니다. 아래는 핵심 설계 결정 세 가지입니다.
1. ntfy.sh HTTP 액션을 버튼에 사용
ntfy.sh는 액션 버튼을 지원합니다. 저는 버튼을 눌렀을 때 별도의 응답 토픽으로 POST 요청을 보내도록 http 액션을 사용합니다.
curl -s -H "Content-Type: application/json" \
-d "$(jq -n \
--arg topic "$TOPIC" \
--arg title "[$PROJECT] $TOOL_NAME" \
--arg message "$TOOL_INPUT" \
--arg allow_url "https://ntfy.sh/${RESPONSE_TOPIC}" \
--arg allow_body "allow|$REQ_ID" \
--arg deny_url "https://ntfy.sh/${RESPONSE_TOPIC}" \
--arg deny_body "deny|$REQ_ID" \
'{
topic: $topic,
title: $title,
message: $message,
priority: 4,
tags: ["lock"],
actions: [
{action:"http", label:"Allow", url:$allow_url, method:"POST", body:$allow_body},
{action:"http", label:"Deny", url:$deny_url, method:"POST", body:$deny_body}
]
}')" "https://ntfy.sh/"
핵심 상세: 알림 토픽과 응답 토픽은 별도의 채널입니다. 이렇게 하면 SSE 스트림이 자체 발송 알림으로 오염되는 것을 방지할 수 있습니다.
2. 응답 매칭을 위한 SSE + 요청 ID
알림을 보낸 뒤, 후크는 ntfy.sh SSE 엔드포인트에서 응답을 기다립니다.
REQ_ID="$(date +%s)-$$"
while IFS= read -r line; do
if [[ "$line" == data:* ]]; then
DATA="${line#data: }"
MSG=$(echo "$DATA" | jq -r '.message // empty' 2>/dev/null)
if [[ "$MSG" == *"|$REQ_ID" ]]; then
DECISION="${MSG%%|*}"
break
fi
fi
done < <(curl -s -N --max-time "$WAIT_TIMEOUT" \
-H "Accept: text/event-stream" \
"https://ntfy.sh/${RESPONSE_TOPIC}/sse")
REQ_ID(타임스탬프 + PID)는 알림 본문에 삽입되어 응답과 매칭됩니다. 이는 동시에 여러 권한 요청이 발생하더라도 각 응답이 올바른 요청에 연결되도록 보장합니다. 이 매커니즘이 없으면 이전 알림의 응답이 새 요청에 적용될 위험이 있습니다.
3. 타임아웃 시 터미널로 대체
타임아웃 기간 내에 응답이 도착하지 않으면 스크립트는 아무 출력도 하지 않고 종료 코드 0으로 종료합니다.
if [ "$DECISION" = "allow" ]; then
jq -n '{hookSpecificOutput:{decision:{behavior:"allow"}}}'
elif [ "$DECISION" = "deny" ]; then
jq -n '{hookSpecificOutput:{decision:{behavior:"deny"}}}'
fi
# Timeout: no output → falls back to interactive prompt
Claude Code의 후크 사양에서는 출력 없음 + 종료 코드 0이 “후크가 결정을 내리지 못함”을 의미하며, 이는 표준 터미널 프롬프트로 대체됩니다.
빠른 시작 (≈ 3 분)
- ntfy.sh 토픽 만들기 (예:
myproject-perm). - 훅 스크립트 추가를
~/.config/claude-code/hooks/PermissionRequest.sh에 넣고 실행 가능하게 만들기. - 환경 변수 설정 (또는 스크립트를 편집)하기:
TOPIC– 만든 알림 토픽.RESPONSE_TOPIC– 응답용 두 번째 토픽 (예:myproject-perm-resp).WAIT_TIMEOUT– 응답을 기다릴 초 단위 시간 (기본값 30).
- Claude Code 재시작 – 다음 권한 요청 시 휴대폰으로 푸시 알림이 전송됩니다.
이제 터미널을 계속 볼 필요 없이 어디서든 Claude Code 작업을 승인하거나 거부할 수 있습니다. 🎉
Source: …
Claude‑Push – Claude Code를 위한 모바일 승인
휴대폰을 보지 않아도, 타임아웃 후 터미널에서 승인을 처리할 수 있습니다. 권한이 조용히 부여되는 일은 없습니다.
설정
사전 요구 사항
- macOS 또는 Linux
bash,jq,curl설치됨- ntfy 앱이 휴대폰에 설치됨 (ntfy.sh)
설치
git clone https://github.com/coa00/claude-push.git
cd claude-push
bash install.sh
설치 프로그램은 다음 단계들을 안내합니다:
| 단계 | 수행 내용 |
|---|---|
| Dependency check | jq와 curl이 사용 가능한지 확인합니다 |
| Topic name input | 비워두면 무작위 토픽을 생성합니다 (예: claude-push-a1b2c3d4) |
| Config file creation | ~/.config/claude-push/config에 기록합니다 |
| Hook deployment | ~/.local/share/claude-push/hooks/claude-push.sh에 스크립트를 배치합니다 |
| Claude settings registration | jq를 사용해 ~/.claude/settings.json에 훅을 안전하게 병합합니다 |
| Test notification | 테스트 푸시를 전송해 모든 것이 정상 동작하는지 확인합니다 |
설치가 끝난 뒤 ntfy 앱에서 토픽을 구독하면 바로 사용할 수 있습니다.
설정 파일
~/.config/claude-push/config를 편집합니다. 재설치는 필요하지 않습니다.
# 토픽 이름 (ntfy.sh 채널의 공유 비밀 역할)
CLAUDE_PUSH_TOPIC="my-unique-topic"
# 타임아웃(초) (이 시간이 지나면 터미널 프롬프트로 전환)
CLAUDE_PUSH_TIMEOUT=90
검증
# Allow/Deny 버튼이 있는 테스트 알림 전송
bash scripts/test.sh test-notify
# 설치 상태 확인
bash scripts/test.sh status
status 명령의 예시 출력:
=== claude-push status ===
[OK] Config: ~/.config/claude-push/config
Topic: my-unique-topic
Timeout: 90s
[OK] Hook: ~/.local/share/claude-push/hooks/claude-push.sh
[OK] Settings: hook registered in ~/.claude/settings.json
[OK] Dependency: jq
[OK] Dependency: curl
제거
bash uninstall.sh
스크립트가 Claude 설정에서 훅을 제거하고 설치된 모든 파일을 정리합니다.
실제로 어떻게 보이는가
-
Claude Code에 리팩터링을 요청하고, 책상에서 떠납니다.
-
몇 분 뒤에 휴대폰이 진동합니다:
[myproject] Bash: npm run build -
휴대폰에서 허용을 탭합니다.
-
돌아왔을 때, 빌드가 이미 완료되어 있습니다.
회의 중이거나, 산책 중이거나, 커피를 마시면서도 휴대폰만 있으면 명령을 승인하거나 거부할 수 있습니다.
Before / After Comparison
| 기능 | Before (터미널 전용) | After (모바일 푸시) |
|---|---|---|
| 승인 방법 | y / n in terminal | Allow / Deny buttons on phone |
| 자리를 비운 경우 | Claude stops and waits | Handle via push notification |
| 제한 시간 | None (waits forever) | Falls back to terminal after 90 s |
| 보안 | Allowlist or manual check | Case‑by‑case approval per notification |
| 설정 비용 | — | bash install.sh in ~3 min |
보안 고려 사항
- ntfy.sh topic name은 공유 비밀입니다. 이를 아는 사람은 알림을 전송하거나 응답을 위조할 수 있습니다.
- 토픽 이름으로 무작위이며 추측하기 어려운 문자열을 사용하세요(설치 프로그램이 기본적으로 하나를 생성합니다).
- 보다 엄격한 제어를 위해 ntfy.sh access control를 구성하세요.
- 위험한 명령어(e.g.,
rm -rf …)를 허용 목록에 명시적으로 차단하여 추가적인 안전 계층을 유지하세요.
마무리
Claude Code의 기존 권한 시스템은 터미널을 직접 감시하거나 모든 것을 화이트리스트에 추가하도록 강요했습니다. claude-push는 세 번째 옵션 – 모바일 승인을 추가합니다.
- 구현: Bash + ntfy.sh HTTP 액션 + Server‑Sent Events.
- 훅 자체는 약 60줄 정도입니다.
- Claude Code의 Hooks API가
allow또는deny가 포함된 JSON 객체만을 기대하기 때문에 동작합니다.
이 패턴은 모든 AI 코딩 에이전트에 적용됩니다. 에이전트가 점점 더 강력해짐에 따라, 화면을 계속 바라볼 필요 없이 가볍고 비동기적인 승인 메커니즘이 필요합니다. 모바일 푸시 알림은 에이전트가 계속 진행될 수 있을 만큼 빠르고, 감독을 위해 충분히 눈에 띄는 최적의 지점에 도달합니다.
가장 좋은 개발자 도구는 화면을 5분 정도라도 멈추게 해주는 도구입니다. claude-push가 바로 그 도구입니다.
AI‑에이전트 권한을 어떻게 관리하고 계신가요? 화이트리스트를 선호하든 수동 검증을 옹호하든, claude-push를 한 번 사용해 보고 의견을 알려 주세요.
https://github.com/coa00/claude-push