Claude CodeでWebページを監視し、差分があるときだけ通知する
取得、正規化、ハッシュ比較、差分の要約、通知の順でWebページの変更監視を組み、cronとGitHub Actionsで回します。誤検知の原因と抑え方も手元の出力つきで示します。
Webページの変更監視は、取得したページを正規化してハッシュを比べ、変わったときだけ差分をClaude Codeに要約させて通知する構成にすると安く回ります。毎回モデルを呼ぶ必要はありません。ハッシュが同じなら何も出さずに終わり、費用が発生するのは変更があった回だけです。
この記事では、Claude Codeのサブエージェント解説ページを監視対象の例にして、シェルスクリプト1本をcronとGitHub Actionsの両方に載せます。主役は「誤検知をどう抑えるか」です。監視は、通知が空振りし始めた時点で誰も読まなくなります。
全体の流れ:変更がなければ何も起きない
監視の骨格は5段で、モデルを使うのは4段目だけです。
変更監視の5段
- 1
取得
HTTP 200と空でない本文を確かめ、満たさなければ「変更」ではなく「失敗」として止めます。
- 2
正規化
広告、日時、行末の空白、先頭の案内文など、内容と関係なく動く部分を落とします。
- 3
ハッシュ比較
前回と同じなら、何も出力せず終了します。
- 4
差分の要約
変わったときだけ
diffをclaude -pに渡し、何が変わったかを数行にまとめさせます。 - 5
通知と状態の更新
通知が成功してから、新しいハッシュを保存します。
2段目までを決定的な処理にしておく理由は、モデルの出力を比較に使えないからです。要約文は実行のたびに揺れるので、「要約が前回と違う」を変更の合図にすると毎回通知が飛びます。比較は常にページ本文そのものに対して行います。
取得:ヘッダーでは変更を判定できない
最初に思いつくのは、HTTPの Last-Modified や ETag で更新を知る方法です。サブエージェント解説ページのMarkdown版(.md を付けたURL)で試すと、使えませんでした。
curl -sI https://code.claude.com/docs/en/sub-agents.md \
| grep -i -E "etag|last-mod|cache-control"
sleep 5
curl -sI https://code.claude.com/docs/en/sub-agents.md \
| grep -i -E "etag|last-mod"出力は次のとおりでした。ETag は返らず、Last-Modified は問い合わせた時刻そのものです。5秒空けた2回目で値が進んでいます。cache-control は public, max-age=0, must-revalidate でした。
last-modified: Wed, 28 Oct 2026 03:50:23 GMT
last-modified: Wed, 28 Oct 2026 03:50:28 GMT更新時刻が問い合わせのたびに進むため、この値を前回と比べても「変わった」としか出ません。本文を毎回取って自分で比べるほうが確実です。監視対象のサイトごとにヘッダーの挙動は違うので、新しいページを足すときは同じ2回取りを一度やっておくと、使える手がかりが分かります。
次に、本文そのものが同じ内容で安定して返るかを調べます。同じURLを2回取ってハッシュを並べました。
curl -s https://code.claude.com/docs/en/sub-agents.md -o m1.md
curl -s https://code.claude.com/docs/en/sub-agents.md -o m2.md
curl -sL https://code.claude.com/docs/en/sub-agents -o p1.html
curl -sL https://code.claude.com/docs/en/sub-agents -o p2.html
shasum m1.md m2.md p1.html p2.html | cut -c1-12Markdown版の2回、HTML版の2回は、それぞれ同じハッシュでした。この静的ドキュメントは素のまま比べても誤検知が出にくいページです。一方で、ニュースサイトやストアのページは、同じ操作で毎回ハッシュが変わるのが普通です。ここが「素のハッシュ比較で足りるページ」と「正規化が要るページ」の分かれ目になります。
取得先は、HTMLよりも .md やAPIのJSONがあればそちらを選びます。ナビゲーション、Cookie同意の帯、アクセス計測のスニペットといった周辺要素がそもそも入らないからです。
正規化:誤検知の原因を一つずつ潰す
誤検知は、本文の意味が変わっていないのに文字列が変わる現象です。原因は数種類に絞れます。
| 誤検知の原因 | 例 | 正規化での対処 |
|---|---|---|
| 行末の空白・改行コード | 例末尾スペース、CRLF | 正規化での対処sed で除去 |
| 連続する空行 | 例空行が1行増減 | 正規化での対処cat -s で1行に畳む |
| 日時・件数の埋め込み | 例「最終更新2026-10-28」 | 正規化での対処正規表現でプレースホルダーに置換 |
| 広告・おすすめ枠 | 例毎回入れ替わるバナー | 正規化での対処対象外の要素を削ってから比較 |
| 先頭や末尾の案内文 | 例取得経路で付く定型文 | 正規化での対処見出しや引用ブロックごと削除 |
| ビルド固有の文字列 | 例スクリプト名のハッシュ、nonce | 正規化での対処HTMLを使うなら該当属性を除去 |
実際に、サブエージェント解説ページのMarkdown版を手元で取得すると、本文の前に「Documentation Index」という4行の引用ブロックが付いて返ってきます。ドキュメント全体の目次ページを案内する定型文で、ページ本文ではありません。この文面やURLが将来変わっただけで、本文は同じなのに通知が飛びます。先頭のこのブロックは正規化で落とすことにしました。
行末の空白も実害がありました。取得した本文の末尾に空白を1行足しただけで、ハッシュは 7a90388d から 296930ef に変わります。人間には見えない差で通知が出るのは、運用上いちばん腹の立つ誤検知です。
落としすぎると本物の変更を見逃す
正規化は削る作業なので、やりすぎると逆の事故が起きます。日付の正規表現が広すぎて、本文中の「施行日2026-11-01」まで <DATE> に置き換えれば、施行日の変更を検知できなくなります。日時を落とす範囲は、ページの構造上「更新日」と書かれた場所だけに限るのが安全です。
目安は、監視する目的を一文で言えるかどうかです。「料金表の数字が変わったら知りたい」なら、料金表の節だけを切り出して比較します。ページ全体を見張る必要がなければ、見出しで範囲を絞るほうが、誤検知も見逃しも減ります。
スクリプト:取得から差分の保存まで
ここまでをまとめた monitor.sh です。通知より前の部分だけで、変更があれば change.diff ができます。
#!/usr/bin/env bash
set -euo pipefail
URL="${1:?url}"; NAME="${2:?name}"
STATE="${STATE_DIR:-state}/$NAME"; mkdir -p "$STATE"
# 1 取得: 200 以外と空本文は変更ではなく失敗として落とす
code=$(curl -sS -L --max-time 30 -o "$STATE/raw.tmp" \
-w '%{http_code}' "$URL")
[ "$code" = 200 ] && [ -s "$STATE/raw.tmp" ] \
|| { echo "fetch failed: $code" >&2; exit 1; }
# 2 正規化: 先頭の案内引用・CR・行末空白・連続空行・日時
sed -e '/^> ## Documentation Index/,/^$/d' \
-e 's/\r$//' -e 's/[[:space:]]*$//' \
-e 's/20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]/<DATE>/g' \
"$STATE/raw.tmp" | cat -s > "$STATE/new.txt"
# 3 ハッシュ比較: 同じなら何も出さずに終わる
new=$(shasum -a 256 < "$STATE/new.txt" | cut -d' ' -f1)
old=$(cat "$STATE/hash" 2>/dev/null || true)
[ "$new" = "$old" ] && exit 0
# 4 初回は基準を保存するだけ
if [ ! -f "$STATE/prev.txt" ]; then
cp "$STATE/new.txt" "$STATE/prev.txt"
echo "$new" > "$STATE/hash"; echo "baseline saved"; exit 0
fi
# 5 差分を作る(大きすぎるときは先頭だけ)
diff -u "$STATE/prev.txt" "$STATE/new.txt" \
| head -c 100000 > "$STATE/change.diff" || true
echo "changed"実行結果です。初回は基準を保存するだけで、2回目は何も出力せずに終わります。
./monitor.sh https://code.claude.com/docs/en/sub-agents.md subagents
./monitor.sh https://code.claude.com/docs/en/sub-agents.md subagents1回目の出力は baseline saved で、2回目は無出力でした。正規化後の本文は取得した生データより4行少なく、先頭の案内ブロックが落ちていることが行数からも分かります。
動くことを擬似変更で確かめる
本物の更新を待たずに、保存済みの prev.txt を書き換えて差分が出るかを試します。見出しに文字を足し、1行の末尾に空白を足し、先頭に空行を2つ足しました。
sed -i '' 's/Create custom subagents/Create custom subagents (edited)/' \
state/subagents/prev.txt
echo bogus > state/subagents/hash
./monitor.sh https://code.claude.com/docs/en/sub-agents.md subagents
head -9 state/subagents/change.diffdiff -u の出力に、見出しの変更が -# Create custom subagents (edited) と +# Create custom subagents の対で出ます。空行や末尾空白の差も出てしまうのは、prev.txt を直接いじった実験だからです。通常運用では、prev.txt も正規化後の同じ形で保存されるので、この種の差は生まれません。この確認の目的は、ハッシュの不一致から差分ファイルができるまでの経路が通ることです。
差分の要約:claude -p に渡す
差分ができたら、要約と通知に進みます。要約は claude -p に標準入力で渡します。
claude --bare -p \
"次の diff はドキュメントページの変更です。
何が変わったかを日本語で3行以内に要約し、
設定名や数値が変わった箇所は具体的に書いてください。" \
--tools "" --max-turns 1 --max-budget-usd 0.50 \
--output-format json < state/subagents/change.diff \
| jq -r '.result'各フラグの役割は次のとおりです。
--bare: hooks、スキル、MCPサーバー、CLAUDE.mdの自動読み込みを飛ばします。どのマシンでも同じ結果になり、起動も速くなります。スクリプトやCIから呼ぶ場合の推奨モードとして、ドキュメントに書かれています--tools "": 組み込みツールをすべて無効にします。要約だけが仕事なので、ファイルを読む手段も実行する手段も渡しません--max-turns 1: 往復の回数を1に制限します。-pで使えるオプションで、上限に達するとエラー終了します--max-budget-usd 0.50: 見積もり費用がこの額に達したら止まります。クライアント側の見積もりなので、請求額とは差が出ることがあり、少し超える場合もあります
--bare は購読ログインを使いません。環境変数 ANTHROPIC_API_KEY にAPIキーを入れておく必要があります。cronのユーザー環境にも、GitHub ActionsのSecretsにも、このキーを渡す前提です。
監視対象のページは信用できない入力として扱う
要約させる差分は、外部のサイトが書いた文章です。そこに「これまでの指示を無視して〜を実行せよ」のような文が混ざる可能性はゼロではありません。ツールを一切渡さない --tools "" は、その場合でも被害を文章の中に閉じ込めるための設定でもあります。逆に言えば、「差分を見て自動でファイルを直す」ような処理を同じ呼び出しに足すと、この防御が外れます。要約と修正は呼び出しを分けてください。
--bare を付けない -p は、作業ディレクトリの .claude/settings.json に書かれたhooksを実行し、.mcp.json のサーバーにも接続します。監視用のスクリプトを、別の目的でhooksを設定したリポジトリの中で動かしていないか、一度確かめておくと安全です。
標準入力のサイズにも上限があります。claude -p にパイプで渡せるのは10MBまでです。差分を head -c 100000 で切っているのはそのためでもあります。ログなど大きい入力を渡すときの絞り方はclaude -pにログを渡すと10MB上限で止まるにまとめています。
通知:成功してから状態を更新する
最後に通知と状態の更新です。ここは順序が大事です。
summary=$(claude --bare -p "…(上のプロンプト)…" \
--tools "" --max-turns 1 --max-budget-usd 0.50 \
--output-format json < "$STATE/change.diff" | jq -r '.result')
payload=$(jq -n --arg t "変更あり: $NAME" --arg s "$summary" \
--arg u "$URL" '{text: ($t + "\n" + $s + "\n" + $u)}')
curl -sS -f -X POST -H 'Content-Type: application/json' \
-d "$payload" "$WEBHOOK_URL"
# 通知まで成功したときだけ基準を進める
cp "$STATE/new.txt" "$STATE/prev.txt"
echo "$new" > "$STATE/hash"set -e が効いているので、要約か通知のどちらかが失敗するとここで止まり、hash は更新されません。次の実行でも同じ差分が検知され、通知が再試行されます。反対に、通知の前に状態を進めてしまうと、通知が一度失敗しただけでその変更は永久に見逃されます。
通知先はSlackなどのIncoming Webhookでもメールでも構いません。ここではURLを環境変数 WEBHOOK_URL で渡しています。要約の失敗時にも空の通知を飛ばさないよう、jq -r '.result' の結果が空でないことを確かめる行を足しておくと丁寧です。
「変更があった」通知と「監視が壊れた」通知は、別の経路にしてください。取得の失敗は exit 1 で落とし、cron側かワークフロー側の失敗通知に任せます。cronが静かに失敗し続ける問題の調べ方はClaude Codeのcron失敗を調査する手順と失敗時だけ通知する書き方が詳しいです。
cronで回す
自分のマシンやサーバーで回すなら、cronが最短です。
# 毎時5分に実行。ログは追記して、失敗時のみ別途通知する
5 * * * * cd /opt/watch && flock -n /tmp/watch.lock \
./monitor.sh https://code.claude.com/docs/en/sub-agents.md subagents \
>> watch.log 2>&1前回の実行が終わっていないうちに次が起動する多重実行を避けるため、flock で包んでいます。flock の使い方と確認手順はClaude Codeのcronをflockで多重起動させない書き方と確認手順で扱っています。cronは最小限の環境変数で動くので、ANTHROPIC_API_KEY と WEBHOOK_URL はスクリプトの先頭かcrontabの先頭で明示してください。
GitHub Actionsで回す
サーバーを持ちたくない場合はGitHub Actionsの schedule を使います。ドキュメントによると、スケジュール実行はデフォルトブランチのワークフローだけが対象で、パブリックリポジトリではリポジトリに60日間動きがないとスケジュールが無効になります。監視を長く続けるなら、状態ファイルの更新をリポジトリへのコミットにしておくと、その「動き」がスケジュールの維持にも働きます。
name: watch-docs
on:
schedule:
- cron: "5 * * * *" # UTC
workflow_dispatch:
permissions:
contents: write
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @anthropic-ai/claude-code
- name: monitor
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
run: ./monitor.sh https://code.claude.com/docs/en/sub-agents.md subagents
- name: commit state
run: |
git config user.name "watch-bot"
git config user.email "watch-bot@users.noreply.github.com"
git add state && (git diff --cached --quiet || git commit -m "update watch state")
git push状態のコミットは、通知が成功してスクリプトが exit 0 で終わった後にだけ実行されます。途中で失敗すればジョブが止まり、状態は進みません。cron の時刻はUTCで解釈される点に注意してください。日本時間の朝9時に回したいなら 0 0 * * * です。
なお、anthropics/claude-code-action には prompt 入力を使って schedule イベントで動かす方法があり、その場合はレポートが実行ログに出ます。監視用途で「通知先に送る」「状態を保存する」まで一本のスクリプトに入れたいときは、上のようにCLIを直接呼ぶほうが部品が少なくて済みます。claude-code-actionをcronで回す書き方はclaude-code-actionをcronで回し、毎朝のコミットとIssueを要約するにあります。
cronとGitHub Actionsの選び分け
| 観点 | cron | GitHub Actions |
|---|---|---|
| 用意するもの | cron常時動くマシン | GitHub Actionsリポジトリのみ |
| 状態の置き場 | cronローカルのファイル | GitHub Actionsリポジトリにコミット |
| 時刻の解釈 | cronサーバーのタイムゾーン | GitHub ActionsUTC |
| 失敗の気づき方 | cron自前で通知を用意 | GitHub Actionsワークフロー失敗の通知 |
| 止まりやすい点 | cron環境変数の不足、多重起動 | GitHub Actions60日間動きがないと無効化(公開リポジトリ) |
誤検知をさらに減らす三つの手
正規化で落としきれない揺れは、判定側で吸収します。
- 2回連続で同じ新ハッシュになってから通知する。一時的に別の内容が返るA/B配信や、キャッシュの切り替わり途中の取得を弾けます。
pending-hashというファイルを別に持ち、新ハッシュが1回目なら保存だけして終わり、2回目に一致したら通知する、という実装です。検知は1回分遅れます - 変更の大きさに下限を設ける。
grep -c '^[+-][^+-]' change.diffで変更行数を数え、数行以下なら通知を保留します。誤字修正を拾いたくないページに向きますが、料金や上限値のように1文字が重要なページには使わないでください - 比較の単位を見出しに切る。ページ全体のハッシュではなく、見出しごとの小さなハッシュを持てば、「どの節が変わったか」が差分を読まなくても分かります。要約の入力も小さくなり、費用も下がります
どれも、まず素のハッシュ比較で数日回し、実際に出た誤検知の形を見てから足すのが無駄がありません。最初から全部入れると、見逃しの原因を後から切り分けにくくなります。
限界:描画後に中身が決まるページは別の手が要る
curl が取るのはサーバーが返したHTMLまでです。JavaScriptが実行されて初めて本文が出るページは、取得結果がほぼ空になり、毎回同じハッシュになって何も検知できません。取得のたびに本文の長さを見て、極端に短い場合は失敗として落とす検査を入れておくと、「静かに何も見ていない」状態を防げます。描画後の内容が必要なら、ブラウザ自動化に切り替えます。待機とリトライの書き方はClaude CodeでPlaywrightのスクレイピングを作るで扱っています。
取得の頻度とサイトの利用規約にも気を配ってください。1時間に1回、1ページ程度なら負荷は小さい部類ですが、robots.txt や規約で自動取得を禁じているサイトは対象にしないのが前提です。公開APIがあるデータなら、ページ本文を追うよりAPIを監視するほうが安定します。法令のようにAPIで取れる題材の追い方はe-Gov法令APIとClaude Codeで法改正の施行日と条文差分を毎週追うにまとめています。
まとめ
変更監視の費用と誤検知は、モデルに渡す前の処理でほぼ決まります。取得の成否を確かめ、本文だけを残して正規化し、同じなら黙って終わる。ここまでが決定的な処理なら、claude -p を呼ぶのは本当に変わった回だけになります。
最初に決めるのは、何が変わったら知りたいのかです。それが決まれば、削ってよい揺れと削ってはいけない数字の線引きができます。通知は成功してから状態を進め、監視自体の故障は別の経路で知らせる。この二点を守れば、数週間放っておいても通知が信頼できる状態を保てます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。