業務効率化のChrome拡張機能をAIエージェントで作る【2026】Manifest V3・APIなし
「今見ているページのリンクをチャットに貼りたい」「問い合わせフォームに毎回同じあいさつ文を打っている」「長い仕様書の中から“締切”だけ探したい」——ブラウザでの小さな繰り返し作業は、自分専用のChrome拡張機能で片づけられます。外部APIもサーバーも不要で、必要なのはHTML・JavaScriptのファイル数個だけ。この記事では、Claude Codeとの実際のやりとりを時系列で再現しながら、Manifest V3(現在のChrome拡張の仕様)で3つの機能を持つ拡張を作り、読み込み・デバッグ・権限の絞り方・社内配布まで解説します。
- Chrome拡張はmanifest.json+ポップアップ(HTML/JS)+コンテンツスクリプト+Service Workerの組み合わせ。外部APIなしで業務の小さな手間を消せる
- AIへの最初の依頼で「Manifest V3で」「権限は最小限に」「innerHTMLを使わない」と伝える。出てきたmanifestの権限は、1つずつ理由を説明させて削る
- activeTab+scriptingなら「クリックしたタブにだけ」一時的にアクセスでき、インストール時の警告も最小。常時動かすのは指定ドメインだけに絞る
- エラーは3か所(ポップアップの検証・Service Worker・ページのコンソール)で見る。場所さえ分かれば、あとはエラー文をAIに貼るだけ
完成イメージ:3つの機能を持つ拡張
今回作るのは「業務ちょい便利ツール」という名前の拡張機能です。ツールバーのアイコン(またはショートカット Alt+Shift+L)でポップアップが開き、次の3つができます。
ポップアップの見た目のイメージ
| 機能 | 動き | 業務での使いどころ |
|---|---|---|
| ① Markdownリンクコピー | 開いているタブのタイトルとURLを [タイトル](URL) 形式でクリップボードへ | Slack・Notion・GitHubのIssue、議事録への参照貼り付け |
| ② 定型文の1クリック入力 | ページの入力欄をクリックしてからボタンを押すと、登録した定型文がカーソル位置に入る | 問い合わせ返信、社内申請フォーム、CRMのメモ欄 |
| ③ キーワードハイライト | 登録したキーワード(「至急」「締切」「要確認」など)をページ内で黄色くマーク | 長い仕様書・規約・社内ポータルの確認漏れ防止 |
定型文とキーワードは設定ページで編集でき、chrome.storage.sync に保存されるので、同じGoogleアカウントでログインした別のPCのChromeにも同期されます(Chromeの同期がオンの場合)。


Chrome拡張(Manifest V3)の仕組み
Chrome拡張機能は、いくつかの「部品」でできています。AIに頼む前に、部品の名前と役割だけ押さえておくと、AIの説明やエラーの意味が分かるようになります。
| 部品 | できること | できないこと・注意 |
|---|---|---|
| ポップアップ | ボタンなどの画面、chrome.* のAPI、クリップボードへの書き込み | 閉じると中の変数は消える。ページのDOMは直接触れない |
| コンテンツスクリプト | 表示中のページのDOM(文字や入力欄)を読み書き | ページ自身のJavaScriptとは別の世界(isolated world)で動く。使えるchrome.* APIは一部だけ |
| Service Worker | イベント(インストール、ショートカット等)への反応 | しばらく使わないと停止する。グローバル変数に状態を持たず chrome.storage に保存する |
| 設定ページ(options) | 定型文・キーワードの編集画面 | 普通のHTMLページとして開く |
Manifest V3では、外部サーバーから取得したJavaScriptを実行することが禁止され、拡張に同梱したコードだけが動きます。安全性を高めるための仕様で、AIに頼むときも「CDNからライブラリを読み込む」ような書き方は避けてもらいます。
準備:Claude Codeと作業フォルダ
必要なのはChromeと、Claude Code(ターミナルで動くAnthropicのコーディングエージェント)だけです。今回はnpmのパッケージもビルドも使わない、素のHTML・JavaScriptで作ります。Claude Codeのインストール方法と料金・プランは公式ドキュメントで確認してください(執筆時点では npm install -g @anthropic-ai/claude-code などの方法が案内されています)。Cursor、Codex、GitHub Copilotのエージェント機能など、ファイルを編集できる他のAIエージェントでも同じ流れで進められます。
mkdir work-helper-extension
cd work-helper-extension
git init # 変更履歴を残して、いつでも戻れるようにする
claude # Claude Code を起動。最初に /init で CLAUDE.md を作っておくCLAUDE.md(Claude Codeが毎回読む、このフォルダのルール)には、次のような方針を書いておくと、会話をリセットしても同じ前提で作業してくれます。
# 業務ちょい便利ツール(Chrome拡張)
## 方針
- Manifest V3。ビルドツールやnpmパッケージは使わない(素のJS)
- permissions は最小限。追加するときは理由を説明し、私の確認を取る
- innerHTML は使わない(textContent / createElement を使う)
- 外部サーバーへの通信はしない。外部スクリプトも読み込まない
## 確認方法
- JSを変更したら node --check で構文チェックする
- 動作確認は chrome://extensions で再読み込みしてから行う(手順を私に案内する)AIとの実際のやりとりを再現
ここからは、編集部がこの拡張を作ったときの流れをもとに、やりとりを時系列で再現します。プロンプトはそのままコピーして使えます。
Step 1:仕様を伝えて、計画を出させる
Plan Mode(Shift+Tabで切り替え。コードを変更する前に計画を提示するモード)にして、最初の依頼を出します。
業務用のChrome拡張機能を Manifest V3 で作りたいです。機能は3つ。
1. ツールバーのボタンからポップアップを開き、今のタブのタイトルとURLを
[タイトル](URL) のMarkdown形式でクリップボードにコピー
2. ポップアップに定型文のボタンを並べ、押すとページの入力欄(カーソル位置)に挿入
3. 登録したキーワードをページ内でハイライト。社内ポータル(https://*.example.co.jp/)では
ページを開いたら自動で、それ以外のサイトではボタンを押したときだけ
定型文とキーワードは設定ページで編集し、chrome.storage.sync に保存。
権限は必要最小限にしてください。ファイル構成と、manifest に書く権限とその理由を
先に見せてください。Claude Codeは、ファイル構成(manifest.json、popup.html/js、content.js/css、background.js、options.html/js)と、権限の案を返してきました。最初の案には、次の権限が含まれていました。
"permissions": ["tabs", "activeTab", "scripting", "storage"],
"host_permissions": ["<all_urls>"],Step 2:権限を「1つずつ理由を説明させて」削る
<all_urls> は「すべてのサイト」へのアクセス権で、インストール時に「すべてのウェブサイト上にある自分のデータの読み取りと変更」という強い警告が出ます。ここで立ち止まって聞き返します。
権限を1つずつ、なぜ必要か説明してください。
activeTab で代わりにならないものは残し、代わりになるものは削ってください。
インストール時にどんな警告が出るかも教えてください。- tabs:タブのタイトルとURLを読むために入れましたが、activeTab があれば、ユーザーが拡張の
ボタンを押したタブについてはタイトルとURLを取得できます。削除します。
- <all_urls>:任意のサイトでハイライトするために入れましたが、ボタンを押したときだけ動けばよい
ので、activeTab + scripting で「クリックしたタブにだけ一時的に」注入できます。削除します。
- 社内ポータルでの自動ハイライトは content_scripts の matches を https://*.example.co.jp/* に
限定します。インストール時の警告はこのドメインについてのみ表示されます。
- storage:設定の保存に必要です(警告は出ません)。
- scripting:ページへの注入に必要です。

Step 3:読み込んで試す → 2回目のハイライトでエラー
Claude Codeがファイルを作り終えると、「chrome://extensions を開いて読み込んでください」と手順を案内してくれます(手順は後述)。Markdownコピーと定型文はすぐに動きましたが、ハイライトボタンを2回押すと、2回目は何も起きません。ページのデベロッパーツール(F12)のコンソールには、次のエラーが出ていました。
Uncaught SyntaxError: Identifier 'keywords' has already been declaredハイライトボタンを2回押すと、ページのコンソールに次のエラーが出て2回目が動きません。
原因を説明してから直してください。何回押しても、ハイライトが二重にならないようにしてください。
Uncaught SyntaxError: Identifier 'keywords' has already been declaredexecuteScript で同じファイルを注入すると、同じページの同じ「拡張用の世界」で2回実行されます。
content.js の一番外側で const keywords を宣言していたため、2回目は「宣言済み」のエラーになりました。
全体を即時実行関数 (async () => { ... })() で包んで変数をその中に閉じ込め、
実行のたびに前回の <mark> を外してからハイライトし直すように修正しました。このエラーは、content_scriptsで自動実行されたページで、さらにボタンを押した場合にも起こります。「一度だけ動かす前提」で書かれたコードが、実際の使われ方では何度も実行される——AIが書いたコードで起こりやすい見落としの典型です。
Step 4:一部のフォームで定型文が「消える」
社内のあるWebフォームでは、定型文が入力欄に表示されるのに、送信すると空として扱われる現象がありました。
あるフォームで、定型文が入力欄に表示されるのに、送信すると空欄扱いになります。
考えられる原因と対策を教えてください。一般的なフォームでも壊れない方法にしてください。Claude Codeの説明は「ReactなどのフレームワークでできたフォームはJavaScript側で入力値を管理しており、value を直接書き換えただけでは変更に気づかないことがある。setRangeText() でカーソル位置に挿入し、input イベントを発生させて変更を知らせる方式にする」というものでした。修正後は、そのフォームでも正しく送信できるようになりました(サイトの作りによっては、これでも対応できない場合があります)。
Step 5:コミットして区切る
JSファイルをすべて node --check で構文チェックしてから、ここまでをGitにコミットしてください。
コミットメッセージには、権限を削った理由と、二重注入の修正内容を書いてください。動く状態ごとにコミットしておけば、次の機能追加で壊れても、git diff で何が変わったかを確認したり、前の状態に戻したりできます。
完成したコードの解説
完成版の主要ファイルです。フォルダの直下に置くだけで動きます(アイコン画像は省略しています。省略するとChromeが自動でイニシャルのアイコンを表示します)。
manifest.json:拡張の設計図
{
"manifest_version": 3,
"name": "業務ちょい便利ツール",
"version": "1.0.0",
"description": "Markdownリンクのコピー、定型文の入力、キーワードのハイライト",
"permissions": ["activeTab", "scripting", "storage"],
"action": {
"default_popup": "popup.html",
"default_title": "業務ちょい便利ツール"
},
"background": {
"service_worker": "background.js"
},
"options_page": "options.html",
"content_scripts": [
{
"matches": ["https://*.example.co.jp/*"],
"js": ["content.js"],
"css": ["content.css"]
}
],
"commands": {
"_execute_action": {
"suggested_key": { "default": "Alt+Shift+L" }
}
}
}"manifest_version": 3:Manifest V3であることの宣言。必須ですpermissions:activeTab(ボタンを押したタブにだけ一時的にアクセス)、scripting(ページへのスクリプト注入)、storage(設定の保存)の3つだけaction.default_popup:ツールバーのボタンを押したときに開く画面content_scripts:matchesに一致するページ(ここでは社内ポータルを想定したexample.co.jp)を開いたとき自動で動くスクリプト。実際に使うときは自社のドメインに書き換えますcommands._execute_action:ポップアップを開くショートカット。chrome://extensions/shortcuts で変更できます
popup.html/popup.js:ポップアップの画面と動き
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<button id="copy-md">📋 このページをMarkdownリンクでコピー</button>
<h2>定型文を入力</h2>
<div id="templates"></div>
<button id="highlight">🖍 キーワードをハイライト</button>
<p id="status" role="status"></p>
<button id="open-options">⚙ 設定(定型文・キーワード)</button>
<script src="popup.js"></script>
</body>
</html>const statusEl = document.getElementById("status");
const show = (msg) => { statusEl.textContent = msg; };
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab;
}
// [タイトル](URL) 形式。タイトル中の [ ] はエスケープして崩れないようにする
function toMarkdownLink(title, url) {
const safeTitle = (title || url).replace(/([\[\]])/g, "\\$1");
return `[${safeTitle}](${url})`;
}
// ① Markdownリンクをコピー
document.getElementById("copy-md").addEventListener("click", async () => {
const tab = await getActiveTab();
await navigator.clipboard.writeText(toMarkdownLink(tab.title, tab.url));
show("コピーしました");
});
// ② 定型文:ページ側で実行される関数(この関数だけがページに注入される)
function insertText(text) {
const el = document.activeElement;
if (el && (el.tagName === "TEXTAREA" ||
(el.tagName === "INPUT" && /^(text|search|email|url|tel)?$/.test(el.type)))) {
el.setRangeText(text, el.selectionStart, el.selectionEnd, "end");
el.dispatchEvent(new Event("input", { bubbles: true })); // 画面側に変更を知らせる
return true;
}
if (el && el.isContentEditable) {
return document.execCommand("insertText", false, text);
}
return false;
}
async function renderTemplates() {
const { templates = [] } = await chrome.storage.sync.get("templates");
const box = document.getElementById("templates");
box.replaceChildren();
for (const t of templates) {
const btn = document.createElement("button");
btn.textContent = t.label; // innerHTML は使わない(設定値にHTMLが混ざっても安全)
btn.addEventListener("click", async () => {
const tab = await getActiveTab();
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: insertText,
args: [t.text]
});
show(result.result ? "入力しました" : "先にページの入力欄をクリックしてください");
});
box.append(btn);
}
}
// ③ ハイライト:開いているページに content.js をその場で注入する
document.getElementById("highlight").addEventListener("click", async () => {
const tab = await getActiveTab();
try {
await chrome.scripting.insertCSS({ target: { tabId: tab.id }, files: ["content.css"] });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["content.js"] });
show("ハイライトしました");
} catch (e) {
show("このページでは使えません(chrome:// やウェブストアなど)");
console.error(e);
}
});
// 設定ページを開く(manifest の options_page)
document.getElementById("open-options").addEventListener("click", () => {
chrome.runtime.openOptionsPage();
});
renderTemplates();ポイントは3つです。
- タイトル内の
[ ]をエスケープ:「[PR] 新機能」のようなタイトルでもMarkdownが崩れないように、\[PR\]に変換しています executeScriptのfuncに渡した関数だけがページで実行される:insertTextはポップアップの中ではなく、ページの中で動きます。そのため、関数の外の変数(statusElなど)は使えません。値はargsで渡します- ボタンの文字は
textContentで設定:設定値に<img onerror=...>のような文字列が入っても、HTMLとして解釈されず安全です(innerHTMLを使わない理由)
content.js/content.css:ページ内のハイライト
// 何度注入されても大丈夫なように、全体を即時実行関数で包む
(async () => {
const { keywords = [] } = await chrome.storage.sync.get("keywords");
const words = keywords.map((w) => w.trim()).filter(Boolean);
// 前回のハイライトを外す(二重にならないように)
for (const mark of document.querySelectorAll("mark.kw-hl")) {
mark.replaceWith(document.createTextNode(mark.textContent));
}
document.body.normalize();
if (words.length === 0) return;
const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
const pattern = new RegExp(`(${escaped.join("|")})`, "g");
// テキストノードだけを集める(script/style/入力欄の中は触らない)
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (node.parentElement.closest("script, style, textarea, input, [contenteditable]")) {
return NodeFilter.FILTER_REJECT;
}
pattern.lastIndex = 0;
return pattern.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
const targets = [];
while (walker.nextNode()) targets.push(walker.currentNode);
let count = 0;
for (const node of targets) {
const frag = document.createDocumentFragment();
for (const part of node.nodeValue.split(pattern)) {
if (!part) continue;
if (words.includes(part)) {
const mark = document.createElement("mark");
mark.className = "kw-hl";
mark.textContent = part;
frag.append(mark);
count++;
} else {
frag.append(document.createTextNode(part));
}
}
node.replaceWith(frag);
}
console.log(`[業務ちょい便利ツール] ${count}件ハイライトしました`);
})();mark.kw-hl { background: #ffe066; color: inherit; padding: 0 2px; border-radius: 3px; }ページ全体の文字を innerHTML で置換するとページの仕組み(イベントや入力中の内容)を壊すため、TreeWalker で文字(テキストノード)だけを探し、該当部分を <mark> で包んでいます。script・style・入力欄の中は対象外です。キーワードに ( や . などの記号が含まれても誤動作しないよう、正規表現用のエスケープもしています。
background.js:インストール時の初期設定
// インストール直後に、定型文とキーワードの初期値を入れておく
const DEFAULTS = {
templates: [
{ label: "お礼", text: "お世話になっております。ご連絡ありがとうございます。" },
{ label: "日程調整", text: "下記の日程でご都合のよい時間をお知らせください。" }
],
keywords: ["至急", "締切", "要確認"]
};
chrome.runtime.onInstalled.addListener(async ({ reason }) => {
if (reason !== "install") return; // 更新時はユーザーの設定を上書きしない
await chrome.storage.sync.set(DEFAULTS);
console.log("初期設定を保存しました", DEFAULTS);
});reason !== "install" で、拡張の更新時にはユーザーが編集した設定を上書きしないようにしています。AIに初期値の処理を書かせると、この条件が抜けて「更新するたびに設定が初期化される」バグになりがちなので、確認ポイントです。
options.html/options.js:設定ページ
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>設定</title></head>
<body>
<h1>設定</h1>
<label>定型文(1行に「ボタン名|本文」)<br>
<textarea id="templates" rows="8" cols="60"></textarea></label>
<label>ハイライトするキーワード(1行に1つ)<br>
<textarea id="keywords" rows="5" cols="60"></textarea></label>
<p><button id="save">保存</button> <span id="msg"></span></p>
<script src="options.js"></script>
</body>
</html>const $ = (id) => document.getElementById(id);
async function load() {
const { templates = [], keywords = [] } = await chrome.storage.sync.get(["templates", "keywords"]);
$("templates").value = templates.map((t) => `${t.label}|${t.text}`).join("\n");
$("keywords").value = keywords.join("\n");
}
$("save").addEventListener("click", async () => {
const templates = $("templates").value.split("\n")
.map((line) => line.split("|"))
.filter(([label, ...rest]) => label.trim() && rest.length)
.map(([label, ...rest]) => ({ label: label.trim(), text: rest.join("|").trim() }));
const keywords = $("keywords").value.split("\n").map((s) => s.trim()).filter(Boolean);
await chrome.storage.sync.set({ templates, keywords });
$("msg").textContent = "保存しました";
});
load();ポップアップの見た目は popup.css に書いています(幅280px、ボタンを縦に並べるだけの簡単なもの)。見た目を整えたいときは、理想の画面のスクリーンショットを貼って「この見た目に近づけて」と頼むのも有効です。
Chromeに読み込んで動かす
ストアを通さず、手元のフォルダをそのまま拡張機能として読み込めます。
- アドレスバーに
chrome://extensionsと入力して開く - 右上の「デベロッパーモード」をオンにする
- 「パッケージ化されていない拡張機能を読み込む」を押し、
work-helper-extensionフォルダを選ぶ - ツールバーのパズルのアイコンから「業務ちょい便利ツール」をピン留めすると使いやすい
試すときは、chrome:// で始まるページや新しいタブのページ、Chromeウェブストアのページでは動きません(Chromeの仕様で拡張機能からの注入が禁止されています)。普通のWebサイトで試しましょう。
エラーの見方(3つの検証場所)
Chrome拡張のデバッグで一番迷うのは、「エラーがどこに出るか」です。部品ごとにコンソールが分かれています。
| どこで動くコード? | エラー・ログの見方 |
|---|---|
| ポップアップ(popup.js) | ポップアップを開いた状態で、ポップアップの上で右クリック →「検証」。専用のデベロッパーツールが開く |
| Service Worker(background.js) | chrome://extensions の拡張のカードにある「Service Worker」のリンク(「ビューを検証」の欄)をクリック。停止中は表示が変わることがある |
| コンテンツスクリプト・注入した関数(content.js、insertText) | そのページでF12 → Console。上部のコンテキスト選択(「top」と書かれたプルダウン)で拡張機能を選ぶと、拡張側の変数も確認できる |
| manifestの書き間違いなど | chrome://extensions のカードに表示される「エラー」ボタン、または読み込み時のエラーダイアログ |
エラーの場所が分かったら、「どこに出たか」+「エラー文の全文」をAIに貼ります。「ポップアップの検証ツールに次のエラーが出ました」と場所を添えるだけで、AIの原因特定は格段に速くなります。
chrome.storage に保存します。「しばらくすると動かなくなる」「変数が初期値に戻る」という症状は、たいていこれが原因です。
権限を最小にする理由とセキュリティ
拡張機能は、権限次第でページに表示されている情報をすべて読み、書き換えられます。社内システムの顧客情報、メール、管理画面も例外ではありません。だからこそ権限は最小にします。
| 権限・設定 | 何ができるようになるか | 今回の判断 |
|---|---|---|
activeTab | ユーザーが拡張を操作したタブにだけ、一時的にアクセス | 採用。常時アクセスが不要な機能はこれで足りる |
scripting | アクセス権のあるページにスクリプト・CSSを注入 | 採用。activeTabと組み合わせて使う |
storage | 拡張専用の保存領域 | 採用。ただしパスワードやトークンは保存しない |
content_scripts.matches | 一致するサイトで常に自動実行(そのサイトへのアクセス権が付く) | 社内ポータルのドメインだけに限定 |
tabs | すべてのタブのURLやタイトルを読める | 不採用。activeTabで足りる |
host_permissions: <all_urls> | すべてのサイトを常時読み書き | 不採用。業務用拡張では最も慎重に扱うべき権限 |
権限を絞る理由は「自分が悪用しないから大丈夫」ではありません。拡張機能自体に不具合や脆弱性があった場合、あるいは配布経路が乗っ取られた場合に、被害が及ぶ範囲を小さくするためです。ほかにも、次の点を守ります。
- 外部にデータを送らない:今回の拡張は通信を一切しません。将来AIの要約機能などを足す場合も、どのデータをどこに送るかを明示的に設計します
innerHTMLを使わない:ページや設定の文字列をHTMLとして解釈すると、スクリプトを埋め込まれる(XSS)おそれがありますchrome.storage.syncに秘密情報を入れない:同期される設定データであり、パスワード保管庫ではありません- 権限を追加するときは人が確認:CLAUDE.mdに「permissionsを追加するときは理由を説明し確認を取る」と書き、コミット前に
git diff manifest.jsonを見る習慣をつけます

社内配布とChromeウェブストア公開
自分だけで使うなら「パッケージ化されていない拡張機能を読み込む」で十分です。チームに配るときは、規模と管理のしやすさで方法を選びます。
| 方法 | 向いている規模 | 注意点 |
|---|---|---|
| フォルダを共有し、各自が「パッケージ化されていない拡張機能」として読み込む | 数人の試験運用 | 各自がデベロッパーモードをオンにする必要がある。更新は各自でファイルを差し替えて再読み込み。会社のポリシーでデベロッパーモードが禁止されている場合もある |
| Chromeウェブストアに非公開(限定公開)で登録し、URLを知る人だけがインストール | 部署〜社内全体 | 審査がある。更新が自動で配信される |
| Google Workspace/Chrome Enterpriseの管理コンソールから、組織内に配布・強制インストール | 会社として管理したい場合 | 情報システム部門の管理下で行う。組織限定での公開や、許可した拡張だけを使える設定も可能 |
いずれの場合も、ソースコードはGitで管理し、誰がいつ何を変えたか追える状態にしておくことが、社内ツールとして信頼される条件です。
Chromeウェブストアに一般公開する場合は、デベロッパー登録(登録料が必要)、説明文・スクリーンショット・プライバシーに関する申告の準備、審査があります。要求される権限が広いほど審査で理由を問われやすいので、ここでも最小権限が効いてきます。手続きや料金は変わることがあるため、執筆時点(2026年9月)の情報として、公開前にChrome Web Store の公式ドキュメントを確認してください。
つまずきポイントとAIへの頼み方
| 症状 | よくある原因 | AIへの頼み方の例 |
|---|---|---|
| 読み込み時に「マニフェストファイルが見つからない/読み取れない」という趣旨のエラー | 選んだフォルダが1階層ずれている、JSONの書き間違い(末尾のカンマなど) | 「manifest.jsonをJSONとして検証して、フォルダ構成も確認して」 |
| 直したのに動きが変わらない | 拡張の再読み込み・ページのリロードをしていない | 「変更を反映する手順を毎回最後に案内して」とCLAUDE.mdに書く |
Cannot access a chrome:// URL | chrome:// や新しいタブのページで試している | 「注入できないページでは分かりやすいメッセージを出して」 |
Identifier '...' has already been declared | 同じスクリプトを2回注入した | 「何回注入されても安全な書き方にして」 |
| 定型文が入らない | 入力欄をクリックしてからポップアップを開いていない/iframe内の入力欄 | 「入力欄が見つからないときの原因を表示して。iframeの中にも対応できるか検討して」 |
| しばらくすると動かない・値が消える | Service Workerの停止で変数がリセット | 「状態をグローバル変数でなく chrome.storage に持つように直して」 |
AIがManifest V2の書き方(browser_action、background page)を混ぜる | 古い情報に引きずられている | 「Manifest V3の書き方だけを使って。V2の書き方があれば指摘して」 |
会話が長くなって話がかみ合わなくなったら /clear でリセットし、CLAUDE.md とファイルを前提に仕切り直します。作業を止めたいときは Esc で中断できます。
発展:ブックマークレットとの比較・次のアイデア
「Markdownコピー」だけならブックマークレットでもできる
ブックマークレットは、ブックマークのURL欄に javascript: で始まるコードを登録し、クリックで実行する仕組みです。インストール不要で、次の1行を新しいブックマークのURLに貼るだけで、機能①と同じことができます。
javascript:(()=>{const t=document.title.replace(/([\[\]])/g,"\\$1");navigator.clipboard.writeText(`[${t}](${location.href})`).then(()=>alert("コピーしました"),()=>alert("コピーできませんでした"));})();| 観点 | ブックマークレット | Chrome拡張機能 |
|---|---|---|
| 導入の手軽さ | ◎ ブックマークに貼るだけ | ○ 読み込み・配布の手順が必要 |
| 設定の保存 | △ 基本的に持てない(コードに書き込む) | ◎ chrome.storageで保存・同期 |
| ページを開いたら自動実行 | × できない(毎回クリック) | ◎ content_scriptsで可能 |
| 動かないページ | CSP(コンテンツセキュリティポリシー)が厳しいサイトでは動かないことがある | chrome:// やストアのページ以外なら概ね動く |
| 画面(UI) | alert程度 | ポップアップ・設定ページ・ショートカット |
| 管理・配布 | コードをコピーして配るだけ。更新の配布は難しい | ストアや管理コンソールで更新を配信できる |
「1つの操作を、たまに、自分だけで」ならブックマークレット、「複数の機能を、毎日、チームで」なら拡張機能、と考えるとよいでしょう。
次に作れるもの(アイデア)
- 右クリックメニュー:選択した文字を定型の検索URL(社内Wikiなど)で開く(
contextMenus権限を追加) - 表のコピー:ページ内の表をTSV(タブ区切り)でコピーし、Excelやスプレッドシートに貼れるようにする
- 入力チェック:社内フォームで、必須項目の入力漏れや全角・半角の混在を送信前に警告する
- サイドパネル:よく使う社内リンク集やメモをブラウザの横に常時表示する
ページの要約など「AIそのもの」を拡張に組み込むと、外部APIとの通信や認証情報の管理が必要になり、一段難しくなります。まずはAPIなしで完結する便利ツールで、作り方と権限の考え方に慣れるのがおすすめです。
事例:自社と他社の取り組み
「人が見落としやすいこと」を機械に見張らせる
このサイト(約57ページの静的サイト)は、Claude Codeとの共同作業で作成・運用しています。その中でAIと一緒に作った keihyo-check.js は、広告表記の有無や位置に加え、target="_blank" のリンクに rel="noopener" が付いているかも機械的にチェックします。新しいタブで開いたページから元のページを操作されないための、小さなセキュリティ対策です。
今回の拡張で「権限の一覧は人がレビューする」「innerHTMLを使わない」とルール化したのも同じ発想です。AIは大量のコードを速く書けるが、安全の観点は人が決め、機械で見張る。当社が導入支援の現場で勧めているのも、この役割分担です。
サイト運用全体の仕組みは【自社事例】AIエージェントでメディアサイトを運用する仕組みで紹介しています。
他社・業界の動き
- Anthropicは、社内でClaude Codeを広く使っており、Claude Code自体のコードの多くもClaude Codeで書いていると公表しています。
- AnthropicのClaude 4発表(2025年5月)では、楽天がClaude Opus 4を約7時間にわたって自律的に動かし、オープンソースのリファクタリングを行った事例が紹介されました。AIエージェントに任せられる作業の長さは年々伸びています。
- エンジニアではない職種の人が、AIエージェントを使って自分の業務用の小さなツールを作る動きが広がっています。ブラウザ拡張は、その中でも「毎日使う場所」を改善できる題材です。
AIエージェントで作れるもの全体は「AIエージェントで開発してみよう」レベル別カタログへ。HTML・JavaScript・ブラウザの仕組みを基礎から学びたい方はIT基礎ロードマップ(特に第2章 フロントエンド入門)がおすすめです。
よくある質問
JavaScriptが書けなくてもChrome拡張は作れますか?
作れます。コードはAIエージェントが書きます。ただし、拡張機能の読み込みと再読み込み、エラーの確認場所(ポップアップ・Service Worker・ページのコンソール)の3つは自分で操作できるようにしておきましょう。エラーメッセージをそのままAIに貼り付けられれば、修正はAIが進めてくれます。
Microsoft Edgeでも使えますか?
EdgeはChromeと同じChromiumを土台にしているため、この記事の拡張機能は基本的にそのまま動きます。edge://extensions で「開発者モード」を有効にし、「展開して読み込み」からフォルダを選びます。ブラウザごとに細かな違いはあるため、配布前にそれぞれで動作確認してください。
社内システムのページで使っても安全ですか?
拡張機能はページの内容を読み書きできるため、権限を必要最小限にすることが前提です。この記事の構成では、クリックしたタブにだけ一時的にアクセスするactiveTabを使い、自動で動くのは指定したドメインだけに限定しています。パスワードやトークンを拡張機能の保存領域に入れない、外部にデータを送信しない、社内の情報システム部門のルールを確認する、の3点も守りましょう。
昔の作り方(Manifest V2)の記事を参考にしてもいいですか?
おすすめしません。ChromeではManifest V2のサポート終了が段階的に進められ、現在はManifest V3が前提です。V2の記事にあるbackground pageやbrowserActionなどの書き方はV3では使えません。AIエージェントに頼むときも「Manifest V3で」と明記すると、古い書き方が混ざるのを防げます。
まとめ|小さな拡張で、毎日のブラウザ作業を少しずつ軽く
Chrome拡張は、外部APIもサーバーも使わずに作れる「毎日使う業務ツール」です。AIエージェントと作るときのポイントは、①「Manifest V3・最小権限・innerHTML禁止」を最初に伝える → ②権限は1つずつ理由を説明させて削る → ③エラーは3つの検証場所から全文を貼る → ④動くたびにGitでコミットの4つ。特に権限の一覧は、AIに任せきりにせず人が読む場所だと覚えておいてください。
次は、データを保存できる本格的なWebアプリづくり(Next.js+SQLite、🌿中級)に進みましょう。1つ前のExcel・CSV集計の自動化(🌱初級)も、業務ツールづくりの入門としておすすめです。
作れるようになったら、スキルを収入につなげる
AIエージェントで「作れる」ことは、それ自体が仕事になります。案件獲得や転職、作業環境づくりに役立つ記事をまとめました。
- 仕事で使えるAIツール比較【2026】:コードを書かずに使えるAIツールを目的別に
- AIが快適に動くノートパソコンの選び方&おすすめ3選【2026】:開発・AIエージェントを動かすならメモリ16GB以上が目安