Claude Codeで「届かないメール」を15分で特定した話と、マルチエージェント開発で踏んだ「棚の見落とし」問題
はじめに
前回の記事では、AIエージェント(Claude Code)と協力しながら24時間ライブ配信管理システムを実装する中で学んだ「陽性対照設計」の重要性を書きました。
その後のセッションでは、システムの安定稼働にとって別の種類の問題が次々と表面化しました。
- メール配達の失敗:β テスト案内が Yahoo! メールでことごとく迷惑メール扱いされる
- 情報の見落とし:「16日間返答がない」と思っていたら、返答は別の棚に届いていた
- マルチエージェント協調:複数の Claude セッションが並行して同じリポジトリで作業する際の競合と報告の齟齬
本記事ではこの3つを軸に、実際のエラーログ・ヘッダー・コード片とともに解説します。個人の感想として、「AIエージェントが並列に動く現場で人間が担うべき役割が見えてきた」と感じています。
やったこと
1. メール配達失敗の原因特定と DMARC 設置
β ユーザーへの案内メールが届かない問題を調査しました。Claude Code がヘッダーを解析し、送信元 IP のブラックリスト照合、SPF/DKIM/DMARC の確認、そして最終的に Yahoo! 固有の判定基準の切り分けまでを行いました。
2. マルチエージェント未実装監査
「Google ドライブ連携」「CP・admin 画面」「その他全体」を3体の Claude セッションに分担させ、それぞれ docs/unimplemented-audit/ 以下のファイルに結果を書き込みました。前回実装した陽性対照の考え方をそのまま監査設計に応用しています。
3. 本番サービスの定期見張り
LP(ランディングページ)と CP(管理画面入口)の死活確認を Claude Code の定期タスクとして整備しました。
ハマったポイント
1. Yahoo! メールの迷惑メール判定:認証3点セットが揃っていても落ちる
まず Posutto(別サービス)の障害報告が共有されました。原因は送信元の名乗りの不一致でした。
# PHP の mail() が使っていた envelope sender(問題の核心)
Return-Path: <旧サーバーアカウント> ← サーバーアカウントのまま
From: info@<your-domain> ← 差出人ドメインと違う
DMARC は From: ヘッダーのドメインと Return-Path のドメインが揃っているかを見ます。揃っていないと dmarc=fail になり、Yahoo! が迷惑メール扱いします。
同じ問題がながしっぱ(自社サービス)にも潜んでいないか確認しました。Claude Code がまず DMARC レコードの有無を確認:
# DMARC レコードを引く(Google のパブリック DNS で確認)
dig TXT _dmarc.nagashippa.jp @<Google-Public-DNS>
結果:レコード自体が存在しない。
次に notify-mail.ts の実装を確認:
// apps/web/lib/notify-mail.ts(抜粋・一部簡略化)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
await transporter.sendMail({
from: '"ながしっぱ" <support@<your-domain>>',
// envelope の指定なし → From: と同じドメインで送られる ✅
});
envelope を省略すると nodemailer は from フィールドをそのまま使います。つまり smtp.mailfrom=nagashippa.jp になり、SPF の許可ドメインと一致します。Posutto の問題(PHP の mail() がサーバーアカウントで envelope を送る)は踏んでいませんでした。
それでも Yahoo! に届かない。DMARC を置いて調査を開始しました:
# 置いたレコード(Xserver のサーバーパネルで設定)
_dmarc.nagashippa.jp TXT "v=DMARC1; p=none; rua=mailto:dmarc@<your-domain>; adkim=r; aspf=r"
反映を確認:
dig TXT _dmarc.nagashippa.jp @ns1.xserver.jp # 権威サーバー
# → "v=DMARC1; p=none; ..." が返る ✅
dig TXT _dmarc.nagashippa.jp @<Google-Public-DNS> # Google(全世界から見える状態)
# → 同じ値 ✅
その後、実際のメールヘッダーで確認:
Authentication-Results: mtaat2072.mail.snz.ynwp.yahoo.co.jp;
dkim=pass header.i=@nagashippa.jp header.s=default;
spf=pass smtp.mailfrom=nagashippa.jp;
dmarc=pass (p=NONE) header.from=nagashippa.jp;
認証3点セットは全部 pass。それでも迷惑メール箱に入ります。
切り分け実験:本文か差出人か
| 試し | 送信元 | 本文 | 結果 |
|---|---|---|---|
| A |
support@<your-domain>(Xserver <旧VPS>) |
3行の当たり障りのない文 | ❌ 迷惑メール |
| B | Gmail | β 案内そのまま | ✅ 受信箱 |
| C |
info@<your-domain>(同じ <旧VPS>) |
同じ3行 | ✅ 受信箱 |
同じサーバーの同じ IP で、ドメインを変えただけで結果が逆転しました。
原因は nagashippa.jp のドメイン年齢です。登録から41日目で、Yahoo! にとっては実績ゼロの新参ドメイン。認証が揃っていても、実績が無ければ疑われます。
# nagashippa.jp の登録日(切り分け後に確認)
登録: 2026-08-13
調査日: 2026-09-23
→ 41日目
個人の感想として、「認証を揃えれば届く」という期待が崩れた瞬間が印象的でした。SPF/DKIM/DMARC はなりすましを防ぐ仕組みであって、評判を保証するものではありません。
対策
即効性のある対策として、β ユーザーへの再送信を Gmail から行い、メール本文に「今後は support@<your-domain> から届きます。迷惑メール箱に入っていたら『迷惑メールではない』を押してください」と案内しました。
また、アプリの設定画面に注意書きを追加しました:
// apps/web/app/(dashboard)/settings/page.tsx(追加部分)
<p className="text-sm text-muted-foreground">
お知らせは <CustomerText>support@<your-domain></CustomerText> から届きます。
迷惑メールに振り分けられることがあるため、このアドレスを連絡先に登録してください。
</p>
<CustomerText> は既存のコンポーネントで、ブラウザの翻訳機能によるメールアドレス破壊を防ぎます。テストで警告が出た最初の実装(生の文字列で書いた版)を、このコンポーネントに差し替えて全件グリーンになりました。
2. 「16日間無音」の正体:棚の見落とし
プロジェクトで使っている中継システム(_relay/ 以下にファイルを置いて情報を受け渡す仕組み)で、次のような状況が起きました。
- 9/07:映像チーム向けに依頼ファイルを
_relay/eizo/に置く - 9/08:Claude Code が
_relay/repo/に回答ファイルを置く - 9/23:「16日間返答がない」として再送
実際には9/08に全部回答していました。
棚が2つある状況で、問い合わせた側は eizo の棚だけを見ていました。回答は repo の棚に置かれていた。
_relay/
eizo/ ← 映像チームが使う棚。依頼はここに置いた
repo/ ← Claude Code が回答を置く棚。9/08 に回答済み ✅
さらに問題があります。9/08 に別の Cowork セッションが回答を読んで返事(C729・C730)まで書いていたにもかかわらず、「未回答」という管理表(B表)が更新されていませんでした。
★★ 届いて、読んで、返事まで書いた物でも、
書き留める面に入っていなければ、届いていないのと同じ。
このインシデントが示す設計上の教訓は、「確認」と「記録」は別の動作ということです。情報を受け取った時点で記録が更新されなければ、後から見た人には「届いていない」と区別がつきません。
加えて、同じセッション内で立てた行動指針を別のセッションに引き継ぐ構造にも問題がありました:
★★★ 型を書いた面と、次に同じ場面に立つ面が、別の面なら、型は効かない。
便に書いた型は、便と一緒に流れます。型は次に踏む場面の紙に書きます。
これは「学習の文脈依存性」の問題です。あるセッションで得た教訓を、別のセッション(または将来の自分)が踏む場面に届けるには、教訓を具体的なチェックリスト・テンプレート・テストとして残す必要があります。
3. マルチエージェント監査で起きた3つの問題
複数の Claude セッションが並行して同じリポジトリで作業するとき、以下の問題が発生しました。
問題①:作業用ブランチ(worktree)が残ってデプロイが止まる
loopcast-docs リポジトリで、あるセッションが作業用の別コピー(git worktree)を作成したまま残してしまいました。
# worktree が残っているとリポジトリの検査が通らない
git -C /path/to/loopcast-docs worktree list
# → .claude/worktrees/now-md-s4-resolved (残ったまま)
# 点検スクリプトがこのディレクトリを拾って5本が赤になった
対策:統合後に必ず worktree を片付けるルールを全セッションの指示テンプレートに追記しました。
# 片付けコマンド(force は使わない)
git -C "<repo_path>" worktree remove ".claude/worktrees/<name>"
git -C "<repo_path>" branch -d <branch_name>
問題②:同じタスクが二重に起動される
NOW.md の §4 への解決済み印追記タスクが2つのセッションに同時に割り当てられ、片方が「すでに済んでいた」ことに気づかず空振りしました。
後から来たセッションの確認コメントは的確でした:
今の状況: 3か所とも、今日 18:53 の記録(6918020)ですでに main に正しく入っていました。
書く必要のある直しは無いので、報告のみとします。
対策:タスクを出す前に、対象ファイルの最近のコミットログを確認するステップを追加しました。
# タスク発行前の確認コマンド例
git log --oneline -10 -- loopcast-docs/docs/NOW.md
問題③:入れ子リポジトリへのアクセス制限
Youtubeライブ リポジトリの中に loopcast-docs という別の git リポジトリが入れ子になっています。外側の worktree から内側を操作しようとすると、セキュリティポリシーで止められます。
# ❌ 外側の worktree から git 操作しようとすると止まる
git -C "/path/to/worktree/loopcast-docs" commit -m "..."
# → 許可なし
# ✅ 絶対パスで元の場所を指定する
git -C "/Users/<username>/Desktop/<project-dir>/Youtubeライブ/loopcast-docs" show HEAD:docs/NOW.md
さらに読むだけは許可、書くのは禁止という非対称なルールがあります。監査タスクでは「読むだけ」と「書いてコミット」の境界を、毎回の指示に明示的に書く必要がありました。
4. シェルの | と ; でコマンドが許可待ちで止まる
Claude Code がシェルコマンドを実行する際、&& や ; でつないだコマンドは「許可待ち」で止まることがあります。特に監視タスク(LP の死活確認)で発生しました。
# ❌ つながれたコマンドは途中で止まることがある
curl -s https://nagashippa.jp/ | grep "title" && echo "OK"
# ✅ 1回に1コマンド。| は head と grep にだけ許可
curl -s -o /dev/null -w "%{http_code} %{time_total}" --max-time 20 https://nagashippa.jp/
# → 結果を見てから次のコマンドへ
curl -s --max-time 20 https://nagashippa.jp/ | head -c 3000
この制限を前提とした監視ルーティンを整備した結果、以下の形式が安定しました:
# 手順1:返事コードと応答時間
curl -s -o /dev/null -w "%{http_code} %{time_total}" --max-time 20 https://nagashippa.jp/
# 手順2:lang と title の確認(別コマンド)
curl -s --max-time 20 https://nagashippa.jp/ | head -c 3000
# 手順3:CP の入口(-L なしでリダイレクト先だけ確認)
curl -s -o /dev/null -w "%{http_code} %{redirect_url}" --max-time 20 https://cp.nagashippa.jp/
この手順を繰り返すことで、10月の2週間で毎朝・毎晩の監視を安定して継続できました。
学び
「届いた」と「記録された」は別のイベント
今回のセッション全体を通じて最も強く感じたのはこの点です。
- メールが届いても迷惑メール箱に入れば読まれない
- 回答が棚に置かれても、別の棚しか見ていなければ届いていないのと同じ
- コミットが main に入っていても、管理表が更新されなければ「未完了」として扱われる
「届いた」「回答した」「実装した」という主張は、受け取り側の確認と記録まで含めて初めて成立します。
期限のある材料は「待っても消える」
今回のログには印象的な表現がありました。
★ 待っている材料に期限が付いているとき、
無音は「進んでいない」ではなく「減っている」。
journald のローテーション(MaxRetentionSec=40day)で、8/26 のログが10/05 に消える問題です。他の決裁は「待てば残る」が、期限付きの材料は「待つと消える」。この非対称性を明示的に伝える枠組みが必要だと気づきました。
エージェントへの指示は「禁止事項を列挙するより、できる操作を列挙する」
マルチエージェント運用で安定していたのは、「禁止一覧」よりも「使えるコマンドの許可リスト」を明示した指示でした。
# 安定していた指示の形
使えるのは curl・ls・cat・head・tail・date・grep・find・wc と、
git の読むだけの命令です(| は head と grep にだけ)
否定の列挙は網羅できませんが、肯定の許可リストは境界が明確です。
数を直したら、その数から出た読みも直す
実装の訂正でよく起きる問題として、「数値は直したが、その数値から導いた解釈が古い版に残る」があります。
R495 §5「散らばっているのは段0だけ」← 30.9秒から出た読み
R496 最大値を 30.9秒 → 4.1秒に訂正
しかし §5 の読み(「段0だけが散らばる」)は R495 に残ったまま
直した数では段0の幅 3.2秒・段2の幅 3.7秒 で、同じくらい揺れる
次に訂正の便を書く際には、「直した数から出ている読みはどれか」を添える運用を採用しました。
まとめ
Claude Code を使った実開発では、AIが「何をすべきか」よりも「何を記録・報告すべきか」の設計が重要だと感じています。
今回の記事で扱った問題の核心はどれも「検知できない失敗」でした。届かないメール(迷惑メール箱)、届かない回答(別の棚)、消える実績(ドメイン年齢)、残るworktree(デプロイブロック)——いずれも「動いているように見える」状態で静かに失敗しています。
陽性対照(前回記事のテーマ)をシステム設計に埋め込むのと同様に、「見えない失敗」を可視化する仕組みを意識的に設計することが、人間とAIが協働する現場では特に重要だと感じています。