マキモノ
業務自動化無料✅ 公式検証済みv1.0.0 / 更新

複数PCへ配る設定ファイルを端末固有値を壊さず中央更新する

中央でキーやwebhookを張り替えた瞬間に全端末が静かに死ぬ事故を、キー単位マージ配布・保護キー・ローテーション順序・沈黙検出で構造的に防ぐ手順

出品者: kim@orgiast.jp2 DL📖 読込 約3,163トークン (約5円)💰 コスパ 11
トークン節約メーター81%節約
ゼロからAIに作らせた場合4.2万トークン
このMDを読ませた場合8,000トークン

3.4万トークンの節約 (API料金換算で約51円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

複数PCへ配る設定ファイルを端末固有値を壊さず中央更新する」は、業務自動化カテゴリのAI指示書(MDファイル)です。中央でキーやwebhookを張り替えた瞬間に全端末が静かに死ぬ事故を、キー単位マージ配布・保護キー・ローテーション順序・沈黙検出で構造的に防ぐ手順この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.4万トークン(API料金換算で約51円)・81%のトークンを節約できます。

カテゴリ
業務自動化
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.2万トークン
この巻物使用時
約8,000トークン
節約量
約3.4万トークン (約51円)
更新日
2026-08-26

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/md-4668bc78/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

複数PCに配る設定ファイルを「端末固有値を壊さず」中央から更新する

社内の複数端末に同じツールを配り、各端末の ~/.<app>/*.env に webhook URL・APIキー・エンドポイントを置いて運用しているとき、中央でその値を張り替えた瞬間に全端末が静かに死ぬ。この指示書は、その事故を構造的に起こさない配布のしかたと、既に死んでいる場合の見つけ方をまとめたもの。

対象読者: 配布ツール(インストーラ/同期フック/鍵配布API)を自分で書いている人・AI。


1. 何が起きるか(2つの失敗モード)

失敗A: 「既存ファイルがあればスキップ」

配布ツールは事故防止のため if (exists(dest)) continue; が既定になりやすい。ここに更新されてほしい値を乗せると、中央で張り替えた瞬間に各端末は古い値を叩き続け、永久に自己回復できない

さらに悪いことに、この手の値は失敗が例外にならない。

  • webhook: 削除済みの URL に POST → 404 が返るだけ。送信側は成功扱いで終わる
  • APIキー: 401 を握りつぶす実装だと「何も起きない」
  • 「届かないこと」は誰にも自己申告されないので、数日〜数週間気づかない

失敗B: 「全文上書き」

Aを嫌って全文上書きにすると、今度は端末固有の値(表示名・ラベル・ローカルパス・その端末だけのフラグ)が中央の値で潰れる。全端末が同じラベルで報告し始め、どの端末からの報告か識別できなくなる。障害調査の手がかりを自分で消すことになる。


2. 解: キー単位マージ + 保護キー集合

配布物を「ファイル」ではなく「キーの集合」として扱う。

  • 配布ペイロードに載っているキーだけ、既存ファイル内の値を差し替える
  • 載っていない行(コメント・空行・端末固有キー)は一字一句そのまま残す
  • PRESERVE_LOCAL_KEYS(例: 表示名ラベル)は、値の更新も新規追記もしない
  • 実効値が同じなら書き込まない(no-op。mtime を無駄に動かさない)

実装(Node.js / 依存なし)

// 配布ペイロードに載っているキーだけ既存ファイルの値を差し替える。
// 端末固有キーは中央値で潰さない(潰すと全端末が同じ名前で報告し識別不能になる)。
export const PRESERVE_LOCAL_KEYS = new Set(['REPORTER_LABEL']);

export function mergeEnvFile(existingText, incomingText, preserveKeys = PRESERVE_LOCAL_KEYS) {
  const existing = String(existingText ?? '');
  const incoming = String(incomingText ?? '').replace(/^/, ''); // BOM は落とす
  const incomingValues = parseEnvText(incoming);
  const existingValues = parseEnvText(existing);
  const preserved = preserveKeys instanceof Set ? preserveKeys : new Set(preserveKeys);
  const newline = existing.includes('\r\n') ? '\r\n' : '\n'; // 既存の改行コードを壊さない
  const lines = existing.split(/\r?\n/);
  const handled = new Set();

  const updatedLines = lines.map((line) => {
    const m = line.match(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)(.*)$/);
    if (!m || !Object.prototype.hasOwnProperty.call(incomingValues, m[2])) return line; // コメント・無関係行はそのまま
    const key = m[2];
    handled.add(key);
    if (preserved.has(key) || existingValues[key] === incomingValues[key]) return line;
    return `${m[1]}${key}${m[3]}${incomingValues[key]}`; // インデントと = の前後空白を維持
  });

  const additions = Object.keys(incomingValues)
    .filter((key) => !handled.has(key) && !preserved.has(key)) // 保護キーは「追記」もしない
    .map((key) => `${key}=${incomingValues[key]}`);
  if (!additions.length) return updatedLines.join(newline);
  if (updatedLines.length === 1 && updatedLines[0] === '') return additions.join(newline);
  if (updatedLines.at(-1) === '') updatedLines.splice(updatedLines.length - 1, 0, ...additions); // 末尾改行を保つ
  else updatedLines.push(...additions);
  return updatedLines.join(newline);
}

呼び出し側(配布の受け取り部):

const cleaned = contents.replace(/^/, '');
if (fs.existsSync(destination)) {
  const existing = fs.readFileSync(destination, 'utf8');
  const updated = mergeEnvFile(existing, cleaned);
  if (updated === existing) continue;             // no-op
  fs.writeFileSync(destination, updated, { encoding: 'utf8', mode: 0o600 });
  fs.chmodSync(destination, 0o600);               // 秘密ファイルの権限を戻す
  continue;
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, cleaned, { encoding: 'utf8', mode: 0o600 });

最低限のテスト観点

  1. 配布キーだけ更新され、端末固有キーと自由記述コメントが残る
  2. 実効値が同じなら返り値が入力と完全一致assert.equal(merged, existing)
  3. CRLF のファイルが CRLF のまま/末尾改行の有無が変わらない
  4. 既存に無いキーは末尾に追記される/保護キーは追記されない
  5. export KEY = "value" 形式・値に = を含むケースで壊れない

3. 「配布対象に載せる/載せない」の判断を間違えない

最大の落とし穴は実装ではなく設計判断のほうにある。

「端末ごとの個別設定だから配布しない」と決めた値でも、中央で変わるなら配布対象にする。

実例: 通知先 webhook を「各端末が自分で設定するもの」と分類して配布対象から外していた。中央でその webhook を作り直したところ、更新経路が存在しないため管理者機以外の全端末の定期報告が止まり、6日間誰も気づかなかった

判断は「誰が決める値か」ではなく「中央で変わりうるか」で分ける。

値の性質配布保護
中央で発行・失効・張り替えが起きる(キー・URL・エンドポイント)するしない(更新対象)
その端末を識別するための名前・ラベルしないする
ローカル絶対パス・端末固有の閾値しないする

4. ローテーションの順序(これを守らないと鶏卵になる)

  1. クライアント修正を先に配布(マージ更新できる版を全端末に行き渡らせる)
  2. 中央の値を張り替え、旧値を一時 LEGACY として併存させる
  3. 再デプロイ
  4. 各端末が新値へ移行したことを実測(受領ログ/サーバ側の「旧値で認証された」件数が 0 になったか)
  5. LEGACY を削除

②から始めると、「新値をもらうには認証が要る/認証には新値が要る」という復帰不能状態になる。


5. 沈黙を検出する(これが無いと同じ事故を繰り返す)

定期報告そのものの途絶を監視対象にする。 届かないことは自己申告されない。

  • 端末ごとの最終報告時刻を集計し、しきい値(例: 想定間隔の3倍)を超えたら警告する
  • 送信先の死活を定期確認する。webhook なら GET404/10015 を検出(User-Agent を付ける。付けないと CDN 側が 429 を返し死活判定を誤る)
  • 生きている別の通知先を台帳に記録しておく。死ぬと宛先情報自体が引けなくなるので、生存中に控えるしかない
  • 通知は必ず生きている宛先へ送る(死んだ宛先に「死んでいます」と送らない)

既に死んでいるかを後から調べる方法

通知先のメッセージ履歴を機械的に舐め、端末ラベルごとの初回/最終出現を集計する。

// 直近N件を取得し、報告本文から端末ラベルを抽出して first/last を集計する
const patterns = [/^\*\*(.+?)\*\* 定期レポート/m, /レポート\*\* — (.+)/];
// → ラベルごとに {件数, 初回, 最終} を出す
  • ある日付を境に複数端末が一斉に無音になっていたら、端末側の事情ではなく配布/宛先側の事故
  • 逆に「その日に初めて出現したラベル」は、その端末でツールが初めて動いた証拠になる(導入完了の客観的な確認に使える)

6. 端末と人の対応を、人手ゼロで埋める

「このPCは誰のものか」を台帳に手入力させると必ず腐る。報告本文に機械可読な識別子を載せる

export function machineIdentity() {
  let gitEmail = '未設定';
  try {
    const r = spawnSync('git', ['config', '--global', 'user.email'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
    const v = r.status === 0 ? r.stdout.trim() : '';
    if (v) gitEmail = v;
  } catch {}
  return { hostname: os.hostname(), username: os.userInfo().username, gitEmail };
}

識別行は本文の先頭付近に置く。 多くの通知APIには本文長の上限があり、末尾に置くと明細が長い端末でだけ欠落する(=一番情報が欲しい端末で消える)。

プライバシー: ホスト名・OSユーザー名・メールアドレスを送るなら、本文に「何を送っているか」を明記する。断りなく増やさない。


7. チェックリスト

  • 中央で変わる値が、配布ペイロードに載っているか
  • 受け取り側は「既存ならスキップ」ではなくキー単位マージ
  • 端末固有キーが保護され、更新も追記もされないか
  • no-op 判定があるか(無変更なら書かない)
  • 秘密ファイルの権限(0600)が更新後も維持されるか
  • ローテーションは「クライアント修正 → 張替+LEGACY → 実測 → LEGACY削除」の順か
  • 報告の途絶を監視しているか
  • 識別子が本文の先頭付近にあるか
  • 秘密値がログ・通知・台帳のいずれにも出ていないか

よくある質問

「複数PCへ配る設定ファイルを端末固有値を壊さず中央更新する」とは何ですか?

中央でキーやwebhookを張り替えた瞬間に全端末が静かに死ぬ事故を、キー単位マージ配布・保護キー・ローテーション順序・沈黙検出で構造的に防ぐ手順

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.2万トークンかかりますが、この巻物を使えば約8,000トークンで済みます。差し引き約3.4万トークン(API料金換算で約51円)・81%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア