🌿 中級⏱ 所要時間の目安:2〜3時間Claude Code / JavaScript・HTML(Manifest V3)

業務効率化のChrome拡張機能をAIエージェントで作る【2026】Manifest V3・APIなし

「今見ているページのリンクをチャットに貼りたい」「問い合わせフォームに毎回同じあいさつ文を打っている」「長い仕様書の中から“締切”だけ探したい」——ブラウザでの小さな繰り返し作業は、自分専用のChrome拡張機能で片づけられます。外部APIもサーバーも不要で、必要なのはHTML・JavaScriptのファイル数個だけ。この記事では、Claude Codeとの実際のやりとりを時系列で再現しながら、Manifest V3(現在のChrome拡張の仕様)で3つの機能を持つ拡張を作り、読み込み・デバッグ・権限の絞り方・社内配布まで解説します。

🔬
調査・編集:AIエージェント研究所 編集部
運営=広告運用・生成AIエージェント導入支援の現役コンサル。このサイト自体も、AIエージェントと一緒に作り・公開・運用しています。

記事中のJavaScriptは構文チェック(node --check)に加え、ハイライト・定型文入力・Markdown変換の処理をテスト用の仮想DOMで動かして確認しています。→ 運営者・編集方針について

公開:2026年9月26日/最終更新:2026年9月26日
この記事の結論(要点まとめ)
  • 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リンクでコピー
定型文を入力
お礼日程調整
🖍 キーワードをハイライト
コピーしました

ポップアップの見た目のイメージ

機能動き業務での使いどころ
① Markdownリンクコピー開いているタブのタイトルとURLを [タイトル](URL) 形式でクリップボードへSlack・Notion・GitHubのIssue、議事録への参照貼り付け
② 定型文の1クリック入力ページの入力欄をクリックしてからボタンを押すと、登録した定型文がカーソル位置に入る問い合わせ返信、社内申請フォーム、CRMのメモ欄
③ キーワードハイライト登録したキーワード(「至急」「締切」「要確認」など)をページ内で黄色くマーク長い仕様書・規約・社内ポータルの確認漏れ防止

定型文とキーワードは設定ページで編集でき、chrome.storage.sync に保存されるので、同じGoogleアカウントでログインした別のPCのChromeにも同期されます(Chromeの同期がオンの場合)。

助手アイ
助手アイ地味だけど毎日使うやつですね!でも拡張機能って、ストアで探してインストールするものだと思ってました。
エージェント博士
エージェント博士ストアの拡張は便利じゃが、「全サイトのデータを読み書き」できる権限を持つものも多い。自分で作れば、必要な機能だけ・必要な権限だけにできる。業務で使うなら、ここが一番の利点なんじゃぞ

Chrome拡張(Manifest V3)の仕組み

Chrome拡張機能は、いくつかの「部品」でできています。AIに頼む前に、部品の名前と役割だけ押さえておくと、AIの説明やエラーの意味が分かるようになります。

宣言:名前・権限・使うファイルブラウザ拡表示中のWebページコンテンツスクリプトcontent.jsページの文字・入力欄を読み書き押すと開くポップアップpopup.html/jsService Worker裏方・background.jsやりとりmanifest.json拡張の設計図
manifest.json(拡張の設計図)が、名前・権限・どのファイルを使うかを宣言します。ポップアップ(popup.html/js)はアイコンを押すと開く小さな画面、コンテンツスクリプト(content.js)は表示中のページの中で動くJS、Service Worker(background.js)はインストール時の初期化などを担う裏方で、コンテンツスクリプトとやりとりします。
部品できることできないこと・注意
ポップアップボタンなどの画面、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エージェントでも同じ流れで進められます。

ターミナルBash
mkdir work-helper-extension
cd work-helper-extension
git init     # 変更履歴を残して、いつでも戻れるようにする
claude       # Claude Code を起動。最初に /init で CLAUDE.md を作っておく

CLAUDE.md(Claude Codeが毎回読む、このフォルダのルール)には、次のような方針を書いておくと、会話をリセットしても同じ前提で作業してくれます。

CLAUDE.md(抜粋)Markdown
# 業務ちょい便利ツール(Chrome拡張)

## 方針
- Manifest V3。ビルドツールやnpmパッケージは使わない(素のJS)
- permissions は最小限。追加するときは理由を説明し、私の確認を取る
- innerHTML は使わない(textContent / createElement を使う)
- 外部サーバーへの通信はしない。外部スクリプトも読み込まない

## 確認方法
- JSを変更したら node --check で構文チェックする
- 動作確認は chrome://extensions で再読み込みしてから行う(手順を私に案内する)

AIとの実際のやりとりを再現

ここからは、編集部がこの拡張を作ったときの流れをもとに、やりとりを時系列で再現します。プロンプトはそのままコピーして使えます。

Step 1:仕様を伝えて、計画を出させる

Plan Mode(Shift+Tabで切り替え。コードを変更する前に計画を提示するモード)にして、最初の依頼を出します。

あなた → Claude Code(プロンプト①)プロンプト
業務用の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)と、権限の案を返してきました。最初の案には、次の権限が含まれていました。

最初の案の manifest.json(抜粋)JSON
"permissions": ["tabs", "activeTab", "scripting", "storage"],
"host_permissions": ["<all_urls>"],

Step 2:権限を「1つずつ理由を説明させて」削る

<all_urls> は「すべてのサイト」へのアクセス権で、インストール時に「すべてのウェブサイト上にある自分のデータの読み取りと変更」という強い警告が出ます。ここで立ち止まって聞き返します。

あなた → Claude Code(プロンプト②)プロンプト
権限を1つずつ、なぜ必要か説明してください。
activeTab で代わりにならないものは残し、代わりになるものは削ってください。
インストール時にどんな警告が出るかも教えてください。
Claude Code の返答(要約)出力
- tabs:タブのタイトルとURLを読むために入れましたが、activeTab があれば、ユーザーが拡張の
  ボタンを押したタブについてはタイトルとURLを取得できます。削除します。
- <all_urls>:任意のサイトでハイライトするために入れましたが、ボタンを押したときだけ動けばよい
  ので、activeTab + scripting で「クリックしたタブにだけ一時的に」注入できます。削除します。
- 社内ポータルでの自動ハイライトは content_scripts の matches を https://*.example.co.jp/* に
  限定します。インストール時の警告はこのドメインについてのみ表示されます。
- storage:設定の保存に必要です(警告は出ません)。
- scripting:ページへの注入に必要です。
助手アイ
助手アイAIが最初から最小にしてくれるわけじゃないんですね。
エージェント博士
エージェント博士AIは「確実に動く」ほうに倒しがちなんじゃ。広い権限は動作確認が楽だからのう。「なぜ必要か説明して」と聞くだけで削れる権限は意外と多い。権限の一覧は、人がレビューすべき一番大事な場所じゃぞ

Step 3:読み込んで試す → 2回目のハイライトでエラー

Claude Codeがファイルを作り終えると、「chrome://extensions を開いて読み込んでください」と手順を案内してくれます(手順は後述)。Markdownコピーと定型文はすぐに動きましたが、ハイライトボタンを2回押すと、2回目は何も起きません。ページのデベロッパーツール(F12)のコンソールには、次のエラーが出ていました。

ページのコンソール出力
Uncaught SyntaxError: Identifier 'keywords' has already been declared
あなた → Claude Code(プロンプト③)プロンプト
ハイライトボタンを2回押すと、ページのコンソールに次のエラーが出て2回目が動きません。
原因を説明してから直してください。何回押しても、ハイライトが二重にならないようにしてください。

Uncaught SyntaxError: Identifier 'keywords' has already been declared
Claude Code の返答(要約)出力
executeScript で同じファイルを注入すると、同じページの同じ「拡張用の世界」で2回実行されます。
content.js の一番外側で const keywords を宣言していたため、2回目は「宣言済み」のエラーになりました。
全体を即時実行関数 (async () => { ... })() で包んで変数をその中に閉じ込め、
実行のたびに前回の <mark> を外してからハイライトし直すように修正しました。

このエラーは、content_scriptsで自動実行されたページで、さらにボタンを押した場合にも起こります。「一度だけ動かす前提」で書かれたコードが、実際の使われ方では何度も実行される——AIが書いたコードで起こりやすい見落としの典型です。

Step 4:一部のフォームで定型文が「消える」

社内のあるWebフォームでは、定型文が入力欄に表示されるのに、送信すると空として扱われる現象がありました。

あなた → Claude Code(プロンプト④)プロンプト
あるフォームで、定型文が入力欄に表示されるのに、送信すると空欄扱いになります。
考えられる原因と対策を教えてください。一般的なフォームでも壊れない方法にしてください。

Claude Codeの説明は「ReactなどのフレームワークでできたフォームはJavaScript側で入力値を管理しており、value を直接書き換えただけでは変更に気づかないことがある。setRangeText() でカーソル位置に挿入し、input イベントを発生させて変更を知らせる方式にする」というものでした。修正後は、そのフォームでも正しく送信できるようになりました(サイトの作りによっては、これでも対応できない場合があります)。

Step 5:コミットして区切る

あなた → Claude Code(プロンプト⑤)プロンプト
JSファイルをすべて node --check で構文チェックしてから、ここまでをGitにコミットしてください。
コミットメッセージには、権限を削った理由と、二重注入の修正内容を書いてください。

動く状態ごとにコミットしておけば、次の機能追加で壊れても、git diff で何が変わったかを確認したり、前の状態に戻したりできます。

完成したコードの解説

完成版の主要ファイルです。フォルダの直下に置くだけで動きます(アイコン画像は省略しています。省略するとChromeが自動でイニシャルのアイコンを表示します)。

manifest.json:拡張の設計図

manifest.jsonJSON
{
  "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:ポップアップの画面と動き

popup.htmlHTML
<!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>
popup.jsJavaScript
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:ページ内のハイライト

content.jsJavaScript
// 何度注入されても大丈夫なように、全体を即時実行関数で包む
(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}件ハイライトしました`);
})();
content.cssCSS
mark.kw-hl { background: #ffe066; color: inherit; padding: 0 2px; border-radius: 3px; }

ページ全体の文字を innerHTML で置換するとページの仕組み(イベントや入力中の内容)を壊すため、TreeWalker で文字(テキストノード)だけを探し、該当部分を <mark> で包んでいます。script・style・入力欄の中は対象外です。キーワードに ( や . などの記号が含まれても誤動作しないよう、正規表現用のエスケープもしています。

background.js:インストール時の初期設定

background.js(Service Worker)JavaScript
// インストール直後に、定型文とキーワードの初期値を入れておく
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:設定ページ

options.htmlHTML
<!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>
options.jsJavaScript
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に読み込んで動かす

ストアを通さず、手元のフォルダをそのまま拡張機能として読み込めます。

  1. アドレスバーに chrome://extensions と入力して開く
  2. 右上の「デベロッパーモード」をオンにする
  3. 「パッケージ化されていない拡張機能を読み込む」を押し、work-helper-extension フォルダを選ぶ
  4. ツールバーのパズルのアイコンから「業務ちょい便利ツール」をピン留めすると使いやすい
コードを変えたら「再読み込み」 ファイルを変更しても、Chromeは自動では反映しません。chrome://extensions の拡張のカードにある再読み込み(↻)ボタンを押し、さらに試すページもリロードします(コンテンツスクリプトはページを開いたときに注入されるため)。「直したはずなのに直らない」の多くはこれが原因です。

試すときは、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の原因特定は格段に速くなります。

Service Workerは「眠る」 Manifest V3のService Workerは、しばらく使われないとChromeによって停止されます。グローバル変数に保存した値は停止とともに消えるので、残したい値は 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 を見る習慣をつけます
エージェント博士
エージェント博士雰囲気でAIに任せきりにする「バイブコーディング」(2025年2月にAndrej Karpathy氏が広めた言葉じゃ)は、試作には楽しい。じゃが業務のブラウザに入れるものは別。仕様を伝え、権限を読み、動作を確かめる——このシリーズが勧めるのは、その地道な進め方じゃよ

社内配布と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:// URLchrome:// や新しいタブのページで試している「注入できないページでは分かりやすいメッセージを出して」
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に貼るだけで、機能①と同じことができます。

ブックマークのURL欄に貼るJavaScript
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で独立するには?未経験から案件を取る6ステップ【2026】自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
記事を読む

次の一歩

姉妹サイトの記事で、次に気になりやすいことを調べられます。