# Claude Code 작업 끝나면 결과 파일까지 메일로 받기

> Claude Code가 긴 작업을 끝내면 결과 파일까지 메일로 보내주게 만드는 방법. Gmail 앱 비밀번호, msmtp와 키체인, 첨부 발송, 훅 연결, 예약 실행을 단계별로 정리하고 Remote Control·ntfy 같은 대안도 비교했습니다.

**Published:** 2026-08-16 | **Updated:** 2026-08-16

---


긴 작업을 시켜놓고 다른 일을 하다가, 한참 뒤에 터미널로 돌아와 보니 이미 30분 전에 끝나 있던 적 있으신가요. 반대로 5분마다 창을 들여다보느라 아무 일도 못 한 적도요.

제 경우엔 여기에 한 가지가 더 얹혀 있었습니다. **작업 결과가 파일로 나오는 경우**입니다. 조사 결과를 정리한 마크다운, 뽑아낸 CSV, 만들어둔 보고서 같은 것들요. 자리를 비운 사이 이게 끝나 있으면, 폰으로는 확인할 방법이 마땅치 않았습니다. SSH로 붙어서 `cat`을 치면 줄바꿈이 다 깨지고, 스크롤은 손가락으로 더듬어야 하고, 표라도 들어 있으면 사실상 못 읽습니다.

**결정적인 건 PPT나 워드 파일이었습니다.** 요즘은 발표 자료나 보고서 초안까지 만들게 시키는데, `.pptx`·`.docx`·`.xlsx`는 **텍스트 파일이 아닙니다.** 안을 열어보면 XML 여러 개를 담은 압축 파일이에요. 실제로 손에 있던 엑셀 파일을 풀어보니 `xl/workbook.xml`, `xl/worksheets/sheet1.xml` 같은 항목이 열 개 들어 있었습니다. 이런 걸 터미널로 `cat` 해봐야 깨진 글자만 쏟아집니다. 원격으로 붙어 있든 뭐든, **결국 그 파일을 여는 앱이 있어야 열립니다.** 폰에는 그 앱들이 다 있고요 — 파일만 도착하면요.

그래서 결국 **끝나면 파일째로 메일로 보내주게** 만들었습니다. 알림도 되고, 첨부가 오고, 폰에서 탭 한 번이면 키노트나 워드가 알아서 띄웁니다. 만들어보니 생각보다 걸리는 게 몇 개 있었는데 대부분 검색해도 잘 안 나오는 것들이라 정리해둡니다.

이 글은 네 가지를 다룹니다. Gmail로 메일을 보낼 수 있게 만드는 것(앱 비밀번호와 발송 도구), 결과 파일을 첨부하는 것, 그걸 Claude Code에 연결하는 것(훅), 그리고 매일 요약을 자동으로 받는 것(예약 실행)입니다. 명령어를 하나씩 다 적어뒀으니 그대로 따라 하시면 됩니다. 메일 말고 다른 방법은 없는지도 뒤에서 따로 비교했습니다.

전체 그림은 이렇습니다. 두 갈래가 같은 발송 경로를 나눠 쓰는 구조예요.

{{< img src="images/contents/mail-notify-flow.png" alt="작업 알림 메일 구조도 - 훅 갈래는 UserPromptSubmit이 시작 시각을 기록하고 Stop이 경과 시간을 재어 10분 넘은 턴만 통과시키며, 예약 갈래는 LaunchAgent가 매일 claude-digest를 실행합니다. 두 갈래 모두 claude-mail과 msmtp를 거쳐 Gmail SMTP로 나가고, 앱 비밀번호는 설정 파일이 아니라 키체인에 둡니다" >}}

> 이 글은 macOS 기준입니다. 리눅스도 흐름은 같지만 키체인 대신 다른 방법(`pass`, `gpg` 등)을 쓰게 됩니다. 그 부분은 마지막에 짚었습니다.

## 왜 메일인가 — 알림 방법을 고르는 기준

터미널 소리, 데스크톱 알림, 슬랙, 메일. 선택지는 많습니다. 저는 메일을 골랐는데 이유가 넷입니다.

**자리를 비워도 남습니다.** 데스크톱 알림은 놓치면 끝이고, 화면을 잠가두면 못 봅니다. 메일은 폰으로 따라옵니다.

**내용을 담을 수 있습니다.** "작업 완료" 한 줄이 아니라 무슨 작업이었고 뭐라고 끝냈는지를 본문에 넣을 수 있어요. 폰으로 메일만 읽고도 상황 파악이 됩니다.

**파일이 그대로 따라옵니다.** 이게 제겐 결정적이었습니다. 첨부를 열면 폰의 뷰어가 마크다운이든 CSV든 알아서 띄워줍니다. 터미널로 붙어서 읽는 것과는 경험이 아예 다릅니다. 게다가 메일함에 남으니 나중에 검색해서 다시 꺼낼 수도 있어요. 잠깐 떴다 사라지는 알림에는 없는 성질입니다.

**추가 계정이 필요 없습니다.** 슬랙 웹훅도 좋지만 워크스페이스가 있어야 하고, 개인 작업에는 과합니다.

단점도 분명합니다. 실시간성이 떨어지고, 설정이 제일 번거롭습니다. 특히 Gmail은 **일반 비밀번호로 로그인이 막혀 있어서** 처음 하면 여기서 한 번 막힙니다. 그 얘기부터 하겠습니다.

## 1단계: Gmail 앱 비밀번호 발급받기

프로그램에서 Gmail로 메일을 보내려면 SMTP라는 방식으로 구글 서버에 로그인해야 합니다. **SMTP**(Simple Mail Transfer Protocol)는 메일을 보내는 규약이라고만 알아두시면 충분합니다.

문제는 여기에 평소 쓰는 구글 비밀번호를 넣으면 **거부당한다**는 겁니다. 구글이 2022년부터 "보안 수준이 낮은 앱"의 접근을 막았거든요. 대신 앱마다 따로 발급받는 **앱 비밀번호**를 쓰게 되어 있습니다.

### 2단계 인증부터 켭니다

앱 비밀번호는 2단계 인증이 켜져 있어야 메뉴에 나타납니다. 안 켜져 있으면 아무리 찾아도 항목이 안 보여서 헤매게 돼요.

1. [Google 계정 보안 설정](https://myaccount.google.com/security)으로 갑니다
2. **2단계 인증**을 켭니다 (전화번호나 인증 앱으로)

### 앱 비밀번호를 만듭니다

1. [앱 비밀번호 페이지](https://myaccount.google.com/apppasswords)로 갑니다
2. 이름을 적습니다. 나중에 알아볼 수 있게 `msmtp`나 `claude-notify` 정도로요
3. **만들기**를 누르면 16자리가 나옵니다

여기서 중요한 게 하나 있습니다. **이 화면을 닫으면 다시 볼 수 없습니다.** 잃어버리면 새로 만드는 수밖에 없어요. 그러니 창을 열어둔 채로 다음 단계까지 진행하시는 걸 권합니다.

> 앱 비밀번호는 계정 비밀번호와 **관계없는 별도의 비밀번호**입니다. 이 16자리로는 지메일에 로그인할 수 없고, 메일 발송 같은 별도의 프로그램과 연결하기 위해 제한된 용도로만 쓰입니다. 유출되면 그 용도로만 악용되고, 계정에서 해당 항목만 삭제하면 즉시 무효화됩니다. 그래도 유출은 막아야 하니 다음 단계가 중요합니다.

## 잠깐 — 나머지는 Claude에게 시켜도 됩니다

여기까지 오셨다면 사실 **사람이 꼭 해야 하는 부분은 끝났습니다.** 앱 비밀번호 발급은 구글 계정에 로그인해서 버튼을 누르는 일이라 위임할 수가 없어요. 하지만 그다음부터는 파일 만들고 권한 주고 설정 넣는 일이라, Claude Code에게 그대로 맡길 수 있습니다.

터미널에서 `claude`를 띄우고 아래를 통째로 붙여넣으세요.

````text
macOS에서 Claude Code 작업 알림을 메일로 받는 환경을 만들어줘. 아래 순서대로 진행해줘.

1. msmtp 설치 (`brew install msmtp`). 이미 있으면 건너뛰기.
2. `~/.msmtprc` 작성. 계정은 Gmail(smtp.gmail.com, 포트 587, tls on, auth on),
   from·user는 내 지메일 주소. 비밀번호는 **파일에 절대 적지 말고**
   `passwordeval "security find-generic-password -s msmtp-gmail -a <내주소> -w"`
   로 키체인에서 읽어오게 해줘. logfile은 `~/.msmtp.log`.
   작성 후 `chmod 600 ~/.msmtprc`.
3. `~/.claude/bin/claude-mail` 작성 — 제목은 인자, 본문은 stdin으로 받아 msmtp로 발송.
   `Content-Type: text/plain; charset=UTF-8` 헤더를 반드시 넣어줘(없으면 한글이 깨져).
   키체인에 항목이 없으면 에러 대신 `exit 0`으로 조용히 끝나게 해줘.
4. `~/.claude/bin/claude-mail-attach` 작성 — 3번과 같되 인자로 받은 파일들을 첨부.
   msmtp는 MIME 조립을 못 하니 파이썬 `email.message.EmailMessage`로 조립한 뒤
   msmtp에 넘겨줘. 없는 파일은 경고만 남기고 건너뛰게.
   둘 다 `chmod +x`.
5. 훅 두 개 작성 — `UserPromptSubmit`에서 세션별 시작 시각을 임시 폴더에 기록하고,
   `Stop`에서 경과 시간을 재서 임계값(환경변수 `CLAUDE_NOTIFY_AFTER_SEC`, 기본 600초)을
   넘을 때만 `claude-mail`로 발송. **시간 게이트가 없으면 매 턴 메일이 와서 스팸이 되니
   이건 반드시 넣어줘.** 그다음 `~/.claude/settings.json`에 두 훅을 등록해줘.
   기존 설정이 있으면 덮어쓰지 말고 병합해줘.
6. 매일 요약 메일용 LaunchAgent를 `~/Library/LaunchAgents/`에 만들고 로드해줘.
   **cron은 쓰지 마.** macOS의 cron은 로그인 키체인을 못 읽어서 조용히 실패해.
   시각은 정각을 피해서 잡아줘.

중요:
- **앱 비밀번호를 나에게 묻지 말고, 어떤 파일에도 적지 마.** 키체인 등록
  (`security add-generic-password ...`)은 내가 직접 실행할 테니, 실행할 명령만 알려줘.
- 각 단계에서 뭘 왜 하는지 한 줄씩 설명해줘.
- 마지막에 발송 테스트 방법을 알려줘.
````

Claude가 파일을 다 만들어두면, 키체인에 비밀번호를 넣는 한 줄만 직접 실행하시면 됩니다. **비밀번호를 만지는 일만 사람 몫**인 거예요. 왜 그래야 하는지는 바로 다음 절에서 설명합니다.

> **그래도 아래를 읽으시길 권합니다.** 이 프롬프트는 "만들어주는 것"이지 "이해시켜주는 것"이 아니거든요. 메일이 안 오기 시작하면 결국 어디가 어떻게 물려 있는지 알아야 고칩니다. 그리고 이 글의 나머지 절반은 **만들면서 걸렸던 함정들**입니다 — 키체인 창의 버튼 하나, `Stop` 훅의 시간 게이트, cron이 조용히 실패하는 이유. Claude가 알아서 피해 가더라도 왜 그렇게 됐는지는 알아두시는 게 좋습니다.

## 2단계: 발송 도구 설치하고 비밀번호는 키체인에 넣기

메일을 보내려면 그 일을 해줄 프로그램이 필요합니다. **msmtp**를 쓰겠습니다. 가볍고, 설정 파일 하나면 되고, 무엇보다 **비밀번호를 파일에 안 적어도 되는** 기능이 있습니다.

```bash
brew install msmtp
```

### 왜 비밀번호를 파일에 적으면 안 되나

msmtp 설정 파일에 `password 여기에16자리` 라고 적어도 동작은 합니다. 많은 안내글이 그렇게 되어 있고요. 그런데 이러면 세 가지가 걸립니다.

- 홈 디렉터리를 통째로 백업하거나 클라우드에 동기화하면 비밀번호가 함께 갑니다
- 설정 파일을 dotfiles 저장소로 관리하는 분이라면 그대로 깃허브에 올라갑니다
- 화면 공유나 스크린샷 한 번에 노출됩니다

그래서 macOS **키체인**에 넣습니다. 키체인은 운영체제가 관리하는 비밀번호 금고예요. 터미널에서 다음 명령을 실행하면 비밀번호를 물어보는데, **입력해도 화면에 보이지 않습니다.** 입력이 되고 있는거야? 라고 생각하실수도 있겠지만, 정상입니다.

```bash
security add-generic-password -s msmtp-gmail -a you@gmail.com -w
```

`-s`는 항목 이름(서비스), `-a`는 계정, `-w`는 비밀번호를 입력받겠다는 뜻입니다. 아까 발급받은 16자리를 붙여넣고 엔터를 치면 됩니다. 공백은 빼고 넣으세요. (you@gmail.com 를 본인 이메일로 수정 후 실행하세요.)

잘 들어갔는지 확인하려면 이렇게 합니다.

```bash
security find-generic-password -s msmtp-gmail -a you@gmail.com -w
```

비밀번호가 화면에 그대로 찍히니 **다른 사람이 볼 수 있는 상황에서는 실행하지 마세요.**

### msmtp 설정 파일 작성

이제 `~/.msmtprc` 파일을 만듭니다. 아래를 그대로 붙여넣고 주소만 바꾸시면 됩니다.

```bash
# msmtp — 작업 알림 메일 발송용
#
# 앱 비밀번호는 이 파일에 두지 않는다. macOS 키체인에서 읽어온다.

defaults
auth            on
tls             on
tls_trust_file  system
logfile         ~/.msmtp.log

account         gmail
host            smtp.gmail.com
port            587
from            you@gmail.com
user            you@gmail.com
passwordeval    "security find-generic-password -s msmtp-gmail -a you@gmail.com -w"

account default : gmail
```

핵심은 마지막에서 두 번째 줄인 **`passwordeval`** 입니다. 비밀번호를 적는 대신 **비밀번호를 가져오는 명령**을 적어두는 거예요. msmtp가 발송할 때마다 이 명령을 실행해 키체인에서 꺼내옵니다. 파일에는 비밀번호가 없습니다.

파일 권한도 잠가둡니다.

```bash
chmod 600 ~/.msmtprc
```

> "msmtp는 권한이 느슨하면 실행을 거부한다"는 설명을 종종 보는데, 실제로 확인해보니 **평문 `password`를 적었을 때만** 그렇습니다. 우리처럼 `passwordeval`만 쓴 파일은 권한이 644여도 그냥 돌아갑니다(msmtp 1.8.34에서 확인). 즉 이 `chmod`는 msmtp가 강제해서 하는 게 아니라 **우리가 알아서 챙기는 것**입니다. 설정 파일 자체에 비밀번호가 없더라도 계정 주소와 서버 정보는 굳이 남에게 보일 이유가 없으니까요.

### 첫 발송 테스트

```bash
printf 'Subject: 테스트\n\n안녕하세요\n' | msmtp you@gmail.com
```

여기서 **macOS가 키체인 접근을 허용할지 묻는 창**이 뜹니다. 이때 반드시 **"항상 허용"**\ 을 누르세요. "허용"만 누르면 발송할 때마다 창이 뜨고, 나중에 자동 실행에서는 창이 뜬 채로 멈춰버립니다. 이 버튼 하나 때문에 "왜 자동으로는 안 되지?"로 한참 헤맬 수 있습니다.

받은편지함에 메일이 왔으면 절반은 끝난 겁니다. 안 왔다면 로그를 보세요.

```bash
tail -20 ~/.msmtp.log
```

`authentication failed`가 보이면 앱 비밀번호가 틀렸거나 공백이 섞인 겁니다. 키체인에 다시 넣으려면 기존 항목을 지우고 등록하세요.

```bash
security delete-generic-password -s msmtp-gmail -a you@gmail.com
security add-generic-password -s msmtp-gmail -a you@gmail.com -w
```

## 3단계: 보내기 편한 명령 만들기

매번 `printf`로 헤더를 조립하는 건 번거로우니 짧은 스크립트로 감싸둡니다. `~/.claude/bin/claude-mail`로 저장하겠습니다.

```bash
#!/bin/bash
# 작업 알림 메일 발송. 제목은 인자, 본문은 stdin.
#   echo "본문" | claude-mail "제목"
#
# 키체인에 앱 비밀번호가 없으면 조용히 넘어간다. 알림 실패가 작업을 막으면 안 된다.

set -u
TO="${CLAUDE_NOTIFY_TO:-you@gmail.com}"
SUBJECT="${1:-Claude Code 알림}"
MSMTP="$(command -v msmtp || echo /opt/homebrew/bin/msmtp)"

[ -x "$MSMTP" ] || { echo "msmtp 없음" >&2; exit 0; }
security find-generic-password -s msmtp-gmail -a "$TO" -w >/dev/null 2>&1 || {
  echo "키체인에 msmtp-gmail 항목 없음 — 발송 건너뜀" >&2; exit 0; }

BODY="$(cat)"
{
  printf 'To: %s\n' "$TO"
  printf 'Subject: %s\n' "$SUBJECT"
  printf 'Content-Type: text/plain; charset=UTF-8\n'
  printf '\n%s\n' "$BODY"
} | "$MSMTP" -t 2>>"$HOME/.msmtp.log"
```

저장한 뒤 실행 권한을 줍니다.

```bash
mkdir -p ~/.claude/bin
chmod +x ~/.claude/bin/claude-mail
```

두 군데가 의도적입니다.

**`Content-Type` 줄**이 없으면 한글 본문이 깨져서 도착합니다. UTF-8을 명시해야 합니다.

제목은 사정이 조금 다릅니다. 엄밀히 따지면 메일 헤더에 한글을 그대로 넣는 건 규격상 맞지 않고, `=?utf-8?B?...?=` 같은 형태로 인코딩하는 게 정석이에요. 그런데 요즘 메일 서버는 대부분 UTF-8을 그대로 받아주기 때문에 실사용에서는 위 스크립트로도 한글 제목이 멀쩡히 도착합니다. 저도 몇 달째 이대로 쓰고 있어요. 다만 회사 메일 서버처럼 오래된 환경으로 보낼 일이 있다면 뒤에 나올 첨부 버전을 쓰세요. **그쪽은 제목 인코딩까지 알아서 처리합니다.**

**키체인 항목이 없으면 `exit 0`으로 조용히 끝냅니다.** 알림은 곁다리 기능인데 이게 실패해서 본 작업이 멈추면 곤란하거든요. 나중에 훅으로 물리면 이 판단이 특히 중요해집니다.

이제 이렇게 쓸 수 있습니다.

```bash
echo "본문입니다" | ~/.claude/bin/claude-mail "제목입니다"
```

### 결과 파일을 첨부해서 보내기

여기서 한 가지 한계에 부딪힙니다. **msmtp는 파일을 첨부해주지 않습니다.** msmtp는 "완성된 메일을 서버로 전달하는" 역할만 하는 도구예요. 첨부가 있는 메일은 **MIME**이라는 형식으로 여러 조각을 묶어서 만들어야 하는데, 그 조립은 msmtp의 일이 아닙니다. `msmtp --help`를 봐도 첨부 관련 옵션은 없습니다.

그래서 조립만 파이썬에게 맡기고 발송은 그대로 msmtp에 넘깁니다. 표준 라이브러리의 `email` 모듈이 MIME 조립을 다 해주니 추가로 설치할 패키지는 없습니다.

> 파이썬이 "macOS에 기본으로 들어 있다"는 말을 종종 보는데, 정확히는 **Xcode Command Line Tools에 딸려 옵니다.** 앞에서 Homebrew로 msmtp를 설치하셨다면 Homebrew가 이 도구 모음을 요구하므로 이미 깔려 있습니다. `python3 --version`이 버전을 뱉으면 준비된 겁니다.

`~/.claude/bin/claude-mail-attach`로 저장합니다.

```bash
#!/bin/bash
# 파일을 첨부해서 보내는 버전. 본문은 stdin, 첨부는 인자.
#   echo "본문" | claude-mail-attach "제목" 보고서.md 결과.csv

set -u
TO="${CLAUDE_NOTIFY_TO:-you@gmail.com}"
SUBJECT="${1:-Claude Code 알림}"; shift || true
MSMTP="$(command -v msmtp || echo /opt/homebrew/bin/msmtp)"

[ -x "$MSMTP" ] || { echo "msmtp 없음" >&2; exit 0; }
security find-generic-password -s msmtp-gmail -a "$TO" -w >/dev/null 2>&1 || {
  echo "키체인에 msmtp-gmail 항목 없음 — 발송 건너뜀" >&2; exit 0; }

BODY="$(cat)"
python3 - "$TO" "$SUBJECT" "$BODY" "$@" <<'PY' | "$MSMTP" -t 2>>"$HOME/.msmtp.log"
import sys, mimetypes, pathlib
from email.message import EmailMessage

to, subject, body, *files = sys.argv[1:]
msg = EmailMessage()
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)                    # 한글 본문 인코딩은 여기서 알아서 처리된다

for f in files:
    p = pathlib.Path(f)
    if not p.is_file():
        print(f"첨부 건너뜀(파일 없음): {f}", file=sys.stderr)
        continue
    ctype, _ = mimetypes.guess_type(p.name)
    maintype, _, subtype = (ctype or "application/octet-stream").partition("/")
    msg.add_attachment(p.read_bytes(), maintype=maintype,
                       subtype=subtype, filename=p.name)

sys.stdout.write(msg.as_string())
PY
```

`chmod +x ~/.claude/bin/claude-mail-attach`로 실행 권한을 주고 이렇게 씁니다.

```bash
echo "조사 끝났습니다. 첨부 확인해주세요." \
  | ~/.claude/bin/claude-mail-attach "[Claude] 조사 결과" 결과보고서.md 요약.csv
```

앞의 `claude-mail`과 달라진 부분만 짚겠습니다.

**`set_content`가 UTF-8을 대신합니다.** 앞에서는 `Content-Type` 헤더를 손으로 적었죠. 여기서는 `EmailMessage`가 본문을 보고 필요한 헤더와 인코딩을 알아서 붙입니다. 손으로 적을 필요도, 적어서도 안 됩니다.

**MIME 타입은 파일 확장자로 추측합니다.** `mimetypes.guess_type`이 `.md`를 보고 `text/markdown`을, `.csv`를 보고 `text/csv`를 돌려줍니다. 모르는 확장자면 `application/octet-stream`으로 떨어지는데, 이건 "그냥 바이너리 파일"이라는 뜻이라 첨부는 정상적으로 됩니다.

**없는 파일은 건너뜁니다.** 경고만 남기고 계속 진행해요. 파일 하나가 없다고 메일 전체가 안 가면 알림으로서 실격입니다.

실제로 마크다운과 CSV를 붙여 보내봤더니, 받은편지함에 **한글 파일명 그대로** 도착했고 타입도 각각 `text/markdown`·`text/csv`로 제대로 잡혔습니다. 존재하지 않는 파일을 인자로 넣어도 나머지 둘은 정상 발송됐고요.

> **용량 주의**: Gmail 첨부 상한은 **25 MB**입니다. 그리고 메일은 첨부를 Base64로 실어 나르기 때문에 실제 전송량이 원본보다 **약 33% 커집니다.** 큰 결과물이라면 압축하거나(`zip`), 파일 대신 요약과 경로만 보내는 편이 낫습니다.

## 4단계: Claude Code에 연결하기 — 훅

여기서부터가 진짜입니다. **훅**(hook)은 Claude Code가 특정 시점에 자동으로 실행해주는 명령이에요. "턴이 끝나면 이 스크립트를 돌려라" 같은 걸 걸어둘 수 있습니다.

쓸 훅은 둘입니다.

| 훅 | 언제 발동하나 | 여기서 할 일 |
|---|---|---|
| `UserPromptSubmit` | 사용자가 메시지를 보낼 때 | 시작 시각 기록 |
| `Stop` | Claude가 응답을 마칠 때 | 경과 시간 재서 메일 발송 |

### 왜 시각을 따로 기록하나 — 이게 핵심입니다

`Stop` 훅은 **턴이 끝날 때마다** 발동합니다. "안녕"이라고 한 마디 물어봐도 발동해요. 조건 없이 메일을 보내면 하루에 수십 통이 날아옵니다.

그래서 **시작 시각을 기록해뒀다가 경과 시간이 임계값을 넘을 때만** 보냅니다. 이 게이트가 없으면 알림 시스템이 아니라 스팸 발생기가 됩니다.

### 시작 훅

`~/.claude/hooks/notify-start.sh`:

```bash
#!/bin/bash
# UserPromptSubmit — 이 턴의 시작 시각을 기록해 둔다.
set -u
D="${TMPDIR:-/tmp}/claude-notify"; mkdir -p "$D"
SID="$(python3 -c 'import json,sys;print(json.load(sys.stdin).get("session_id","unknown"))' 2>/dev/null || echo unknown)"
date +%s > "$D/$SID.start"
exit 0
```

훅은 표준입력으로 JSON을 받습니다. 거기서 세션 ID를 꺼내 그 이름으로 시작 시각을 파일에 적어둡니다. 세션마다 파일이 따로 생기니 여러 창을 띄워도 섞이지 않습니다.

### 종료 훅

`~/.claude/hooks/notify-stop.sh`:

```bash
#!/bin/bash
# Stop — 이 턴이 임계값보다 오래 걸렸을 때만 메일을 보낸다. 기본 10분.
set -u
THRESHOLD="${CLAUDE_NOTIFY_AFTER_SEC:-600}"
D="${TMPDIR:-/tmp}/claude-notify"

IN="$(cat)"
read -r SID CWD <<EOF
$(printf '%s' "$IN" | python3 -c '
import json,sys
d=json.load(sys.stdin)
print(d.get("session_id","unknown"), d.get("cwd","?"))
' 2>/dev/null || echo "unknown ?")
EOF

F="$D/$SID.start"
[ -f "$F" ] || exit 0
START="$(cat "$F")"; rm -f "$F"
ELAPSED=$(( $(date +%s) - START ))
[ "$ELAPSED" -ge "$THRESHOLD" ] || exit 0
MIN=$(( ELAPSED / 60 ))

printf '작업 폴더: %s\n소요 시간: %d분\n세션: %s\n' "$CWD" "$MIN" "$SID" \
  | "$HOME/.claude/bin/claude-mail" "[Claude] 작업 완료 — $(basename "$CWD") (${MIN}분)"
exit 0
```

읽는 순서는 이렇습니다. 시작 파일이 없으면 그냥 끝냅니다(훅을 걸기 전에 시작된 세션). 있으면 경과 시간을 재고, 임계값 미만이면 조용히 끝냅니다. 넘었을 때만 메일을 보냅니다.

두 파일 모두 실행 권한이 필요합니다.

```bash
mkdir -p ~/.claude/hooks
chmod +x ~/.claude/hooks/notify-start.sh ~/.claude/hooks/notify-stop.sh
```

### 훅 등록

`~/.claude/settings.json`에 다음을 넣습니다. 파일이 이미 있다면 `hooks` 키만 추가하세요.

```json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "\"$HOME/.claude/hooks/notify-start.sh\"", "timeout": 5 }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "\"$HOME/.claude/hooks/notify-stop.sh\"", "timeout": 30, "async": true }
        ]
      }
    ]
  }
}
```

`Stop` 쪽의 **`"async": true`** 를 눈여겨보세요. 메일 발송이 끝날 때까지 기다리지 않고 넘어간다는 뜻입니다. 이게 없으면 네트워크가 느릴 때 응답이 끝나고도 몇 초씩 멈춰 있게 됩니다.

훅은 **다음 세션부터** 적용됩니다. 지금 창에서는 안 걸리니 새로 열어서 시험하세요.

### 시험해보기

임계값을 잠깐 낮춰서 확인하는 게 편합니다. 환경변수로 조절할 수 있게 만들어뒀으니 10초로 낮춰보세요.

```bash
CLAUDE_NOTIFY_AFTER_SEC=10 claude
```

아무 질문이나 던지고 10초 이상 걸리는 작업을 시키면 메일이 옵니다. 확인했으면 그냥 평소대로 `claude`로 실행하면 기본값 10분으로 돌아갑니다.

## 5단계: 매일 요약 받기 — 예약 실행

작업이 끝날 때마다 오는 알림 말고, **하루치를 모아서** 받고 싶을 수도 있습니다. 저는 `~/Projects` 아래 저장소들의 그날 커밋을 모아 밤에 한 통 받게 해뒀습니다.

```bash
#!/bin/bash
# ~/Projects 하위 저장소의 최근 커밋을 모아 메일로 보낸다.
#   claude-digest          → 최근 1일
#   claude-digest 7 주간   → 최근 7일, 제목에 "주간"
set -u
DAYS="${1:-1}"
LABEL="${2:-일간}"
ROOT="$HOME/Projects"

BODY="$(
  cd "$ROOT" || exit
  found=0
  while IFS= read -r g; do
    r="$(dirname "$g")"
    log="$(git -C "$r" log --since="${DAYS} days ago" --format='  %ad  %s' --date=format:'%m/%d %H:%M' 2>/dev/null)"
    [ -n "$log" ] || continue
    found=1
    printf '## %s\n%s\n\n' "${r#./}" "$log"
  done < <(find . -maxdepth 3 -name .git -type d 2>/dev/null | sort)
  [ "$found" = 1 ] || printf '최근 %s일간 커밋 없음.\n' "$DAYS"
)"

printf '%s\n' "$BODY" | "$HOME/.claude/bin/claude-mail" "[Claude] ${LABEL} 작업 요약 — $(date '+%Y-%m-%d')"
```

`~/.claude/bin/claude-digest`로 저장하고 실행 권한을 준 뒤, 먼저 손으로 돌려보세요.

```bash
chmod +x ~/.claude/bin/claude-digest
~/.claude/bin/claude-digest
```

### cron을 쓰면 조용히 실패합니다

이제 매일 자동으로 돌게 예약할 차례인데, **여기가 이 글에서 가장 중요한 함정**입니다.

정기 실행이라고 하면 보통 `cron`을 떠올립니다. 그런데 macOS에서 cron으로 이걸 돌리면 **메일이 안 옵니다.** 에러도 안 뜹니다. 로그도 안 남습니다. 그냥 조용히 아무 일도 안 일어나요.

이유는 **키체인**입니다. cron은 사용자 GUI 세션 밖에서 돌기 때문에 로그인 키체인이 잠겨 있고, `security find-generic-password`가 실패합니다. 앞에서 만든 `claude-mail`은 키체인을 못 읽으면 조용히 넘어가도록 되어 있으니(그게 훅에서는 올바른 동작이었죠) 결과적으로 아무 흔적 없이 사라집니다.

해결은 macOS의 예약 실행 방식인 **LaunchAgent**를 쓰는 것입니다. 사용자 로그인 세션에서 돌기 때문에 키체인이 열려 있습니다.

`~/Library/LaunchAgents/com.example.claude-digest.daily.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.example.claude-digest.daily</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/여러분계정/.claude/bin/claude-digest</string>
    <string>1</string>
    <string>일간</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key><integer>23</integer>
    <key>Minute</key><integer>47</integer>
  </dict>
  <key>StandardErrorPath</key>
  <string>/Users/여러분계정/.claude/digest.log</string>
  <key>StandardOutPath</key>
  <string>/Users/여러분계정/.claude/digest.log</string>
</dict>
</plist>
```

경로에 `~`를 쓸 수 없어서 전체 경로를 적어야 합니다. `echo $HOME`으로 확인해 그 값을 넣으세요.

문법을 검사하고 등록합니다.

```bash
plutil -lint ~/Library/LaunchAgents/com.example.claude-digest.daily.plist
launchctl load ~/Library/LaunchAgents/com.example.claude-digest.daily.plist
launchctl list | grep claude-digest
```

마지막 명령에서 이름이 보이면 등록된 겁니다. 시각을 바꾸려면 plist를 고치고 `launchctl unload` 후 다시 `load` 하시면 됩니다.

> `load`·`unload`는 애플이 오래전 deprecated로 표시한 구식 명령입니다. 지금도 잘 동작하고 검색하면 대부분 이 형태로 나오지만, 최신 방식은 `launchctl bootstrap gui/$(id -u) <plist>`와 `launchctl bootout gui/$(id -u)/<라벨>`입니다. 새로 익히실 거라면 이쪽을 권합니다.

정각을 피해 23시 47분처럼 애매한 시각으로 잡은 건 의도입니다. 많은 예약 작업이 정각에 몰려서, 몇 분 비껴두면 그만큼 덜 밀립니다.

## Gmail 커넥터는 어디에 쓰나 — 보내기와 읽기는 다릅니다

여기까지 읽으시고 이런 생각이 드셨을 수 있습니다. "Claude에 Gmail 연결하는 기능이 있던데, 그거 쓰면 되는 거 아닌가?"

좋은 질문이고, 답은 **역할이 다르다**입니다.

| 구분 | Gmail 커넥터 | msmtp |
|---|---|---|
| 주 용도 | 메일 **읽기**·검색·초안 작성 | 메일 **보내기** |
| 인증 | 구글 계정 연동(OAuth) | 앱 비밀번호 |
| 동작 조건 | 대화 중, Claude가 떠 있을 때 | 명령만 있으면 언제든 |
| 예약 실행 | 불가 | 가능 |
| 훅에서 사용 | 불가 | 가능 |

결정적인 차이는 **마지막 두 줄**입니다. 커넥터는 대화 중에 Claude가 도구로 호출하는 방식이라, 새벽 세 시에 LaunchAgent가 혼자 돌 때는 쓸 수 없습니다. 훅도 마찬가지예요. 훅은 셸 스크립트를 실행하는 것이라 커넥터에 접근할 방법이 없습니다.

그럼 커넥터는 쓸모없느냐, 그렇지 않습니다. **이 시스템을 검증할 때 아주 유용했습니다.**

메일을 보낸 뒤 "정말 도착했나"를 확인하려면 보통 지메일을 열어봐야 합니다. 그런데 커넥터가 붙어 있으면 대화 중에 바로 확인할 수 있습니다. SMTP가 `250 OK`를 반환했다고 해서 받은편지함에 도착했다는 뜻은 아니거든요. 스팸으로 분류될 수도 있고, 한글이 깨져 있을 수도 있습니다. **보낸 쪽 로그와 받은 쪽 실물을 둘 다 확인**해야 검증이 끝납니다.

정리하면 이렇습니다. **보내는 건 msmtp, 확인하는 건 커넥터.** 둘은 경쟁 관계가 아니라 짝입니다.

> 커넥터 연결은 Claude 설정의 커넥터 항목에서 Gmail을 추가하고 구글 계정 권한에 동의하면 됩니다. 앱 비밀번호와 무관하며, 읽기 권한을 주는 것이니 어떤 범위를 허용하는지 확인하고 붙이세요.

## 메일 말고 다른 방법은 없나 — 대안 넷

메일이 유일한 답은 아닙니다. "긴 작업의 결과를 폰에서 받아본다"는 목표만 놓고 보면 방법이 여럿이고, 저도 메일로 정하기 전에 이것들을 재봤습니다. 상황이 다르면 다른 선택이 나을 수 있으니 정리해둡니다.

### 1. Remote Control — 세션 자체를 폰으로 가져오기

가장 정면 승부인 방법입니다. Claude Code에는 **로컬 세션을 폰이나 브라우저에서 이어서 쓰는 기능**이 있습니다. 터미널에서 `/remote-control`(줄여서 `/rc`)을 치거나, 처음부터 `claude --remote-control`로 띄우면 됩니다.

```bash
claude --remote-control "내 프로젝트"
```

그러면 Claude 앱이나 claude.ai에서 그 세션이 열립니다. **실행과 파일 접근은 계속 내 맥에서** 일어나고, 폰은 창 역할만 합니다. 그래서 폰에서 "방금 만든 보고서 보여줘"라고 하면 Claude가 로컬 파일을 읽어서 대화창에 띄워줍니다. `@`를 치면 내 프로젝트의 파일 경로가 자동완성되고요. 푸시 알림도 받을 수 있습니다 — `/config`에서 켜면 Claude가 알릴 만하다고 판단했을 때와 권한 승인이 필요할 때 폰으로 옵니다.

이게 있는데 왜 메일을 만들었느냐고 하실 수 있는데, 성격이 다릅니다.

- **세션이 살아 있어야 합니다.** 로컬 프로세스가 곧 세션이라, 터미널을 닫거나 맥이 꺼지면 오프라인이 됩니다. 반면 메일은 이미 떠난 것이라 보낸 쪽이 죽어도 그대로 남습니다.
- **읽는 것이지 받는 것이 아닙니다.** 파일 내용을 대화로 보는 것이고, 폰에 파일이 남지는 않습니다. 나중에 다시 찾으려면 세션을 다시 열어야 해요.
- **PPT·워드·엑셀은 이 방식으로 못 봅니다.** 앞에서 말한 그 문제입니다. 대화창은 결국 글자를 보여주는 곳이라, 텍스트가 아닌 문서는 잘해야 안에서 뽑아낸 글자 몇 줄이 나올 뿐 슬라이드도 서식도 표도 오지 않습니다. **문서를 문서로 보려면 그 파일이 폰에 도착해야** 합니다. 제가 메일을 만든 주된 이유가 이것이었어요.
- 조건도 있습니다. 리서치 프리뷰 단계이고 Pro·Max·Team·Enterprise 플랜에서 쓸 수 있으며, API 키 인증으로는 안 됩니다.

**작업 중에 개입하고 싶을 때는 이쪽이 압도적으로 낫습니다.** 폰에서 방향을 바꿔주거나 권한 요청에 답할 수 있으니까요. 다만 "끝난 결과를 나중에 조용히 확인"하는 용도로는 메일이 더 맞았습니다. 둘은 배타적이지 않아서 같이 켜두셔도 됩니다.

### 2. ntfy — 설정 5초, 첨부 3시간

[ntfy](https://ntfy.sh/)는 HTTP 요청 한 번으로 폰에 푸시를 쏘는 서비스입니다. 계정도 설정 파일도 없습니다. 토픽 이름을 정해서 앱에서 구독해두고, 그 주소로 던지면 끝이에요.

```bash
# 파일 첨부해서 보내기
curl -T 결과보고서.md -H "Filename: 결과보고서.md" \
     -H "Title: 작업 완료" https://ntfy.sh/내가정한토픽이름
```

실제로 해봤더니 응답에 첨부 정보가 이렇게 담겨 옵니다(전체 응답에서 첨부 부분만 발췌했습니다).

```json
{
  "attachment": {
    "name": "결과보고서.md",
    "size": 40,
    "url": "https://ntfy.sh/file/Ktw6QNT7b42j.txt"
  }
}
```

공개 서버 기준 **첨부는 15 MB까지, 3시간 뒤 삭제**됩니다. 설정 난이도가 압도적으로 낮은 게 장점이고, 실시간성도 메일보다 낫습니다.

문제는 **보안입니다.** 공개 서버에서는 **토픽 이름을 아는 사람은 누구나 구독**할 수 있고, 이름 외에 다른 인증이 없습니다. 첨부 URL도 마찬가지예요. 추측하기 어려운 긴 이름을 쓴다 해도 그건 자물쇠가 아니라 그냥 안 알려진 상태일 뿐입니다. **업무 산출물이나 개인정보가 든 파일은 올리면 안 됩니다.** "빌드 끝남" 같은 신호용으로 쓰거나, 직접 서버를 띄워서(self-host) 접근 제어를 걸고 쓰는 게 맞습니다.

### 3. 동기화 폴더 — 가장 단순한 방법

결과물을 iCloud Drive나 Dropbox 폴더에 복사만 하는 방법입니다.

```bash
cp 결과보고서.md ~/Library/Mobile\ Documents/com~apple~CloudDocs/claude-out/
```

이 한 줄이면 폰의 파일 앱에서 열립니다. 설정할 게 사실상 없고, 용량 제한도 클라우드 용량만큼입니다. 큰 파일에는 이쪽이 확실히 낫습니다.

대신 **알림이 없습니다.** 끝난 걸 모르면 폴더를 안 열어보게 되죠. 그래서 이건 단독으로 쓰기보다 **알림과 짝지어 쓰는 편**이 좋습니다. 메일로는 "끝났고 요약은 이렇다"만 보내고 파일은 동기화 폴더에 두는 식으로요.

사실 설정이 귀찮다면, 클로드에게 작업 완료 후 A 파일을 B 폴더에 복사후 알려줘. 라는 명령으로 쉽게 할 수도 있죠.

### 4. git push — 저장소 안의 결과물이라면

결과물이 어차피 저장소 안에 들어갈 것이라면, 커밋해서 푸시하는 게 제일 자연스럽습니다. 폰의 GitHub 앱이나 웹에서 그대로 읽히고, 마크다운은 렌더링까지 됩니다. 이력이 남는다는 것도 다른 방법에는 없는 장점이에요.

당연히 **커밋할 만한 것에만** 해당합니다. 중간 산출물이나 임시 파일까지 밀어 넣으면 저장소가 지저분해집니다.

### 정리하면

| 방법 | 파일 전달 | 알림 | 나중에 다시 찾기 | 걸리는 조건 |
|---|---|---|---|---|
| 메일(이 글) | 25 MB까지 첨부 | 있음 | 메일함에서 검색 | 앱 비밀번호 발급, 설정 30분 |
| Remote Control | 대화로 열람 | 푸시 | 세션을 다시 열어야 | 세션이 떠 있어야 함, 유료 플랜 |
| ntfy | 15 MB, 3시간 | 푸시 | 3시간 뒤 사라짐 | 공개 서버는 사실상 무방비 |
| 동기화 폴더 | 용량 제한 사실상 없음 | 없음 | 폴더에 그대로 | 클라우드 계정 |
| git push | 저장소 크기만큼 | 없음 | 커밋 이력에 영구 | 커밋할 만한 것만 |

제가 메일을 고른 건 **파일·알림·보관 셋이 한꺼번에 되는 유일한 방법**이었기 때문입니다. 파일이 오고, 알림이 오고, 반년 뒤에도 검색해서 꺼낼 수 있습니다. 대신 설정이 제일 번거롭죠 — 그래서 이 글이 길어진 것이기도 합니다.

한 번 만들어두면 다시 안 건드려도 된다는 점은 덧붙이고 싶습니다. 저는 이 글을 쓰는 동안 설정 파일을 한 번도 다시 열지 않았습니다.

## 잘 안 될 때 — 증상별로 찾기

만들면서 실제로 걸렸던 것들입니다.

**메일이 아예 안 온다** — 먼저 `tail -20 ~/.msmtp.log`를 보세요. 로그에 아무것도 없으면 발송 시도조차 안 된 겁니다. `claude-mail`이 키체인을 못 찾아 조용히 빠져나간 경우가 대부분이에요. `security find-generic-password -s msmtp-gmail -a you@gmail.com -w`가 되는지 확인하세요.

**`authentication failed`가 찍힌다** — 앱 비밀번호가 틀렸습니다. 16자리를 공백 없이 넣었는지 확인하고, 안 되면 새로 발급받으세요.

**손으로는 되는데 예약 실행에서만 안 된다** — 십중팔구 cron을 쓰신 겁니다. 위의 LaunchAgent로 바꾸세요.

**매번 키체인 접근 창이 뜬다** — 첫 실행 때 "허용"을 누르셨습니다. "항상 허용"이어야 합니다. 키체인 접근 앱에서 해당 항목의 접근 제어를 고치거나, 항목을 지우고 다시 등록하세요.

**본문 한글이 깨져서 온다** — `Content-Type: text/plain; charset=UTF-8` 헤더가 빠졌습니다.

**제목만 깨져서 온다** — 받는 쪽 서버가 헤더의 UTF-8을 그대로 받아주지 않는 경우입니다. 첨부 버전(`claude-mail-attach`)을 쓰면 제목을 규격대로 인코딩해서 보냅니다.

**메일이 너무 자주 온다** — 시간 게이트가 없거나 임계값이 너무 낮습니다. `CLAUDE_NOTIFY_AFTER_SEC`를 올리세요.

## 접근성 한 꼭지 — 알림은 여러 갈래일수록 좋습니다

이 블로그가 접근성을 다루니 한 가지만 덧붙이겠습니다.

작업 완료를 **소리로만** 알리는 방식이 흔합니다. 터미널 벨이나 시스템 사운드요. 그런데 소리는 청각에 의존하는 단일 채널이라, 못 듣는 상황이면 그대로 놓칩니다. 청각장애가 있는 경우는 물론이고, 이어폰을 빼둔 사람이나 소음이 큰 사무실에서도 마찬가지예요.

메일 알림이 좋은 건 **읽는 방식을 사용자가 고를 수 있다**는 점입니다. 눈으로 봐도 되고, 스크린 리더로 들어도 되고, 폰 진동으로 알아챌 수도 있습니다. 같은 정보를 여러 감각으로 받을 수 있게 두는 것 — WCAG가 말하는 **인식의 용이성**이 정확히 이 이야기입니다.

그래서 메일 본문을 만들 때도 한 가지를 신경 쓰면 좋습니다. **제목만 읽어도 무슨 일인지 알게** 쓰는 겁니다. `[Claude] 작업 완료 — codeslog (23분)`처럼요. 스크린 리더 사용자는 메일 목록을 제목으로 훑는데, 제목이 전부 "알림"이면 목록이 아무 정보가 없는 벽이 됩니다. 이건 [링크 텍스트에 목적지를 담아야 하는 이유]({{< relref "/posts/markdown-syntax-guide" >}})와 같은 원리예요.

## 리눅스에서는

키체인 부분만 바꾸면 나머지는 같습니다. `passwordeval`에 넣을 명령을 그 환경의 비밀번호 관리자로 바꾸시면 돼요.

```
# pass 를 쓰는 경우
passwordeval "pass show gmail/app-password"

# gpg 로 암호화한 파일을 쓰는 경우
passwordeval "gpg --quiet --for-your-eyes-only --decrypt ~/.msmtp-password.gpg"
```

예약 실행은 리눅스라면 cron이나 systemd 타이머를 그냥 쓰셔도 됩니다. macOS의 키체인 문제는 리눅스에는 없거든요. 다만 gpg를 쓴다면 에이전트가 잠겨 있는지는 확인하셔야 합니다.

## 한 장 요약

- Gmail은 일반 비밀번호로 SMTP 발송이 안 된다. **2단계 인증 → 앱 비밀번호 16자리** 발급.
- **앱 비밀번호 발급만 사람이 하고, 나머지 설정은 Claude에게 맡겨도 된다.** 프롬프트는 본문에.
- PPT·워드·엑셀은 텍스트가 아니라 XML을 담은 압축 파일이다. **터미널로도 대화창으로도 안 열린다.** 파일이 폰에 도착해야 열린다.
- 앱 비밀번호는 설정 파일이 아니라 **키체인**에. msmtp의 `passwordeval`로 읽어온다.
- 첫 발송 때 키체인 창에서 **"항상 허용"**. "허용"만 누르면 자동 실행이 막힌다.
- 본문 한글이 깨지면 `Content-Type: ... charset=UTF-8` 헤더가 빠진 것. 제목 인코딩은 첨부 버전이 알아서 한다.
- **msmtp는 첨부를 못 만든다.** MIME 조립은 파이썬 `email` 모듈에 맡기고 발송만 넘긴다. Gmail 상한 25 MB.
- `Stop` 훅은 매 턴 발동한다. **시간 게이트가 없으면 스팸**이 된다.
- 예약 실행은 **cron이 아니라 LaunchAgent**. cron은 키체인을 못 읽어 조용히 실패한다.
- **Gmail 커넥터는 읽기·확인용, msmtp는 발송용.** 훅과 예약 실행에서는 커넥터를 쓸 수 없다.
- 작업 중에 개입하고 싶으면 **Remote Control**, 끝난 결과를 나중에 확인하려면 메일. 같이 켜도 된다.
- 알림은 소리 하나에 의존하지 말고, 제목만 읽어도 알 수 있게 쓴다.

{{< faq >}}

## 참고 자료

- [Google 앱 비밀번호 안내](https://support.google.com/accounts/answer/185833)
- [msmtp 공식 문서](https://marlam.de/msmtp/msmtp.html)
- [Claude Code 훅 문서](https://code.claude.com/docs/en/hooks-guide)
- [Claude Code Remote Control 문서](https://code.claude.com/docs/en/remote-control)
- [Apple launchd 문서](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html)
- [파이썬 `email.message` 문서](https://docs.python.org/3/library/email.message.html)
- [ntfy 발행 문서 — 첨부](https://docs.ntfy.sh/publish/#attachments)

