Claude Media
Claude Codeでelectron-updaterを実装し、ローカル更新サーバーで検証する

Claude Codeでelectron-updaterを実装し、ローカル更新サーバーで検証する

electron-updaterの自動更新をClaude Codeで実装し、latest.ymlのハッシュ・段階配信・署名をローカルの更新フィードで確かめる手順。Stop hookで検証を自動化します。

electron-updaterは、Electronアプリに自動更新を足すためのパッケージです。更新の配信元に置くのは、ビルド成果物と latest.yml というメタデータのファイル。クライアントは latest.yml を読んで新しい版があるかを判断し、成果物のハッシュを突き合わせてから入れ替えます。

Claude Codeに実装を任せるときの難所は、コードを書くことではありません。更新が本当に届くかを、実機のインストーラーなしで確かめる手段をどう用意するかです。この記事では、ローカルのHTTPサーバーを更新フィードに見立て、latest.yml と成果物の整合を検証するスクリプトを作ります。それをStop hookにつなぎ、Claudeが「できました」と言う前に必ず通る関門にします。

先に決める: どのバージョンのドキュメントを読むか

electron-builderの公式ドキュメントは、読み始める前に版を確かめる必要があります。自動更新のページには「next (v27)」の表示があり、リリース前の内容だと明記されています。安定版はv26のドキュメントです。

npmのdist-tagでは、electron-updaterの latest が6.8.11、next が7.0.0-alpha.12でした。electron-builderは latest が26.18.0、next が27.0.0-alpha.13です。

機能載っているドキュメント備考
latest.yml の生成・汎用サーバー配信載っているドキュメントnext (v27)のページに記載備考仕組み自体は以前からある
stagingPercentage による段階配信載っているドキュメント同上備考latest.yml を手で編集する方式
チャンネル(alpha / beta / latest)載っているドキュメント同上備考generateUpdatesFilesForAllChannels
署名付きマニフェスト載っているドキュメントSigned Update Manifestsページ備考v27の機能として説明されている
autoInstallEvent: "onNextLaunch"載っているドキュメント同ページ内備考electron-updater 7.0以降と記載

署名付きマニフェストと autoInstallEvent は、v27の新機能として書かれています。安定版のv26に同じ挙動を期待しないでください。依頼文には、使う版を先に書いておきます。

線引きをもう少し具体的にすると、次のとおりです。v26で使えるのは、generic プロバイダーでの配信、latest.yml の sha512 と size の照合、stagingPercentage の手編集、チャンネルの切り替えです。この記事の verify-feed.mjs とStop hookは、ここまでで完結します。一方、署名付きマニフェスト、autoInstallEvent: "onNextLaunch"、disableWebInstaller の既定値変更は、v27のドキュメントに書かれた挙動です。v26のプロジェクトで「署名の検証で守られている」と前提を置くと、実際には何も検証されていない状態になります。

依頼文の例を挙げます。「electron-builder 26の範囲で実装する。署名付きマニフェストと autoInstallEvent は使わない。v27へ上げるときは別の作業として扱う」。こう書いておけば、Claudeがv27の節を読んで気を利かせ、存在しない設定キーを electron-builder.yml に足す事態を避けられます。

Claude Codeに渡す前提をCLAUDE.mdに置く

最初にCLAUDE.mdで前提を固定します。Claudeは、electron-updaterの古い記事にある setFeedURL を呼ぶコードを書きがちです。公式ドキュメントは「setFeedURL は呼ばない」としています。electron-builderがビルド時に app-update.yml を resources に作るからです。

## 自動更新(electron-updater)
- バージョンは electron-builder 26 / electron-updater 6 系。v27 の機能は使わない
- `autoUpdater` は `electron-updater` から import する(electron 組み込みは使わない)
- `setFeedURL` は呼ばない。配信先は electron-builder の `publish` 設定が決める
- 更新フィードの確認は `node scripts/verify-feed.mjs <URL>` で行う
- 更新に関わる変更を終えたら、必ずローカルの `feed/` を作り直して上のコマンドを通す

Electronアプリ自体の土台は、Claude CodeでElectronアプリを開発する手順で作った構成を前提にします。

実装を3段階に分けて依頼する

一度に頼むと、設定とコードとテストが混ざって切り分けられません。次の順に、段階ごとに確認を挟みます。

手順

electron-updaterの実装を頼む順序

  1. 1

    publish設定とフィードの生成

    汎用サーバー向けの publish 設定を足し、ビルドで latest.yml が出ることを確かめます。

  2. 2

    メインプロセスの更新処理

    autoUpdater のイベントを受けてログを出すコードを書きます。

  3. 3

    ローカルフィードの検証

    feed/ を配信し、latest.yml と成果物の整合を検証スクリプトで通します。

publish設定: 汎用サーバーを指す

配信先を自前のHTTPサーバーにするには、generic プロバイダーを指定します。公式の説明では、汎用サーバーに限り、成果物とメタデータを自分でアップロードする必要があります。ローカル検証と相性がよい理由は、ここにあります。

# electron-builder.yml(抜粋)
publish:
  provider: generic
  url: http://127.0.0.1:8123

ビルド後の出力ディレクトリに、成果物と並んで latest.yml が出ているかを確認させます。macOSは latest-mac.yml、Linuxは latest-linux.yml です。macOSでは、latest-mac.yml を作るために zip ターゲットが要ります。既定のターゲットは dmg と zip なので、明示しなければ問題になりません。

メインプロセス: イベントにログを付ける

デバッグはロガーを設定するだけで足ります。公式ドキュメントは electron-log を勧めています。次のような形が出発点です(公式の例に沿った構成)。

import { app } from "electron"
import { autoUpdater } from "electron-updater"
import log from "electron-log"
 
export function setupAutoUpdate(): void {
  autoUpdater.logger = log
  log.transports.file.level = "info"
 
  autoUpdater.on("update-available", (info) => log.info("available", info.version))
  autoUpdater.on("download-progress", (p) => log.info("progress", Math.round(p.percent)))
  autoUpdater.on("update-downloaded", (info) => log.info("downloaded", info.version))
  autoUpdater.on("error", (err) => log.error("updater error", err))
 
  if (app.isPackaged) {
    autoUpdater.checkForUpdatesAndNotify()
  }
}

autoUpdater.on("error", ...) は必ず頼みます。署名の検証失敗もハッシュの不一致も、このイベントで届くからです。

コード例で受けているのは4つだけです。autoUpdaterは、ほかにも次のイベントを出します。

  • checking-for-update: 更新の確認が始まったとき。ログにこれが出ていなければ、checkForUpdatesAndNotify() がそもそも呼ばれていません。app.isPackaged の条件で弾かれている開発中は、ここで気づけます
  • update-not-available: 確認は済み、新しい版がなかったとき。フィードの version が現在の版以下だと、ここに来ます。checking-for-update の次にこれが出るなら、通信は成功していて、版の比較で止まっています
  • update-cancelled: ユーザーの操作などでダウンロードが中断されたとき。error とは別に届きます
  • appimage-filename-updated: AppImageの更新に成功し、ファイルが最終的な場所へ移された後に出ます。Linuxで、AppImageとして配布している場合に限ります

checking-for-update から update-available、download-progress、update-downloaded までの順にログが並べば、フィードの読み取りからダウンロードまでが通ったと判断できます。途中で止まった箇所が、切り分けの起点です。Claudeに追加を頼むときは、これらを log.info で1行ずつ出すようにすれば足ります。

パッケージ化していない開発中に更新のUIを試すには、プロジェクトのルートに dev-app-update.yml を置きます。中身は publish 設定をYAMLで書いたものです。そのうえで、autoUpdater.forceDevUpdateConfig = true を設定します。これも公式ドキュメントの記載です。

更新フィードを検証するスクリプトを作る

latest.yml の sha512 と size は、成果物と一致していなければなりません。electron-updaterはダウンロードした成果物のsha512を必ず照合するので、フィード側がずれた時点で更新は失敗します。この照合を、Electronを起動せずに手元で再現するのが検証スクリプトの役目です。

次の verify-feed.mjs は、依存パッケージなしで動きます。latest.yml を取得し、files[] の各エントリを実際にダウンロードして、サイズとsha512を比べます。stagingPercentage があれば、0〜100の範囲かも見ます。

// scripts/verify-feed.mjs
import { createHash } from "node:crypto";
 
const base = process.argv[2];
const manifestName = process.argv[3] ?? "latest.yml";
const errors = [];
 
const res = await fetch(new URL(manifestName, base + "/"));
if (!res.ok) { console.error(`${manifestName}: HTTP ${res.status}`); process.exit(2); }
const text = await res.text();
 
const version = text.match(/^version:\s*(.+)$/m)?.[1]?.trim();
const staging = text.match(/^stagingPercentage:\s*(\d+)/m)?.[1];
const files = [...text.matchAll(/-\s+url:\s*(.+)\n\s+sha512:\s*(\S+)\n\s+size:\s*(\d+)/g)]
  .map((m) => ({ url: m[1].trim(), sha512: m[2], size: Number(m[3]) }));
 
if (!version) errors.push("version がありません");
if (files.length === 0) errors.push("files[] がありません");
if (staging !== undefined && !(Number(staging) >= 0 && Number(staging) <= 100))
  errors.push(`stagingPercentage が範囲外: ${staging}`);
 
for (const f of files) {
  const r = await fetch(new URL(encodeURI(f.url), base + "/"));
  if (!r.ok) { errors.push(`${f.url}: HTTP ${r.status}`); continue; }
  const buf = Buffer.from(await r.arrayBuffer());
  const actual = createHash("sha512").update(buf).digest("base64");
  if (buf.length !== f.size) errors.push(`${f.url}: size ${buf.length} != ${f.size}`);
  if (actual !== f.sha512) errors.push(`${f.url}: sha512 不一致`);
}
 
if (errors.length) { console.error(errors.join("\n")); process.exit(2); }
console.log(`OK version=${version} files=${files.length} staging=${staging ?? "なし(全員)"}`);

macOSとLinuxのフィードは、ファイル名が違います。第2引数にマニフェスト名を渡すと、同じスクリプトでそのまま検証できます。

node scripts/verify-feed.mjs http://127.0.0.1:8123 latest-mac.yml
node scripts/verify-feed.mjs http://127.0.0.1:8123 latest-linux.yml

複数のOS向けに配信するなら、Stop hookのスクリプトにもこの2行を足しておきます。feed/ にあるマニフェストのうち、1つだけ壊れていても見逃さないためです。

簡易パーサーなので、読めるのは files: の下に url / sha512 / size が並ぶ標準的な形だけです。実際の latest.yml で空振りしたら、YAMLライブラリに置き換えます。

手元での実行結果

Node.js 20で、64KBの乱数ファイルを成果物に見立てたダミーのフィードを作り、ローカルのHTTPサーバーで配信して流しました。electron-builderが出力した本物の latest.yml ではなく、同じ項目を持つ手書きのものです。

node scripts/verify-feed.mjs http://127.0.0.1:8123
# OK version=1.1.0 files=1 staging=10
 
# 成果物の1バイトを書き換えたあと
node scripts/verify-feed.mjs http://127.0.0.1:8123
# TestApp Setup 1.1.0.exe: sha512 不一致   (終了コード 2)
 
# latest.yml を stagingPercentage: 150 に書き換えたあと
node scripts/verify-feed.mjs http://127.0.0.1:8123
# stagingPercentage が範囲外: 150
# TestApp Setup 1.1.0.exe: sha512 不一致   (終了コード 2)

このスクリプトが保証するのは、フィードの整合だけです。署名の検証、インストーラーの起動、OSごとの入れ替えは確認できません。そこは、パッケージ化した実アプリで試す領域です。

Stop hookで検証を毎回通す

検証コマンドをCLAUDE.mdに書いても、Claudeが飛ばす場合があります。確実に通すには、Stop hookにします。Stopは「Claudeが応答を終えるとき」に発火し、終了コード2を返すとClaudeは止まらず作業を続けます。公式の一覧でも、Stopの終了コード2は「Claudeが止まるのを防ぎ、会話を続けさせる」と説明されています。

#!/bin/bash
# .claude/hooks/verify-update-feed.sh
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
[ -f feed/latest.yml ] || exit 0          # フィードが無い間は何もしない
 
python3 -m http.server 8123 --directory feed --bind 127.0.0.1 >/dev/null 2>&1 &
SERVER=$!
trap 'kill $SERVER 2>/dev/null' EXIT
sleep 1
 
if ! node scripts/verify-feed.mjs http://127.0.0.1:8123 >&2; then
  echo "更新フィードの検証に失敗しました。latest.yml と成果物を直してください。" >&2
  exit 2
fi

冒頭の stop_hook_active の確認は省けません。Claude Codeは、Stop hookが続けてブロックを8回繰り返し、その間にClaudeのツール呼び出しがなければ、フックを上書きして止めます。確認なしで書くと、直せない不一致のたびに8回空回りします。8回で足りない場合の上限は、環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で上げられます。

設定は .claude/settings.json に書きます。Stopにはmatcherが使えないので、hooks だけを書きます。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-update-feed.sh"
          }
        ]
      }
    ]
  }
}

このフックを前述のダミーのフィードで流すと、整合しているときは終了コード0、成果物を壊すと終了コード2とメッセージが出ました。stop_hook_active が true の入力では、フィードが壊れていても0で抜けます。

フックが失敗したときの扱いを詳しく決めたい場合は、Claude Code hooksのonFailureで失敗時に操作を止める設定が参考になります。

段階配信とチャンネルを検証に載せる

stagingPercentageは手で編集する

段階配信は、latest.yml に stagingPercentage: 10 を書くと有効になります。公式ドキュメントによると、更新はユーザーの10%に配られます。配信を巻き戻したいときの注意が、ひとつあります。壊れた版を取り下げるには、壊れた版より大きいバージョン番号を出さなければなりません。同じ番号で出し直しても、すでに壊れた版を入れた人はそのままだからです。

割合を変える作業は、成果物の sha512 と size には影響しません。Claudeには「stagingPercentage を変えるときは、成果物に触らない」と指示しておきます。

チャンネル: betaはlatestも受け取る

package.json の version に -beta を付けると、betaチャンネルの成果物になります。generateUpdatesFilesForAllChannels を true にしておくと、各チャンネルの更新ファイルが生成されます。

クライアント側は autoUpdater.channel = "beta" で受け取る系統を選びます。betaを選んだユーザーはbetaとlatestの両方を、alphaを選んだユーザーはalpha・beta・latestを受け取ります。公式ドキュメントには、GitHub Releasesを配信先にした場合は version のタグが効かず、channel を明示する必要がある、という注意もあります。

署名付きマニフェスト: v27で変わること

v27の説明では、latest.yml 自体にEd25519の署名が付きます。鍵ペアは次のコマンドで作ります。

npx electron-builder create-update-key

秘密鍵は、CIシークレットにbase64で入れて ELECTRON_BUILDER_UPDATE_SIGN_KEY で渡す形が基本です。公開鍵はアプリの app-update.yml に自動で埋め込まれます。公開鍵を持つ版のアプリは、署名のないマニフェストを取り込みません。ERR_UPDATER_MANIFEST_NOT_SIGNED(署名なし)か ERR_UPDATER_MANIFEST_SIGNATURE_INVALID(改ざん・信頼しない鍵)で、ダウンロード前に止まります。

Claude Codeに作業を頼むときは、次の点を前提にしてください。

  • 秘密鍵は、リポジトリにもClaudeの読める範囲にも置かない。.gitignore と権限設定で守る
  • 署名の対象には stagingPercentage が入っている。したがって署名後に latest.yml の段階配信の割合を手で書き換えると、署名の検証に通らなくなります
  • 署名後に変えても検証に影響しないのは、releaseName、releaseNotes、releaseDate の3つ

署名後に割合だけを変える手順は、ドキュメントに記載がありません。段階配信を使うなら、割合を変えるたびに再署名か再公開が要るものとして、運用を組む必要があります。

署名の検証は、先ほどのスクリプトでは再現しません。electron-builderには electron-builder verify コマンドがあり、各マニフェストを、パッケージ済みアプリが埋め込む信頼鍵と照合します。リリース前の検証には、こちらを使います。

更新のセキュリティ設定: v27で変わる2つの既定

署名付きマニフェストとは別に、v27ではAppUpdaterの設定が2つ、変更または追加されています。

  • disableWebInstaller: 既定が true になりました。NSISのWebインストーラーは、インストール時にマニフェストが示すURLから本体を取得します。この取得物は署名の検証を通らない場合があるためです。true のままだと、Webインストーラーの更新は ERR_UPDATER_WEB_INSTALLER_DISABLED で拒否されます。nsis-web を意図して配布するときだけ、アプリの準備が整う前に updater.disableWebInstaller = false を設定します。v27で作ったnsis-webのインストーラーは目印のファイルを持ち、NsisUpdaterが自動で false にします。なお、nsis-web のアプリでは updateManifest: false にしないよう書かれています。マニフェストがWebパッケージのパス・SHA-512・サイズを保証しているからです
  • allowUnverifiedLinuxPackages: v27で追加された設定で、既定は true です。.deb と .rpm の自動更新を入れるとき、GPG署名を強制するかどうかを決めます。electron-builder自身はLinuxパッケージに署名しないため、既定は緩い側です。false にすると、パッケージマネージャーが対応している範囲で署名の検査が強制されます

どちらも、フィードの整合を見る verify-feed.mjs では確かめられません。更新が拒否されるのはクライアント側の判断だからです。CLAUDE.mdには「Webインストーラーを使うか」「Linuxパッケージの署名を強制するか」を書いておき、Claudeが黙って disableWebInstaller = false を足すことを防ぎます。

旧クライアントへの互換: electronUpdaterCompatibility

v27の latest*.yml は、新しい files[] 形式だけで出力されます。electron-updater 1.x〜2.15.0だけが必要とする、先頭階層の path と sha512 は含まれません。古いクライアントがまだ現場に残っているなら、electronUpdaterCompatibility で互換の範囲を宣言します。既定は >=2.16 です。OSごとに指定でき、たとえば win.electronUpdaterCompatibility と書けます。

範囲が2.16.0未満にかかると、先頭階層の path・sha512 と、Windows向けのSHA-256のチェックサム sha2 も出力されます。2.0.0未満にかかるときは、古い latest-mac.json も作られます。ただし、sha2 だけで検証するメタデータは非推奨です。v27は警告を出し、v28は受け付けません。1.x〜2.15のクライアントを実際に配っていないのに、互換の範囲を下げる理由はありません。

この記事の検証スクリプトは files[] だけを見ます。互換の範囲を下げたフィードでは、先頭階層の値も出るので、スクリプトを広げるかどうかは配るクライアントの版で決めます。

更新を入れるタイミングと、独自の検証フック

終了時のインストールが壊れる場合に備える

既定では、ダウンロード済みの更新はアプリの終了時にインストールされます。設定項目は autoUpdater.autoInstallEvent で、値は "manual"、"onQuit"、"onNextLaunch" のどれかです。既定は "onQuit" です。

WindowsでOSのシャットダウンや再起動と重なると、インストーラーが途中で強制終了され、アプリが中途半端にアンインストールされた状態になるおそれがあります。electron-updater 7.0以降(electron-builder v27)では、セッション終了を検知して終了時のインストールを見送る保護が常に働きます。さらに "onNextLaunch" を選ぶと、更新を保留しておき、次の起動時にインストールします。ドキュメントでは、v28で "onNextLaunch" が既定になる予定だと書かれています。

保留したインストールを自動で実行できないターゲットもあります。deb・rpm・pacmanはpkexecやsudoで昇格するため、またマシン単位のNSISはUACが出るため、起動時の自動インストールは見送られます。この場合は installPendingUpdateIfAvailable() を、アプリが好きなタイミングで呼びます。Claudeには、対象ターゲットを先に伝えてから実装を頼みます。

ダウンロード後の検証を自前で足す

すべてのアップデーターが持つ verifyUpdateFile に関数を代入すると、インストール可能になる前に独自の検証を差し込めます。失敗を返すと更新は中止され、ファイルは削除され、ERR_UPDATER_INVALID_UPDATE_FILE が発火します。失敗には、理由を示す message が必須です。

import { NsisUpdater } from "electron-updater"
 
const updater = new NsisUpdater()
updater.verifyUpdateFile = async ({ updateFilePath, originalUpdateFileName }) => {
  try {
    checkSignature(updateFilePath, getCorrespondingSignatureFile(originalUpdateFileName))
    return { response: "success" }
  } catch (err) {
    return { response: "failure", message: `${err}` }
  }
}

checkSignature は例のための仮の関数で、検証の中身は自分で書きます。このフックは、ダウンロード直後だけでなく、前回の起動で保存したキャッシュの再利用時や、次回起動時のインストール直前にも呼ばれます。検証を通っていないファイルが、本来の名前のままインストーラーに渡ることはありません。

認証ヘッダーと配信元のオリジン

非公開のフィードに Authorization ヘッダーを付ける場合、ヘッダーとフィードURLのクエリ文字列が送られるのは、フィードと同じオリジン(スキーム・ホスト・ポート)に対してだけです。latest.yml の files[].url が別のオリジンを指していると、そのダウンロードにはヘッダーが付きません。ドキュメントは、更新ファイルをフィードと同じオリジンから相対パスで配るか、署名付きURLを使うよう案内しています。ローカルの検証でヘッダーなしのまま通っても、本番で認証付きフィードに載せ替えると挙動が変わります。

よくあるつまずき

症状見る場所
macOSで更新が走らない見る場所アプリにコード署名が要ります。署名なしでは自動更新は動かないと公式に書かれています
Windowsで更新が走らない見る場所対象はNSISだけです。Squirrel.Windowsは対象外です
APPIMAGE env is not defined見る場所AppImageとして起動していません。インストール済みのアプリで試すのが推奨です
更新後に署名エラー見る場所公開鍵を埋め込んだ版を出した後に、署名なしのマニフェストを公開していないか
割合を戻しても配信が変わらない見る場所取り下げは、より大きいバージョン番号の公開で行います

自動更新の対象は、macOSがDMG、LinuxがAppImage・DEB・Pacman・RPM、WindowsがNSISです。それ以外のターゲットで配信を計画している場合は、Claudeに実装を頼む前に、この対応表を見直してください。

なお、Claude Code自体の更新方法やチャンネルの選び方は別の話です。そちらはClaude CodeのautoUpdatesChannelでstableとlatestを選ぶにまとめています。

まとめ

electron-updaterの検証で一番手間がかかるのは、実機のインストーラーを毎回用意することです。フィードの整合だけなら、ローカルのHTTPサーバーと30行ほどのスクリプトで足ります。それをStop hookにつなぐと、Claudeは latest.yml と成果物がずれた状態では作業を終えられません。

そのうえで、署名と実インストールの確認は残ります。v27の署名付きマニフェストを使う場合は、段階配信の割合も署名の対象だという点を、運用の設計に入れておく必要があります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. electron.build/docs/features/auto-update
  4. electron.build/docs/features/signed-update-manifests
  5. electron.build/docs/tutorials/release-using-channels
この記事を共有:XはてブLinkedIn