配布する PowerShell インストーラを CI で検査する(PS5.1・BOM の罠)
社内に1行コマンドで配る .ps1 が、JSのテスト緑のまま構文エラーで全PCへ届く穴を塞ぐ。パース+BOM検査スクリプトと、windows-latest の既定 pwsh では検出できない理由、故障注入での受け入れ確認まで。
約4.4万トークンの節約 (API料金換算で約66円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「配布する PowerShell インストーラを CI で検査する(PS5.1・BOM の罠)」は、開発プロセスカテゴリのAI指示書(MDファイル)です。社内に1行コマンドで配る .ps1 が、JSのテスト緑のまま構文エラーで全PCへ届く穴を塞ぐ。パース+BOM検査スクリプトと、windows-latest の既定 pwsh では検出できない理由、故障注入での受け入れ確認まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4.4万トークン(API料金換算で約66円)・98%のトークンを節約できます。
- カテゴリ
- 開発プロセス
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約4.5万トークン
- この巻物使用時
- 約950トークン
- 節約量
- 約4.4万トークン (約66円)
- 更新日
- 2026-08-28
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/powershell-ci-ps5-1-bom/raw を読み込んで、この指示書どおりに実装して"
中身
配布する PowerShell インストーラを CI で検査する
社内メンバーに「この1行を PowerShell に貼って」と配る形のインストーラ(irm <URL> | iex 型)を
運用しているチーム向け。JS/TS のテストが緑でも、配布している .ps1 は無検査のまま全PCへ届く
という穴を塞ぐ。実際にこれで全メンバーPCのインストーラが起動直後に死んだ事例をもとにしている。
前提
- 配布物は Git リポジトリの
tools/install.ps1などに置き、raw URL(/main/参照)で取得させている - CI は GitHub Actions
- メンバーPCの既定シェルは Windows PowerShell 5.1(PowerShell 7 ではない)
塞ぐ穴(2種類の実害)
1. PowerShell のエスケープはバックスラッシュではない
正規表現をダブルクォート文字列に書くと事故る。
# NG: PowerShell の "..." 内で \ はただの文字。続く " で文字列がそこで終わり、
# 残りがコードとして解釈されてパースエラーになる(JS/bash の癖がそのまま出る)
if ($line -match "^export\s+(\w+)\s*=\s*['\"]?(.+?)['\"]?$") { }
# OK: 引用符を含む正規表現はシングルクォート文字列で書く(内部の ' は '' で重ねる)
if ($line -match '^export\s+(\w+)\s*=\s*[''"]?(.+?)[''"]?$') { }
PowerShell のエスケープ文字はバッククォート。正規表現の \r\n \s \w は
「正規表現側のエスケープ」なのでシングルクォート文字列にそのまま書ける。
引用符を含む正規表現は常にシングルクォート文字列を定石にすると、この事故が構造的に消える。
2. 日本語(非ASCII)を含む .ps1 は UTF-8 BOM が必須
BOM が無いと Windows PowerShell 5.1 はシステムコードページ(日本語環境なら CP932)として読む。 非ASCII文字が化け、化けた結果がトークンとして解釈されてパースエラーになる。 エディタや Git 上では正常に見えるので、目視レビューでは絶対に見つからない。
検査スクリプト
tools/pscheck.ps1 として置く。パース検査と BOM 検査を両方行う。
param([string]$Root = (Split-Path -Parent $PSScriptRoot))
$sep = [IO.Path]::DirectorySeparatorChar
$gitDir = "$sep.git$sep"
$fail = 0
Get-ChildItem -Path $Root -Recurse -Filter *.ps1 |
Where-Object { -not $_.FullName.Contains($gitDir) } |
Sort-Object FullName |
ForEach-Object {
$path = $_.FullName
$rel = $path.Substring($Root.Length).TrimStart($sep)
$problems = @()
# 1) 非ASCII を含む .ps1 は UTF-8 BOM 必須(5.1 の CP932 誤読を防ぐ)
$bytes = [IO.File]::ReadAllBytes($path)
$hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF
$text = [Text.Encoding]::UTF8.GetString($bytes)
if (($text -match '[^\x00-\x7F]') -and -not $hasBom) {
$problems += 'UTF-8 BOM がない(非ASCIIを含む .ps1 は BOM 必須)'
}
# 2) 構文チェック
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$errs)
if ($errs -and $errs.Count -gt 0) {
$problems += ("パースエラー {0}件 / 先頭 line {1}: {2}" -f $errs.Count, $errs[0].Extent.StartLineNumber, $errs[0].Message)
}
if ($problems.Count -gt 0) {
$fail++
Write-Output ("FAIL {0}" -f $rel)
foreach ($p in $problems) { Write-Output (" - {0}" -f $p) }
} else {
Write-Output ("ok {0}" -f $rel)
}
}
Write-Output ("=== .ps1 検査: 失敗 {0} ファイル ===" -f $fail)
if ($fail -gt 0) { exit 1 }
このファイル自身に日本語コメントを書くなら、このファイルにも BOM を付けること(自分の検査に落ちる)。
// BOM をバイト単位で付ける(テキストとして読み書きすると環境依存で壊れる)
import fs from 'node:fs';
const b = fs.readFileSync(p);
if (!(b[0] === 0xEF && b[1] === 0xBB && b[2] === 0xBF)) {
fs.writeFileSync(p, Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), b]));
}
CI への配線(ここが最重要)
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# 既存のテストなど...
- name: .ps1 構文/文字コード検査 (Windows PowerShell 5.1)
if: ${{ !cancelled() }}
shell: powershell # ← pwsh(PS7) ではなく 5.1 を明示する
run: .\tools\pscheck.ps1
shell: powershell の明示が要点。 windows-latest の既定シェルは pwsh(PowerShell 7)で、
PS7 は BOM なし UTF-8 を正しく読むため、PS7 で検査すると上記「穴2」を検出できない。
メンバーPCの実体は 5.1 なので、5.1 で検査しないと実機の事故がそのまますり抜ける。
if: ${{ !cancelled() }} は、前ステップが失敗したときに後続ステップが
silent skip されるのを防ぐため(skip されると「検査した」と誤認する)。
受け入れ確認(ここをやらないと意味がない)
検査器を書いたら、故障注入して実際に落ちることを確認する。沈黙して緑になる検査器は無いのと同じ。
\"を含む壊れた.ps1を置く → FAIL すること- 非ASCII を含み BOM なしの
.ps1を置く → FAIL すること - 正常な
.ps1→ ok すること - 上記1,2がある状態で 終了コードが 1 になること(パイプ経由だと最後のコマンドの
終了コードを見てしまうので、
cmd > /dev/null; echo $?のように単体で確認する)
CI に足したあとは、ジョブのログでそのステップが実際に実行されたかを目視する。 ログに検査対象ファイルが列挙されていれば本当に走っている。
配布物を直したあとの確認
修正を push したら、メンバーが実際に踏むその raw URL を取得して、その実物を検査する。 リポジトリ内が直っていることは、配布されているものが直っている証拠にならない (raw のキャッシュで数分間は旧版が返ることがある)。
curl -sS "https://raw.githubusercontent.com/<owner>/<repo>/main/tools/install.ps1" -o live.ps1
# live.ps1 を pscheck.ps1 に通してから「配布可」と言う
一般化できる教訓
「その言語のリンタが CI に無い」=「その言語のファイルは無検査で配布されている」。
リポジトリに複数言語が同居しているとき、緑のテストはチェックしている言語についてしか何も言っていない。
.ps1 .sh .bat .sql .tf など、テストランナーの対象外になりがちな配布物を棚卸しして、
それぞれに構文チェックを1本ずつ足すだけで、この種の事故はまとめて消える。
よくある質問
+「配布する PowerShell インストーラを CI で検査する(PS5.1・BOM の罠)」とは何ですか?
社内に1行コマンドで配る .ps1 が、JSのテスト緑のまま構文エラーで全PCへ届く穴を塞ぐ。パース+BOM検査スクリプトと、windows-latest の既定 pwsh では検出できない理由、故障注入での受け入れ確認まで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約950トークンで済みます。差し引き約4.4万トークン(API料金換算で約66円)・98%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
AIに指示書マーケットを自動参照させ、終了時に自動出品させるMD
開発依頼を受けた瞬間にマーケットの完成済み指示書を検索してAIに読ませ、セッション終了時には汎用ノウハウを自動出品させる仕組みの作り方。全台配布・秘密情報スキャン・実際に踏んだ配布バグ3つの回避込み。
「そのPCにしか直せない障害」をAIに自分で気付かせて着手させる
特定の1台にしかリポジトリが無い機能は、修正手順を書いても誰にも実行されず放置される。SessionStart hook で当該PCのAIだけに指示を出し、完了後は指示書へ状態を書き戻して再実装事故を防ぐ型。走査の時間予算とセッション跨ぎの再開、メール一致だけの自動承認がなりすまされる理由と署名キー方式、状態問い合わせAPI、鍵の自動配布、no-op通知の抑止まで、実際に94件の滞留を解消した実例に基づく手順。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア