Claude CodeでTampermonkeyスクリプトを作り、壊れたセレクタを直す
Tampermonkeyのユーザースクリプトを、メタデータ(@match・@grant)の絞り込みから書かせ、ページ改修で壊れたセレクタを保存したHTMLで再現して直す手順です。
Tampermonkeyのユーザースクリプトは、先頭のメタデータ(// ==UserScript== から // ==/UserScript== まで)と、その下のJavaScriptで成り立ちます。Claude Codeに任せやすいのは、この2つを分けて考えたときです。メタデータは「どのページで、何の権限で動かすか」を決める契約で、JavaScript側は「ページのどこを触るか」を決めます。壊れやすいのは後者で、サイトの改修でセレクタが外れるたびに動かなくなります。
この記事では、架空の社内チケット画面を例に、メタデータを最小の権限で書かせるところから、改修後に壊れたセレクタを保存したHTMLで再現して直すところまでを順に追います。
メタデータは「狭く書く」ことを先にClaude Codeへ伝える
Tampermonkeyの公式ドキュメントでは、@match は <protocol>://<domain><path> の形で、スクリプトを動かすページを指定します。ドメインには *.tmnk.net のようにワイルドカードを使え、これは tmnk.net とそのサブドメインに一致します。複数行の指定もできます。
指定が広すぎると、無関係なページでもスクリプトが走ります。依頼文には、対象を具体的なURLで書いておきます。
社内チケット一覧(https://tickets.example.com/list 配下)だけで動く
Tampermonkeyのユーザースクリプトを tickets-highlight.user.js に作って。
- @match は上記のパスだけ。*://*/* は使わない
- @grant は使う関数だけ。不要なら none
- iframe では動かさない
- セレクタはファイル先頭の定数にまとめる最後の1行が、後半の「壊れたセレクタを直す」手順の土台になります。セレクタが関数のあちこちに散らばっていると、直す範囲が見えません。
@matchと@includeの違い
@include も似た役割ですが、公式ドキュメントによると、*://tmnk.net/* のように書くと https://example.com/?http://tmnk.net/ にも一致する、という落とし穴があります。Tampermonkeyは :// を含む @include を @match に近い形で解釈して、スキームとホストの部分でワイルドカードが過剰に広がらないようにしています。ただし @include はURLのハッシュ部分を扱えず、正規表現(/^https:\/\/…$/)も書けます。生成されたスクリプトに @include * が入っていたら、狭い @match に置き換えてもらいます。
@exclude は、@include や @match に含まれていても除外したいURLを指定するためのものです。管理画面だけ除きたいときなどに使えます。
@grantは「使う関数の一覧」にする
@grant は、GM_* や GM.* の関数、unsafeWindow、一部の window 関数を使う許可の一覧です。たとえば値を保存するなら GM_setValue と GM_getValue を並べます。
動作は3通りに分かれます。
@grantの書き方で変わること
関数を列挙する
列挙した
GM_*だけが使えます。基本はこの形です。@grant none
サンドボックスが無効になります。
GM_*関数は使えず、GM_infoだけが使えます。@grantを書かない
空のリストとして扱われます。
noneとは別の状態です。
「何も書かない」と「none」が違う点は、レビューで見落としやすい箇所です。スタイルを足したいだけなら GM_addStyle を @grant に書く、DOMを読んで印を付けるだけなら none にする、というように、使う関数から逆算して決めます。
window.close や window.focus のようなタブ操作も、使うなら @grant に書く必要があります。タブを閉じる機能は強力なので、公式ドキュメントもその旨を断っています。
最小のスクリプトを書かせる
例として、期限切れのチケット行を目立たせるスクリプトを考えます。ページは table.ticket-list の各行に data-status="overdue" が付いている、という前提です。
// ==UserScript==
// @name Ticket highlight
// @namespace https://tickets.example.com/
// @version 0.1.0
// @description 期限切れのチケット行を目立たせる
// @match https://tickets.example.com/list*
// @grant GM_addStyle
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
const SELECTORS = {
overdueRows: 'table.ticket-list tr[data-status="overdue"]',
};
GM_addStyle('.tm-overdue { outline: 2px solid #d33; }');
function markRows(root) {
const rows = root.querySelectorAll(SELECTORS.overdueRows);
rows.forEach((row) => row.classList.add('tm-overdue'));
return rows.length;
}
markRows(document);
})();メタデータの意味を、公式ドキュメントの記述と照らして確認します。
@run-at document-idleは、DOMContentLoadedの後に注入される設定で、@run-atを省いたときの既定値です。@noframesは、メインのページでだけ動かし、iframeでは動かさない指定です。@versionは、更新確認を使うなら必須です。@updateURLに更新確認用のURLを書く場合、@versionがないと更新確認が動きません。
配布せず自分のブラウザだけで使うなら、@updateURL と @downloadURL は不要です。
動的に描画されるページでは@run-atだけでは足りない
ページがJavaScriptで後から行を描画するなら、document-idle の時点で行がまだ存在しないことがあります。その場合は MutationObserver で監視する形にするか、シングルページアプリならURL変更を拾います。Tampermonkeyには window.onurlchange があり、@grant window.onurlchange を書くと使えます。
// @grant window.onurlchange
if (window.onurlchange === null) {
window.addEventListener('urlchange', () => markRows(document));
}window.onurlchange が null なら、その機能が使える環境です。@include はハッシュ部分を扱えないので、ハッシュで画面が切り替わるサイトはこの仕組みで拾います。
壊れたセレクタを保存したHTMLで再現する
ここからが本題です。サイトが改修されて、行が <table> から <div role="row"> に変わったとします。スクリプトは何も起こさず、エラーも出ません。querySelectorAll は一致しなければ空のリストを返すだけだからです。
Claude Codeに「直して」とだけ頼むと、ページの実物を見ていない状態で当て推量のセレクタを書くことになります。ベストプラクティスのページには、Claudeが読み取れる確認手段(テスト、ビルドの終了コード、スクリーンショットなど)を渡すと、人が張り付かなくても進められる、とあります。ユーザースクリプトでは、その確認手段を自分で用意します。
手順1: 壊れた時点のDOMを保存する
ブラウザの開発者ツールで、対象部分の outerHTML をコピーして保存します。個人情報や社内の固有名詞は、ここで伏せ字にします。
壊れたセレクタを直す流れ
- 1
改修前後のHTMLを保存する
動いていた時期の断片を
fixtures/before.html、壊れた後のものをfixtures/after.htmlに置きます。 - 2
一致件数を確認するテストを書かせる
before.htmlでは1件以上、after.htmlでは現状0件、という失敗するテストを先に作ります。 - 3
セレクタを直して全フィクスチャで通す
直した後は、
before.htmlとafter.htmlの両方で件数が出ることを確認します。 - 4
実ページで目視する
テストが通っても、最後は実ページで印が付くかを自分で見ます。
手順2: 件数を検査するテストを書かせる
Node.jsで jsdom を使うと、保存したHTMLに対してスクリプトを実行できます。ブラウザを起動する検証が必要なら、Claude CodeでRustのWebAssemblyを作るのヘッドレスブラウザ検証が参考になります。ここで使うコードは筆者側の組み立て例で、jsdom は別途インストールが必要です。
npm install --save-dev jsdom
node --test test/// test/selectors.test.js
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const { JSDOM } = require('jsdom');
function load(htmlPath) {
const html = fs.readFileSync(htmlPath, 'utf8');
const dom = new JSDOM(html, { runScripts: 'outside-only' });
dom.window.GM_addStyle = () => {};
const code = fs.readFileSync('tickets-highlight.user.js', 'utf8');
dom.window.eval(code);
return dom;
}
test('改修後のHTMLで期限切れ行に印が付く', () => {
const dom = load('fixtures/after.html');
const marked = dom.window.document.querySelectorAll('.tm-overdue');
assert.ok(marked.length > 0, '一致が0件です');
});スクリプトの末尾で markRows(document) を呼んでいるので、eval した時点で処理が走ります。GM_addStyle はTampermonkey側が提供する関数なので、テストでは空の関数に差し替えています。
この段階で fixtures/after.html のテストは失敗します。失敗の出力をClaude Codeに読ませ、「SELECTORS の中だけを直す。fixtures/before.html のテストも残す」と指示します。
test/selectors.test.js が after.html で失敗している。
fixtures/after.html の構造を読み、SELECTORS.overdueRows だけを直して。
- before.html と after.html の両方のテストを通す
- 直した理由を、2つのHTMLの差として説明する
- 一致件数が0件のままなら、セレクタを広げず報告して止まる最後の条件が、当て推量を止める歯止めです。div だけを狙う広いセレクタを書けば件数は0になりませんが、無関係な要素に印が付きます。セレクタの壊れ方をテストで検査する考え方は、スクレイパーの契約検査を扱ったClaude CodeでScrapyのスクレイパーを作ると共通しています。
手順3: セレクタの候補を持たせる
改修のたびに書き換えるのが負担なら、SELECTORS に候補の配列を持たせる形に変えてもらう方法があります。
const SELECTORS = {
overdueRows: [
'table.ticket-list tr[data-status="overdue"]',
'[role="row"][data-status="overdue"]',
],
};
function findRows(root) {
for (const selector of SELECTORS.overdueRows) {
const rows = root.querySelectorAll(selector);
if (rows.length > 0) return rows;
}
return [];
}この形は、旧版と新版が混在する期間に向いています。一方で、候補が増えるほど古い分が残り続けるので、フィクスチャのテストで全候補が使われているかを確認し、使われなくなったものは削ります。
実ページの確認にはClaude in Chromeを使える
保存したHTMLは「ある時点の断片」でしかありません。実際のページの状態は、Claude CodeのChrome連携で読み取らせられます。公式ドキュメントでは、コンソールのエラーやDOMの状態を直接読み、原因のコードを直す使い方が挙げられています。
使うには、次の条件があります。
- Google Chrome、Microsoft Edge、またはBrave・Arc・Vivaldi・Operaのような別のChromium系ブラウザ(WSLでは使えません)
- Claude in Chrome拡張のバージョン1.0.36以降
- Pro、Max、Team、Enterpriseのいずれかの直接のAnthropicプラン
/login でサインインしている必要があり、APIキーや claude setup-token で作った長期トークンで認証していると、Chrome連携は無効のままです。
Tampermonkeyを入れた同じブラウザで連携を使えば、スクリプトが走った後のDOMを読ませられます。依頼文には、見てほしいパターンを具体的に書きます。
https://tickets.example.com/list を開いて、
.tm-overdue が付いた要素の数と、コンソールのエラーの有無を教えて。公式ドキュメントは、コンソールの出力は冗長になりやすいので、探すパターンを絞って伝えるよう勧めています。サイト単位の権限は拡張側の設定を引き継ぐので、社内システムのように読ませたくないサイトは、拡張の設定で制限します。
外部通信と外部ライブラリを足すときの注意
スクリプトから別のサイトへリクエストを送るなら、GM_xmlhttpRequest を @grant に書き、送信先を @connect に列挙します。@connect に書けるのは、ドメイン名(サブドメインを含めて許可)、サブドメイン名、self(スクリプトが動くドメイン)、localhost、IPアドレス、* です。リダイレクトがある場合は、最初のURLと最終URLの両方が検査されます。
// @grant GM_xmlhttpRequest
// @connect api.example.com@connect * は、確認ダイアログに「常に許可」ボタンを出すためのものとして公式ドキュメントに紹介されていますが、個人用のスクリプトでは、送信先を列挙して * を付けない選択もできます。生成されたスクリプトに @connect * が入っていたら、本当に任意のドメインへ送る必要があるのかを確認します。
外部ライブラリは @require で読み込みます。公式ドキュメントには、URLの末尾に #sha256=… を付けて整合性を検査する書き方が示されています。CDNの配布物が差し替わった場合の備えになるので、Claude Codeにライブラリ追加を頼むときは、バージョンを固定したURLとハッシュ付きで書くよう指定します。
プロジェクトのCLAUDE.mdに書いておく規約
毎回同じ指示を書かずに済むよう、スクリプトを置くディレクトリのCLAUDE.mdに規約をまとめます。
## ユーザースクリプトの規約
- @match は対象の具体的なパスだけ。*://*/* と @include * は使わない
- @grant は使う関数だけ。不要なら none
- セレクタは SELECTORS 定数に集約し、関数内にベタ書きしない
- セレクタを変更するときは fixtures/ のHTMLに対するテストを先に書く
- 一致が0件のとき、セレクタを広げて通さない。報告して止まる
- @connect * は使わないベストプラクティスのページは、CLAUDE.mdには「コードを読んでも推測できないこと」を書くよう勧めています。メタデータの方針と、セレクタを SELECTORS に集約するという約束は、コードを読んだだけでは分からない種類の規約です。
公開前に確かめること
配布や共有を考えるなら、更新確認を使うために @version を必ず書き、変更のたびに上げます。@name は、末尾にロケールを付けて // @name:de Ein Test のように多言語化もできます。@downloadURL に none を書くと、更新確認が行われなくなります。
また、@sandbox はバージョン4.18以降で使える指定で、スクリプトを注入する環境をTampermonkeyに選ばせます。ページ側の変数に触らずDOMだけを扱うなら、DOM モードで足ります。ページ側のオブジェクトに触る必要があれば unsafeWindow を使い、@grant unsafeWindow を書きます。必要になった時点で、理由と合わせてClaude Codeに足してもらう形が安全です。
次に壊れたときの最短手順
ユーザースクリプトがエラーを出さずに効かなくなったら、次の順に当たります。
- 開発者ツールのコンソールでエラーの有無を見る
@matchが現在のURLに一致しているか(スクリプトのヘッダーと実際のURLを見比べる)を見る- 対象部分の
outerHTMLを保存してfixtures/に追加する - 失敗するテストを先に書かせ、
SELECTORSだけを直させる - 実ページで印や動作を目視する
エラーが出ないまま動かないケースの大半は、2か3のどちらかです。スクリプトが走っていなければ @match を、走っていて何も起きなければセレクタを疑う、と切り分けると、依頼文もそれぞれ短くなります。