13
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude CodeでGoogle Slidesを更新したくて、GAS Web Appにたどり着いた話

13
Posted at

はじめに

GMOコネクトの永田です。

毎週の定例会議のたびに、Google Sheetsから数字を拾ってGoogle Slidesのテーブルに転記しています。20個以上のセルを一つずつ手で更新しており、とても面倒でした😇

自動化したくて Google Slides API を調べたら GCP プロジェクトが必要、Claude にブラウザで直接編集させたらセル選択がズレまくる、といった試行錯誤を経て、最終的に GAS Web App に行き着きました。

その試行錯誤の記録です。


先にまとめ

最終的にこういう構成になりました。

Claude Code skill(SKILL.md)
  → curl(GAS Web App API)
    → Google Sheets(データ読み取り)
    → Google Slides(テーブル更新)
  • Google Slides API は GCP プロジェクトが必要。GAS なら Workspace 契約内で完結する
  • ブラウザ操作はテーブルのセル編集に向かない。API で構造化データを渡すほうが確実
  • GAS Web App の「全員」デプロイは 302 リダイレクトで POST ボディが消える。全操作 GET で設計した

やりたかったこと

定例会議のスライドには、四半期ごとの売上・粗利をまとめたテーブルが2枚あります。年度の四半期別サマリーと、当四半期の月別内訳です。

毎週、スプレッドシートの集計シートから数値を拾ってこの2枚を更新するのですが、セルが20個以上あり、手作業だと10〜15分かかります。

これを Claude Code のスキルにして、スライド更新して と言うだけで完了させたい。


なぜこのアーキテクチャにしたか

最初から GAS Web App を選んだわけではありません。

Google Sheets/Slides API を使わなかった理由

最初に思いつくのは Google の公式 API です。調べてみると、Google Workspace API の利用には GCP(Google Cloud Platform)プロジェクトの作成が必須でした。GCP プロジェクトの作成、Sheets API / Slides API の有効化、OAuth 2.0 クライアント ID の作成、トークンリフレッシュの実装。Google Workspace を契約していても GCP は別系統の管理画面なので、スライドの自動更新のためだけに GCP プロジェクトを立ち上げて認証基盤を管理するのはハードルが高い。

MCP Server(google-slides-mcp 等)も調べましたが、やはり GCP の OAuth 認証情報が前提になっていました。

2026年3月にリリースされた Google Workspace CLI(gws)もセットアップを簡略化してくれますが、GCP プロジェクト自体は必要です。まだ開発版ということもあり、今回は採用を見送りました。
(gwsがStableになって、ClaudeでGoogle Workspaceの自動操作を色々としたいとなったら、ぜひ使ってみたいです)

一方、GAS なら Google Workspace の契約内で完結します。GAS には Sheets や Slides を操作するビルトインサービスがあり、追加の認証設定なしで使えます。

Claude のブラウザ直接操作の限界

Claude in Chrome で直接スライドを編集することも試しました。

Google Slides のテーブルはセル選択がシビアで、クリック位置が数ピクセルずれると隣のセルを選択してしまいます。操作の間違いが頻発し、リトライで時間がかかる。20個以上のセルを順番に正確に更新するのは無理がありました。

テーブルのセル編集のように、ピクセル単位の精度が求められる操作にブラウザ操作は向いていません。

GAS Web App という選択

そこで、GAS をバックエンドにして Web App としてデプロイし、Claude Code からは curl で叩くことにしました。

比較軸 Google API 直接 ブラウザ操作 GAS Web App
GCP プロジェクト 必要 不要 不要
認証設定 OAuth2 必要 不要 独自トークン
精度 高い 低い 高い
実装の手軽さ 低い 高い

GAS なら Google Workspace 契約のみで使え、Sheets/Slides にネイティブアクセスでき、Web App としてデプロイすれば外部から HTTP で呼び出せます。


GAS 連携時のハマりポイント

ハマり1: 「ドメイン内」デプロイだと curl から叩けない(403)

GAS Web App のデプロイ設定にはアクセス権の選択肢があります。

設定 アクセス可能な範囲
自分のみ デプロイしたアカウントだけ
ドメイン内 同じ Google Workspace ドメインのユーザーのみ
全員 認証なしで誰でもアクセス可能

最初は「ドメイン内」でデプロイしました。ブラウザからは問題なくアクセスできますが、curl で叩くと 403 Forbidden が返ります。

$ curl -sL "https://script.google.com/a/macros/example.com/s/XXXXX/exec?action=readAll"
# → 403 Forbidden

「ドメイン内」設定では Google のログイン認証が要求されます。ブラウザなら既存のログインセッションで透過的に通りますが、curl ではこの認証フローを処理できないため 403 になります。

Claude Code からアクセスするには「全員」デプロイが必要ですが、そのままでは誰でもアクセスできてしまいます。そこで独自トークンによる認証を入れました。

独自トークン認証の実装

やったことは単純で、GAS 側の CONFIG にランダム生成したトークンを設定し、doGet() の冒頭でクエリパラメータ token と照合するだけです。一致しなければ {"error":"Unauthorized"} を返します。トークンはランダム値を生成し、呼び出し側では .env に保存して git の管理対象外にしています。

ハマり2: POST が使えない(302 → 405)

今回いちばんハマったところです。

トークンをクエリパラメータに載せると URL に含まれてしまう。セキュリティを考えると POST のリクエストボディに入れたい。そこで全操作を POST に変更してデプロイしました。

$ curl -sL -X POST "https://script.google.com/.../exec" \
  -H "Content-Type: application/json" \
  -d '{"token":"SECRET","action":"readAll"}'

返ってきたのは JSON ではなく、Google のエラーページの HTML でした。

調べると、GAS の「全員」デプロイは内部で 302 リダイレクトを行っています。

1. POST → script.google.com/a/macros/.../exec
2. ← 302 Found (Location: script.googleusercontent.com/.../exec)
3. GET → script.googleusercontent.com/.../exec  ← ここでボディが消える

HTTP の仕様上、302 レスポンスを受けたクライアントはリダイレクト先へのリクエストを GET に変えることが認められており(RFC 9110 Section 15.4.3)、実際ほぼすべてのクライアントがそう動きます。POST のリクエストボディはここで失われます。

ステータスコード リダイレクト後のメソッド ボディ
301 / 302 / 303 GET に変換 消える
307 / 308 元のメソッドを維持 保持される

RFC 上は 301/302 とも POST→GET の変換は MAY(任意)ですが、現実にはほぼすべてのクライアントが変換します。GAS が 302 を使っている限り、POST ボディは届きません。

なお、リダイレクト先を直接呼べばいいのではと思うかもしれませんが、後述の通りこのホスト自体が GET しか受け付けません。

ワークアラウンドも全滅

302 でもメソッドを維持するオプションを試しましたが、ダメでした。

# curl の --post302 オプション(302でもPOSTを維持)
$ curl -sL --post302 "https://script.google.com/.../exec" \
  -H "Content-Type: application/json" \
  -d '{"token":"SECRET","action":"readAll"}'
# → リダイレクト先が 405 Method Not Allowed

Python で手動リダイレクトも試しました。

import urllib.request

url = "https://script.google.com/.../exec"
# リダイレクトを自動追従せずにLocationヘッダーを取得
req = urllib.request.Request(url, method='POST', data=b'{"action":"readAll"}')
# → リダイレクト先のURLを取得して、そこにPOSTを送信
# → 405 Method Not Allowed

リダイレクト先の script.googleusercontent.com 自体が GET しか受け付けていませんでした。クライアント側の工夫では回避できない、GAS の仕様上の制約です。

結局、読み取りも書き込みも全操作 GET で設計しました。書き込みデータは URL エンコードした JSON をクエリパラメータ data で渡し、GAS 側で JSON.parse(e.parameter.data) して受け取る形です。トークンが URL に含まれる点は、HTTPS で暗号化されること、curl からしか使わずブラウザ履歴やリファラーには露出しないことから、今回は受容しています。


Claude Code スキルとして仕上げる

GAS Web App ができたら、Claude Code のスキルとして組み立てます。

SKILL.md の役割

Claude Code のスキルは ~/.claude/skills/<スキル名>/SKILL.md という Markdown ファイルで定義します。トリガーフレーズ、処理ステップ、API の叩き方などを書いておくと、Claude がそれに従って動きます。

今回のスキルは4ステップです。

Step 0: 年度確認(checkYear API)
  スプレッドシートの年度が正しいか確認

Step 1: データ読み取り(readAll API)
  スプレッドシートから最新の売上・粗利データを取得

Step 2: サマリー提示 → ユーザー承認
  読み取ったデータをテーブル形式で表示し、OKをもらう

Step 3: スライド更新(updateAll API)
  承認後にスライドのテーブルを一括更新

Step 2 でユーザー承認を挟んでいるのがポイントです。データを読み取った段階で一旦止めて、内容を確認してもらってから書き込みに進む。自動化しつつも最終確認は人間がやる、という方針です。

汎用的な設計パターン

外部APIでデータを取得し、整形して、ユーザーに確認してもらい、承認後に更新を実行する。この4ステップはスライド更新に限った話ではなく、たとえばデータベースの集計値をレポートに反映したり、外部 API の情報でドキュメントを更新したりといった場面にも同じ構成が使えます。

まとめ

GCP 不要で Google Slides を外部から更新する手段として、GAS Web App は手軽でした。ハマったのは GAS 固有の制約です。

  • 「全員」デプロイは 302 リダイレクトで POST ボディが消える。リダイレクト先も POST を拒否するので、全操作 GET で設計する
  • 書き込みデータは URL エンコードした JSON をクエリパラメータで渡す
  • 認証は独自トークン + .env で十分

302 の挙動は GAS のドキュメントにも書かれていないので、同じところでハマっている方の手がかりになればと思います。


最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

13
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?