내가 Claude Code용 모바일 승인 시스템을 만든 방법, 이제 드디어 책상에서 떠날 수 있다

발행: (2026년 2월 28일 오후 03:01 GMT+9)
20 분 소요
원문: Dev.to

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)
  1. Claude가 PR을 생성하면 GitHub에 푸시됩니다.
  2. GitHub Actions가 PR 이벤트를 감지하고 Webhook을 트리거합니다.
  3. Webhook은 Firebase Cloud Functions 로 전달되어
    • FCM을 통해 모바일 디바이스에 푸시 알림을 보냅니다.
    • 승인/거절 요청을 처리할 수 있는 REST 엔드포인트를 제공합니다.
  4. 사용자는 모바일 앱에서 알림을 확인하고, 버튼을 눌러 승인 또는 거절을 전송합니다.
  5. 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 으로 요청을 전송합니다.

배포 및 테스트

  1. Firebase 프로젝트에 Functions 와 Cloud Messaging 을 활성화
  2. firebase deploy --only functions 로 함수 배포
  3. 모바일 디바이스에 Expo 혹은 React Native CLI 로 앱을 설치
  4. GitHub 레포에 PR을 열어 푸시 알림이 정상적으로 오는지 확인
  5. 승인/거절 버튼을 눌러 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 분)

  1. ntfy.sh 토픽 만들기 (예: myproject-perm).
  2. 훅 스크립트 추가~/.config/claude-code/hooks/PermissionRequest.sh에 넣고 실행 가능하게 만들기.
  3. 환경 변수 설정 (또는 스크립트를 편집)하기:
    • TOPIC – 만든 알림 토픽.
    • RESPONSE_TOPIC – 응답용 두 번째 토픽 (예: myproject-perm-resp).
    • WAIT_TIMEOUT – 응답을 기다릴 초 단위 시간 (기본값 30).
  4. 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 checkjqcurl이 사용 가능한지 확인합니다
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 registrationjq를 사용해 ~/.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 설정에서 훅을 제거하고 설치된 모든 파일을 정리합니다.

실제로 어떻게 보이는가

  1. Claude Code에 리팩터링을 요청하고, 책상에서 떠납니다.

  2. 몇 분 뒤에 휴대폰이 진동합니다:

    [myproject] Bash: npm run build
  3. 휴대폰에서 허용을 탭합니다.

  4. 돌아왔을 때, 빌드가 이미 완료되어 있습니다.

회의 중이거나, 산책 중이거나, 커피를 마시면서도 휴대폰만 있으면 명령을 승인하거나 거부할 수 있습니다.

Before / After Comparison

기능Before (터미널 전용)After (모바일 푸시)
승인 방법y / n in terminalAllow / Deny buttons on phone
자리를 비운 경우Claude stops and waitsHandle via push notification
제한 시간None (waits forever)Falls back to terminal after 90 s
보안Allowlist or manual checkCase‑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

참고 문헌

0 조회
Back to Blog

관련 글

더 보기 »

일이 정신 건강 위험이 될 때

markdown !Ravi Mishrahttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fu...