긴 작업을 시켜놓고 다른 일을 하다가, 한참 뒤에 터미널로 돌아와 보니 이미 30분 전에 끝나 있던 적 있으신가요. 반대로 5분마다 창을 들여다보느라 아무 일도 못 한 적도요.
제 경우엔 여기에 한 가지가 더 얹혀 있었습니다. 작업 결과가 파일로 나오는 경우입니다. 조사 결과를 정리한 마크다운, 뽑아낸 CSV, 만들어둔 보고서 같은 것들요. 자리를 비운 사이 이게 끝나 있으면, 폰으로는 확인할 방법이 마땅치 않았습니다. SSH로 붙어서 cat을 치면 줄바꿈이 다 깨지고, 스크롤은 손가락으로 더듬어야 하고, 표라도 들어 있으면 사실상 못 읽습니다.
결정적인 건 PPT나 워드 파일이었습니다. 요즘은 발표 자료나 보고서 초안까지 만들게 시키는데, .pptx·.docx·.xlsx는 텍스트 파일이 아닙니다. 안을 열어보면 XML 여러 개를 담은 압축 파일이에요. 실제로 손에 있던 엑셀 파일을 풀어보니 xl/workbook.xml, xl/worksheets/sheet1.xml 같은 항목이 열 개 들어 있었습니다. 이런 걸 터미널로 cat 해봐야 깨진 글자만 쏟아집니다. 원격으로 붙어 있든 뭐든, 결국 그 파일을 여는 앱이 있어야 열립니다. 폰에는 그 앱들이 다 있고요 — 파일만 도착하면요.
그래서 결국 끝나면 파일째로 메일로 보내주게 만들었습니다. 알림도 되고, 첨부가 오고, 폰에서 탭 한 번이면 키노트나 워드가 알아서 띄웁니다. 만들어보니 생각보다 걸리는 게 몇 개 있었는데 대부분 검색해도 잘 안 나오는 것들이라 정리해둡니다.
이 글은 네 가지를 다룹니다. Gmail로 메일을 보낼 수 있게 만드는 것(앱 비밀번호와 발송 도구), 결과 파일을 첨부하는 것, 그걸 Claude Code에 연결하는 것(훅), 그리고 매일 요약을 자동으로 받는 것(예약 실행)입니다. 명령어를 하나씩 다 적어뒀으니 그대로 따라 하시면 됩니다. 메일 말고 다른 방법은 없는지도 뒤에서 따로 비교했습니다.
전체 그림은 이렇습니다. 두 갈래가 같은 발송 경로를 나눠 쓰는 구조예요.

이 글은 macOS 기준입니다. 리눅스도 흐름은 같지만 키체인 대신 다른 방법(
pass,gpg등)을 쓰게 됩니다. 그 부분은 마지막에 짚었습니다.
왜 메일인가 — 알림 방법을 고르는 기준#
터미널 소리, 데스크톱 알림, 슬랙, 메일. 선택지는 많습니다. 저는 메일을 골랐는데 이유가 넷입니다.
자리를 비워도 남습니다. 데스크톱 알림은 놓치면 끝이고, 화면을 잠가두면 못 봅니다. 메일은 폰으로 따라옵니다.
내용을 담을 수 있습니다. “작업 완료” 한 줄이 아니라 무슨 작업이었고 뭐라고 끝냈는지를 본문에 넣을 수 있어요. 폰으로 메일만 읽고도 상황 파악이 됩니다.
파일이 그대로 따라옵니다. 이게 제겐 결정적이었습니다. 첨부를 열면 폰의 뷰어가 마크다운이든 CSV든 알아서 띄워줍니다. 터미널로 붙어서 읽는 것과는 경험이 아예 다릅니다. 게다가 메일함에 남으니 나중에 검색해서 다시 꺼낼 수도 있어요. 잠깐 떴다 사라지는 알림에는 없는 성질입니다.
추가 계정이 필요 없습니다. 슬랙 웹훅도 좋지만 워크스페이스가 있어야 하고, 개인 작업에는 과합니다.
단점도 분명합니다. 실시간성이 떨어지고, 설정이 제일 번거롭습니다. 특히 Gmail은 일반 비밀번호로 로그인이 막혀 있어서 처음 하면 여기서 한 번 막힙니다. 그 얘기부터 하겠습니다.
1단계: Gmail 앱 비밀번호 발급받기#
프로그램에서 Gmail로 메일을 보내려면 SMTP라는 방식으로 구글 서버에 로그인해야 합니다. SMTP(Simple Mail Transfer Protocol)는 메일을 보내는 규약이라고만 알아두시면 충분합니다.
문제는 여기에 평소 쓰는 구글 비밀번호를 넣으면 거부당한다는 겁니다. 구글이 2022년부터 “보안 수준이 낮은 앱"의 접근을 막았거든요. 대신 앱마다 따로 발급받는 앱 비밀번호를 쓰게 되어 있습니다.
2단계 인증부터 켭니다#
앱 비밀번호는 2단계 인증이 켜져 있어야 메뉴에 나타납니다. 안 켜져 있으면 아무리 찾아도 항목이 안 보여서 헤매게 돼요.
- Google 계정 보안 설정으로 갑니다
- 2단계 인증을 켭니다 (전화번호나 인증 앱으로)
앱 비밀번호를 만듭니다#
- 앱 비밀번호 페이지로 갑니다
- 이름을 적습니다. 나중에 알아볼 수 있게
msmtp나claude-notify정도로요 - 만들기를 누르면 16자리가 나옵니다
여기서 중요한 게 하나 있습니다. 이 화면을 닫으면 다시 볼 수 없습니다. 잃어버리면 새로 만드는 수밖에 없어요. 그러니 창을 열어둔 채로 다음 단계까지 진행하시는 걸 권합니다.
앱 비밀번호는 계정 비밀번호와 관계없는 별도의 비밀번호입니다. 이 16자리로는 지메일에 로그인할 수 없고, 메일 발송 같은 별도의 프로그램과 연결하기 위해 제한된 용도로만 쓰입니다. 유출되면 그 용도로만 악용되고, 계정에서 해당 항목만 삭제하면 즉시 무효화됩니다. 그래도 유출은 막아야 하니 다음 단계가 중요합니다.
잠깐 — 나머지는 Claude에게 시켜도 됩니다#
여기까지 오셨다면 사실 사람이 꼭 해야 하는 부분은 끝났습니다. 앱 비밀번호 발급은 구글 계정에 로그인해서 버튼을 누르는 일이라 위임할 수가 없어요. 하지만 그다음부터는 파일 만들고 권한 주고 설정 넣는 일이라, Claude Code에게 그대로 맡길 수 있습니다.
터미널에서 claude를 띄우고 아래를 통째로 붙여넣으세요.
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를 쓰겠습니다. 가볍고, 설정 파일 하나면 되고, 무엇보다 비밀번호를 파일에 안 적어도 되는 기능이 있습니다.
brew install msmtp왜 비밀번호를 파일에 적으면 안 되나#
msmtp 설정 파일에 password 여기에16자리 라고 적어도 동작은 합니다. 많은 안내글이 그렇게 되어 있고요. 그런데 이러면 세 가지가 걸립니다.
- 홈 디렉터리를 통째로 백업하거나 클라우드에 동기화하면 비밀번호가 함께 갑니다
- 설정 파일을 dotfiles 저장소로 관리하는 분이라면 그대로 깃허브에 올라갑니다
- 화면 공유나 스크린샷 한 번에 노출됩니다
그래서 macOS 키체인에 넣습니다. 키체인은 운영체제가 관리하는 비밀번호 금고예요. 터미널에서 다음 명령을 실행하면 비밀번호를 물어보는데, 입력해도 화면에 보이지 않습니다. 입력이 되고 있는거야? 라고 생각하실수도 있겠지만, 정상입니다.
security add-generic-password -s msmtp-gmail -a [email protected] -w-s는 항목 이름(서비스), -a는 계정, -w는 비밀번호를 입력받겠다는 뜻입니다. 아까 발급받은 16자리를 붙여넣고 엔터를 치면 됩니다. 공백은 빼고 넣으세요. ([email protected] 를 본인 이메일로 수정 후 실행하세요.)
잘 들어갔는지 확인하려면 이렇게 합니다.
security find-generic-password -s msmtp-gmail -a [email protected] -w비밀번호가 화면에 그대로 찍히니 다른 사람이 볼 수 있는 상황에서는 실행하지 마세요.
msmtp 설정 파일 작성#
이제 ~/.msmtprc 파일을 만듭니다. 아래를 그대로 붙여넣고 주소만 바꾸시면 됩니다.
# msmtp — 작업 알림 메일 발송용
#
# 앱 비밀번호는 이 파일에 두지 않는다. macOS 키체인에서 읽어온다.
defaults
auth on
tls on
tls_trust_file system
logfile ~/.msmtp.log
account gmail
host smtp.gmail.com
port 587
from [email protected]
user [email protected]
passwordeval "security find-generic-password -s msmtp-gmail -a [email protected] -w"
account default : gmail핵심은 마지막에서 두 번째 줄인 passwordeval 입니다. 비밀번호를 적는 대신 비밀번호를 가져오는 명령을 적어두는 거예요. msmtp가 발송할 때마다 이 명령을 실행해 키체인에서 꺼내옵니다. 파일에는 비밀번호가 없습니다.
파일 권한도 잠가둡니다.
chmod 600 ~/.msmtprc“msmtp는 권한이 느슨하면 실행을 거부한다"는 설명을 종종 보는데, 실제로 확인해보니 평문
password를 적었을 때만 그렇습니다. 우리처럼passwordeval만 쓴 파일은 권한이 644여도 그냥 돌아갑니다(msmtp 1.8.34에서 확인). 즉 이chmod는 msmtp가 강제해서 하는 게 아니라 우리가 알아서 챙기는 것입니다. 설정 파일 자체에 비밀번호가 없더라도 계정 주소와 서버 정보는 굳이 남에게 보일 이유가 없으니까요.
첫 발송 테스트#
printf 'Subject: 테스트\n\n안녕하세요\n' | msmtp [email protected]여기서 macOS가 키체인 접근을 허용할지 묻는 창이 뜹니다. 이때 반드시 “항상 허용”을 누르세요. “허용"만 누르면 발송할 때마다 창이 뜨고, 나중에 자동 실행에서는 창이 뜬 채로 멈춰버립니다. 이 버튼 하나 때문에 “왜 자동으로는 안 되지?“로 한참 헤맬 수 있습니다.
받은편지함에 메일이 왔으면 절반은 끝난 겁니다. 안 왔다면 로그를 보세요.
tail -20 ~/.msmtp.logauthentication failed가 보이면 앱 비밀번호가 틀렸거나 공백이 섞인 겁니다. 키체인에 다시 넣으려면 기존 항목을 지우고 등록하세요.
security delete-generic-password -s msmtp-gmail -a [email protected]
security add-generic-password -s msmtp-gmail -a [email protected] -w3단계: 보내기 편한 명령 만들기#
매번 printf로 헤더를 조립하는 건 번거로우니 짧은 스크립트로 감싸둡니다. ~/.claude/bin/claude-mail로 저장하겠습니다.
#!/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"저장한 뒤 실행 권한을 줍니다.
mkdir -p ~/.claude/bin
chmod +x ~/.claude/bin/claude-mail두 군데가 의도적입니다.
Content-Type 줄이 없으면 한글 본문이 깨져서 도착합니다. UTF-8을 명시해야 합니다.
제목은 사정이 조금 다릅니다. 엄밀히 따지면 메일 헤더에 한글을 그대로 넣는 건 규격상 맞지 않고, =?utf-8?B?...?= 같은 형태로 인코딩하는 게 정석이에요. 그런데 요즘 메일 서버는 대부분 UTF-8을 그대로 받아주기 때문에 실사용에서는 위 스크립트로도 한글 제목이 멀쩡히 도착합니다. 저도 몇 달째 이대로 쓰고 있어요. 다만 회사 메일 서버처럼 오래된 환경으로 보낼 일이 있다면 뒤에 나올 첨부 버전을 쓰세요. 그쪽은 제목 인코딩까지 알아서 처리합니다.
키체인 항목이 없으면 exit 0으로 조용히 끝냅니다. 알림은 곁다리 기능인데 이게 실패해서 본 작업이 멈추면 곤란하거든요. 나중에 훅으로 물리면 이 판단이 특히 중요해집니다.
이제 이렇게 쓸 수 있습니다.
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로 저장합니다.
#!/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())
PYchmod +x ~/.claude/bin/claude-mail-attach로 실행 권한을 주고 이렇게 씁니다.
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:
#!/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:
#!/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읽는 순서는 이렇습니다. 시작 파일이 없으면 그냥 끝냅니다(훅을 걸기 전에 시작된 세션). 있으면 경과 시간을 재고, 임계값 미만이면 조용히 끝냅니다. 넘었을 때만 메일을 보냅니다.
두 파일 모두 실행 권한이 필요합니다.
mkdir -p ~/.claude/hooks
chmod +x ~/.claude/hooks/notify-start.sh ~/.claude/hooks/notify-stop.sh훅 등록#
~/.claude/settings.json에 다음을 넣습니다. 파일이 이미 있다면 hooks 키만 추가하세요.
{
"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초로 낮춰보세요.
CLAUDE_NOTIFY_AFTER_SEC=10 claude아무 질문이나 던지고 10초 이상 걸리는 작업을 시키면 메일이 옵니다. 확인했으면 그냥 평소대로 claude로 실행하면 기본값 10분으로 돌아갑니다.
5단계: 매일 요약 받기 — 예약 실행#
작업이 끝날 때마다 오는 알림 말고, 하루치를 모아서 받고 싶을 수도 있습니다. 저는 ~/Projects 아래 저장소들의 그날 커밋을 모아 밤에 한 통 받게 해뒀습니다.
#!/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로 저장하고 실행 권한을 준 뒤, 먼저 손으로 돌려보세요.
chmod +x ~/.claude/bin/claude-digest
~/.claude/bin/claude-digestcron을 쓰면 조용히 실패합니다#
이제 매일 자동으로 돌게 예약할 차례인데, 여기가 이 글에서 가장 중요한 함정입니다.
정기 실행이라고 하면 보통 cron을 떠올립니다. 그런데 macOS에서 cron으로 이걸 돌리면 메일이 안 옵니다. 에러도 안 뜹니다. 로그도 안 남습니다. 그냥 조용히 아무 일도 안 일어나요.
이유는 키체인입니다. cron은 사용자 GUI 세션 밖에서 돌기 때문에 로그인 키체인이 잠겨 있고, security find-generic-password가 실패합니다. 앞에서 만든 claude-mail은 키체인을 못 읽으면 조용히 넘어가도록 되어 있으니(그게 훅에서는 올바른 동작이었죠) 결과적으로 아무 흔적 없이 사라집니다.
해결은 macOS의 예약 실행 방식인 LaunchAgent를 쓰는 것입니다. 사용자 로그인 세션에서 돌기 때문에 키체인이 열려 있습니다.
~/Library/LaunchAgents/com.example.claude-digest.daily.plist:
<?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으로 확인해 그 값을 넣으세요.
문법을 검사하고 등록합니다.
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로 띄우면 됩니다.
claude --remote-control "내 프로젝트"그러면 Claude 앱이나 claude.ai에서 그 세션이 열립니다. 실행과 파일 접근은 계속 내 맥에서 일어나고, 폰은 창 역할만 합니다. 그래서 폰에서 “방금 만든 보고서 보여줘"라고 하면 Claude가 로컬 파일을 읽어서 대화창에 띄워줍니다. @를 치면 내 프로젝트의 파일 경로가 자동완성되고요. 푸시 알림도 받을 수 있습니다 — /config에서 켜면 Claude가 알릴 만하다고 판단했을 때와 권한 승인이 필요할 때 폰으로 옵니다.
이게 있는데 왜 메일을 만들었느냐고 하실 수 있는데, 성격이 다릅니다.
- 세션이 살아 있어야 합니다. 로컬 프로세스가 곧 세션이라, 터미널을 닫거나 맥이 꺼지면 오프라인이 됩니다. 반면 메일은 이미 떠난 것이라 보낸 쪽이 죽어도 그대로 남습니다.
- 읽는 것이지 받는 것이 아닙니다. 파일 내용을 대화로 보는 것이고, 폰에 파일이 남지는 않습니다. 나중에 다시 찾으려면 세션을 다시 열어야 해요.
- PPT·워드·엑셀은 이 방식으로 못 봅니다. 앞에서 말한 그 문제입니다. 대화창은 결국 글자를 보여주는 곳이라, 텍스트가 아닌 문서는 잘해야 안에서 뽑아낸 글자 몇 줄이 나올 뿐 슬라이드도 서식도 표도 오지 않습니다. 문서를 문서로 보려면 그 파일이 폰에 도착해야 합니다. 제가 메일을 만든 주된 이유가 이것이었어요.
- 조건도 있습니다. 리서치 프리뷰 단계이고 Pro·Max·Team·Enterprise 플랜에서 쓸 수 있으며, API 키 인증으로는 안 됩니다.
작업 중에 개입하고 싶을 때는 이쪽이 압도적으로 낫습니다. 폰에서 방향을 바꿔주거나 권한 요청에 답할 수 있으니까요. 다만 “끝난 결과를 나중에 조용히 확인"하는 용도로는 메일이 더 맞았습니다. 둘은 배타적이지 않아서 같이 켜두셔도 됩니다.
2. ntfy — 설정 5초, 첨부 3시간#
ntfy는 HTTP 요청 한 번으로 폰에 푸시를 쏘는 서비스입니다. 계정도 설정 파일도 없습니다. 토픽 이름을 정해서 앱에서 구독해두고, 그 주소로 던지면 끝이에요.
# 파일 첨부해서 보내기
curl -T 결과보고서.md -H "Filename: 결과보고서.md" \
-H "Title: 작업 완료" https://ntfy.sh/내가정한토픽이름실제로 해봤더니 응답에 첨부 정보가 이렇게 담겨 옵니다(전체 응답에서 첨부 부분만 발췌했습니다).
{
"attachment": {
"name": "결과보고서.md",
"size": 40,
"url": "https://ntfy.sh/file/Ktw6QNT7b42j.txt"
}
}공개 서버 기준 첨부는 15 MB까지, 3시간 뒤 삭제됩니다. 설정 난이도가 압도적으로 낮은 게 장점이고, 실시간성도 메일보다 낫습니다.
문제는 보안입니다. 공개 서버에서는 토픽 이름을 아는 사람은 누구나 구독할 수 있고, 이름 외에 다른 인증이 없습니다. 첨부 URL도 마찬가지예요. 추측하기 어려운 긴 이름을 쓴다 해도 그건 자물쇠가 아니라 그냥 안 알려진 상태일 뿐입니다. 업무 산출물이나 개인정보가 든 파일은 올리면 안 됩니다. “빌드 끝남” 같은 신호용으로 쓰거나, 직접 서버를 띄워서(self-host) 접근 제어를 걸고 쓰는 게 맞습니다.
3. 동기화 폴더 — 가장 단순한 방법#
결과물을 iCloud Drive나 Dropbox 폴더에 복사만 하는 방법입니다.
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 [email protected] -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분)처럼요. 스크린 리더 사용자는 메일 목록을 제목으로 훑는데, 제목이 전부 “알림"이면 목록이 아무 정보가 없는 벽이 됩니다. 이건 링크 텍스트에 목적지를 담아야 하는 이유와 같은 원리예요.
리눅스에서는#
키체인 부분만 바꾸면 나머지는 같습니다. 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, 끝난 결과를 나중에 확인하려면 메일. 같이 켜도 된다.
- 알림은 소리 하나에 의존하지 말고, 제목만 읽어도 알 수 있게 쓴다.
