🤖 AIとつくる

Gmail の確認を Claude Code に任せる — 読み取り専用の CLI を作った

約4,800字
ターミナルで gmail-cli search from:sc-noreply@google.com を実行し、Google Search Console からの通知メール2件の日時・差出人・件名が一覧表示されている画面。上部に「Gmail の確認を Claude Code に任せる」の見出し

X の通知や Google Search Console のお知らせなど、確認したいメールが少しずつ増えてきました。毎日受信箱を見に行くのは手間なのと、漏れがあるかもしれないので、Claude Code に読んでもらうことにしました。

Google Cloud の設定から読み取り専用の小さな CLI まで Claude Code に作ってもらいました。認可したのは最初の1回で、3週間使っていますがまだ認可は切れていません。Gmail API の OAuth スコープを gmail.readonly にして、削除も送信もできない状態で読み取りしてもらっています。

どの方式にするかは私が判断して、設定・実装・動作確認は Claude Code に任せました。Google Cloud の管理画面も認可画面も、私はブラウザを一度も触っていません。

以降は、設定と実装を担当した Claude Code の執筆です (内容は私が確認・修正しています)。


何ができるようになったか

依頼者は、読みたいメールを指定して頼む形で使っています。

  • 「最新のメールを読んで」
  • 「何月何日のメールを読んで」
  • Gmail の画面で開いたメールの URL をコピーして「これ読んで」

3つめが手軽です。Gmail の URL は末尾がそのスレッドの ID になっていて、スレッドの ID は先頭のメールの ID と同じなので、そのまま CLI に渡せます。

セッション側では、この2つのコマンドを組み立てて実行しています。

# 条件で絞って一覧
$ gmail-cli search "from:sc-noreply@google.com newer_than:7d"

# ID を指定して本文を読む
$ gmail-cli read 19fdedf31466201c

検索の条件は Gmail の検索構文がそのまま使えるので、from:newer_than: で絞れます。実際に読んでいるのは、Search Console からのインデックス関連の通知 (「重複しています。Google により、ユーザーがマークしたページとは異なるページが正規ページとして選択されました」など) や、X からのアカウント関連の通知です。

依頼者は将来的に、通知メールを定期的に自動で読ませることも考えています。この CLI はそのベースになるものです。いますぐ困っている状況ではないので、優先度が上がってきた時点でさらに自動化を進める予定です。

4つの案を比べる

考えられる案は4つありました。

渡すことになる権限持ち味判断
claude.ai の Gmail コネクタ提供側が決めた範囲 (利用者は選べない)設定するだけで使える。claude.ai の Web やスマホからも同じ受信箱を扱える不採用
IMAP + アプリパスワード受信箱の全操作メールソフトと同じ枯れた経路。アプリ側の実装がいちばん軽い不採用
Gmail API + OAuthスコープで選べる読み取りだけに固定できる。cron や claude -p からも動く採用
Playwright で Gmail の画面を操作画面でできること全部ログイン済みプロファイルがあり、その日のうちに動かせる不採用

コネクタは設定するだけで済むので有力でしたが、この環境ではブログ運用とは別の Google アカウントで Drive コネクタを接続済みでした。コネクタは一度認証したアカウントを切り替えられないという報告があり (anthropics/claude-code#59512)、加えて claude -p や cron の headless 実行ではコネクタのツールが読み込まれないという報告もあります (#36833)。どちらも stale ラベルで自動クローズされていて、修正されたという記載は見つかりませんでした (2026-08 時点)。日次の集計に組み込む先を考えるとこの制約が効くため、コネクタの接続枠はもう一方のアカウント用に空けておくことにしました。

アプリパスワードと OAuth は、どちらも自作のプログラムから読む方式ですが、権限の絞り方が違います。この点は依頼者からの質問ではっきりしました。

👤 ブログ主のプロンプト
案2でも readonly にできたりしないんですかね? どこまでのアクセスが許可されますかね?
🤖 Claude Code
アプリパスワードは受信箱の全操作ができる合鍵なので、読み取りだけには絞れない。Gmail API + OAuth ならスコープを gmail.readonly に指定でき、認可していない操作は通らない — と回答 (要約)

アプリパスワードは権限を選べず、OAuth は選べる。この違いから、読み取りだけに固定できる Gmail API + OAuth を採用しました。

ブラウザ操作の案は、Google 側から見た自動化の扱いがはっきりしないため、恒久的に使う経路にはしない判断です。

gmail.readonly は読み取り以外を通さない

Gmail API の権限は、アクセストークンを取るときに指定するスコープで決まります。公式のスコープ一覧から、範囲の狭い順に抜粋するとこうなります。

スコープ分類公式の説明 (要約)
gmail.labels非機密ラベルの参照と編集
gmail.metadata制限付きラベルやヘッダーなどのメタデータを参照。本文は読めない
gmail.readonly制限付きメールと設定を参照する (採用したのはこれ)
gmail.send機密自分の代わりにメールを送信する
gmail.modify制限付き読む・作成する・送る。ゴミ箱を経由しない完全削除はできない
https://mail.google.com/制限付き読む・作成する・送る・完全に削除する

分類は Google 側の区分で、制限付き (restricted) は本来アプリの審査対象になる区分です (この記事の後半で触れます)。読むだけなら gmail.readonly、ヘッダーだけで足りるならさらに狭い gmail.metadata も選べます。認可していない操作は API を通りません

認可の受け取りは、デスクトップアプリ向けのループバック方式です。CLI が空きポートで一時的な HTTP サーバーを立て、ブラウザでの同意が終わると Google がそのポートへ認可コードを返してきます。

CLIlocalhost:PORTBrowserGoogleconsentauth URLscoperedirect: code → token

CLI が認可用の URL を作ってブラウザへ渡し、同意した結果 (認可コード) がループバックのアドレス経由で CLI に戻ってきて、トークンに交換される、という流れです。

個人利用で気をつける2点

1つめ: OAuth アプリの公開ステータスを「本番」にする。 「テスト」のままだとリフレッシュトークンの有効期限が7日になり、毎週認可し直すことになります。自分しか使わないアプリですが、本番に切り替えておきました。

2つめ: 審査は要らないが、認可時に警告が出るgmail.readonly は制限付きスコープなので本来はアプリの審査 (セキュリティアセスメント) の対象ですが、個人利用 (100人未満) であれば審査を受けずに使えます。そのかわり、認可のときにこの画面が出ます。

Google の認可画面に出る警告。「このアプリは Google で確認されていません」という見出しと、デベロッパー欄 (メールアドレスはマスク) の説明、「安全なページに戻る」ボタンが表示されている

自分で作ったアプリを自分で許可するので、ここは「詳細」を開いて先へ進みます。

Claude Code に管理画面を操作させるときの注意点

Google Cloud プロジェクトの作成、Gmail API の有効化、同意画面の構成、本番への切り替え、デスクトップアプリのクライアント作成、そして最後の認可フローまで、すべて Claude Code が Playwright でブラウザを操作して進めました。この環境では Playwright のブラウザを仮想ディスプレイに隠して動かしているので (前に書いた記事)、依頼者の画面には何も出ません。

同じことをする人向けに、引っかかった点を2つ。

  • Google Cloud の管理画面の「開始」ボタンは <button> ではなく <a> タグなので、getByRole('button') では見つかりません。shadow DOM も含めてテキストが一致する要素を数え上げ、座標を出してクリックする形にしました。見た目がボタンでも要素が違うことがある、というのは管理画面を自動操作させるときの注意点の1つです
  • metadataHeaders を複数指定するときは urlencode(..., doseq=True) を付ける。付けないと配列が文字列化されて Gmail API に渡り、エラーにならないまま差出人と件名が空で返ってきます
>>> d = {"metadataHeaders": ["From", "Subject"]}
>>> urllib.parse.urlencode(d)
'metadataHeaders=%5B%27From%27%2C+%27Subject%27%5D'   # 配列が文字列のまま送られる
>>> urllib.parse.urlencode(d, doseq=True)
'metadataHeaders=From&metadataHeaders=Subject'        # 期待どおり

まとめ

メールを確認する作業を、指定して頼むだけの形にできました。スコープを読み取りに固定してあるので、頼むたびに「これは大丈夫か」を考えなくて済んでいます。

いま必要なのは読むところまでですが、下書きを作ってもらいたくなれば、そのときスコープを足して認可し直すことになります。最初から広い鍵を渡さなくても、必要になった分だけ足せるのが OAuth の使い勝手だと思っています。

一方で、鍵の管理そのものは残ります。クライアントシークレットとトークンは ~/.config/gmail-cli/ に権限 600 で置いていて、ここが漏れれば読み取りはできてしまいます。スコープを絞るのは「できることを減らす」対策で、「鍵を守らなくてよくなる」わけではありません。

同じようにメール確認を任せたい方の参考になればうれしいです。

実装メモ: 小さく作るための選択

CLI は Python 3.12 の標準ライブラリだけ、233行です (urllib.requesthttp.server)。サブコマンドは auth / labels / list / search / read の5つ。細かい選択を3つ書いておきます。

  • リダイレクト先は http://localhost:<空きポート>。公式ドキュメントが挙げているのは http://127.0.0.1:porthttp://[::1]:portlocalhost は挙げられていませんが、デスクトップクライアントではこの形で通っています。ポートは起動のたびにソケットを開いて空き番号を取得し、その番号でローカルの HTTP サーバーを立てて認可コードを受けます
  • アクセストークンの有効期限を管理していない。呼び出しのたびにリフレッシュトークンで取り直しています。1日に数回しか叩かない使い方なので、期限の判定を持つより短く書けるほうを選びました
  • 接続が切れたら2回まで再試行する。一覧を取ってから1件ずつメタデータを引く作りなので、10件取ると11回叩きます。この連打で RemoteDisconnected がたまに出たため、素朴なリトライを入れています

参考リンク

一次情報 (Google 公式):

同じテーマの記事・体験談: