1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本語の .ps1 は BOM 付き UTF-8 で保存する:Windows の実機で Claude Code からスキルを動かすまで

1
Posted at

株式会社CHIERO のチエロです。
日本語を含む .ps1 は、BOM 付きの UTF-8 で保存します。Windows 標準の PowerShell 5.1 は、BOM のない UTF-8 を別の文字コードとして読むためです。
BOM 付きで保存した PowerShell スクリプト4本は、Windows Server 2025 の実機で動きました。Claude Code(claude -p)から、入れた入口スキルを呼ぶところまで確かめています。

Mac 前提の .sh を、Windows の PowerShell に持っていくと、動かなくなる理由がいくつもあります。
私は、自分の会社で使っているフォルダ一式(AI社員の入口と手順書の一式)の .sh 4本を移すときに、文字コード、実行ポリシー、パスの書き方、画面の日本語の4つに備えました。
このうち、BOM と実行ポリシーは、わざと失敗する条件にして、止まるところを実機で確かめています。4本とも、BOM 付きで保存した状態で、初回の実行から動きました。

この記事では、次の3つを書きます。

  • 移すときに備えた4つの点と、そのときのコード(実物の抜粋)。失敗を再現して確かめた点と、書き方の紹介にとどまる点を分けて書きます
  • claude -p で、スキルが本当に呼ばれたかを確かめた方法
  • Codex の Windows 対応について、公式ドキュメントに書かれていること

確かめたのは2026年10月26日で、環境は1台です。
Codex の実機確認と、Windows 10・11 のパソコンでの確認は終わっていません。
確かめた範囲と、確かめていない範囲は、最後の節に分けて書きました。

実機と、移した4本

項目 値
機械 Windows Server 2025 Standard の VPS(ビルド 26100・x64・ロケール ja-JP・コードページ 932)
PowerShell 5.1.26100.32995(Windows 標準。pwsh(7)は入っていない)
実行ポリシー RemoteSigned(機械の既定)
Node.js / git v24.18.0 / 2.55.0.windows.1
Claude Code 2.1.198(npm で入れた claude.ps1)
メモリ 3 GB
接続 SSH の鍵認証。ファイルは scp で送った

Claude Code の公式のシステム要件は、メモリ 4 GB 以上です。この機械は 3 GB で、下の短い非対話の実行は動きましたが、おすすめできる環境ではありません。
Codex は入っていません。

移したのは、次の4本です。

Mac(.sh) Windows(.ps1) 役割
install.sh install.ps1 入口スキルを、Claude Code と Codex のスキル置き場へ入れる
check_env.sh check_env.ps1 版・メモリ・空き容量・git などを表示する(何も書かない)
memory-check.sh memory-check.ps1 日誌の抜けと、リンク切れを数える
new-role.sh new-role.ps1 新しい担当の雛形を作る

実機では、4本の実行、install.ps1 の各指定、実行ポリシーの3通り、Claude Code からの呼び出しを含めて、24項目のテストをしました。
.ps1 の中身は、.sh が sed や awk でしていた処理を、PowerShell の書き方に置き換えたものです(文字列の置換や日付の計算は、.NET の関数を使っています)。
動作の確認は、Mac の .sh の結果と比べる形にしました。比べたのは memory-check・new-role・install の3本で、check_env は Windows 側の出力を読んで確かめています。

移すときに備える4つの点

文字コード、実行ポリシー、パス、画面の日本語の順に書きます。
BOM と実行ポリシーは、失敗を再現して確かめました。パスと画面の日本語は、備えた書き方の紹介です。

1. BOM のない UTF-8 の .ps1 は、構文エラーになる(失敗を再現)

Mac のエディタで保存した .ps1 は、ふつう BOM のない UTF-8 になります。
これを Windows PowerShell 5.1 で実行すると、日本語の文字列を含む行が壊れます。

Microsoft の公式ドキュメントには、次の趣旨が書かれています。

  • 非 ASCII の文字をスクリプトに書くときは、UTF-8 の BOM 付きで保存する
  • BOM がないと、Windows PowerShell はスクリプトを、旧来の「ANSI」コードページの文字コードとして読む

出典:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding

この機械の ANSI は、日本語の CP932 です。
同じ4本を、わざと BOM なしの UTF-8 で保存し直して実行したところ、構文エラーで動きませんでした。
')' が存在しません などのエラーが、多数出ました。
BOM 付きに戻すと、初回から動きます。

私の一式では、.ps1 を BOM 付きで保存し、出荷前の検査にも入れました。

if ext == '.ps1':
    if not raw.startswith(b'\xef\xbb\xbf'):
        add('ps1 が BOM 付き UTF-8 でない', rel, 0)

Mac 側で手早く確かめるなら、先頭の3バイトを見ます。

head -c 3 install/install.ps1 | xxd
# 00000000: efbb bf

efbb bf が出れば、BOM 付きです。

逆に、スクリプトが書き出すファイルは、BOM なしにしています。
install.ps1 が入れる SKILL.md が、その例です(私の一式の出荷前の検査は、.ps1 以外に BOM が付いていると指摘します)。
Windows PowerShell 5.1 の Set-Content -Encoding UTF8 や Out-File は、BOM を付けたり、UTF-16 で書いたりします(上の公式ドキュメントに、コマンドごとの既定が書かれています)。
そこで、.NET の WriteAllText に、BOM なしの UTF8Encoding を渡しています。

$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)

$text = [System.IO.File]::ReadAllText($p.Src, $Utf8NoBom)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
# (途中で {WORKSPACE} を実際のパスに置き換える)
[System.IO.File]::WriteAllText($target, $text, $Utf8NoBom)

読むときにも、先頭に BOM が残っていたら外します。
実機で入れた48件のうち、テストで足した1件を除く47件の SKILL.md を Mac に持ち帰って、install.sh が入れる物と比べたところ、中身は同じでした(違ったのは、3つ目の節で書くパスの表記だけです)。
先頭に BOM はありませんでした。

保存し直したあとに構文エラーが無いかは、Windows 側で Parser::ParseFile を使うと、実行せずに確かめられます。私が実機で使った API は、これです。書き方の例を載せます。

$tokens = $null; $errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors) | Out-Null
$errors.Count   # 0 なら構文エラーなし

実機では、4本とも 0 件でした(BOM 付きの状態で)。

2. 実行ポリシーで、スクリプトが止まる(失敗を再現)

2つ目は、実行ポリシーです。

私の機械は RemoteSigned でした。
公式ドキュメントによると、どの範囲にも設定が無いときの実効の既定は、Windows のクライアントで Restricted、Windows Server で RemoteSigned です。
つまり、私の機械は、動きやすい側の既定でした。
手元の Windows 10・11 のパソコンでは、同じ結果になるとは限りません。

出典:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies

実機で、3通りを試しました。

実行のしかた 結果
-ExecutionPolicy を付けない(機械は RemoteSigned・scp で送った) 動いた
-ExecutionPolicy Bypass を付ける 動いた
-ExecutionPolicy Restricted を付ける 「このシステムではスクリプトの実行が無効になっているため…」で止まった

そこで、手順書に書く実行の形は、1つに決めました。

powershell -NoProfile -ExecutionPolicy Bypass -File .\install\install.ps1 -DryRun
  • -ExecutionPolicy Bypass は、この実行1つにだけ効きます。PC の設定は変わりません
  • -NoProfile は、利用者のプロファイルが実行に混ざるのを避けます
  • -DryRun は、入れる先を表示するだけで、何も書きません。最初の1回は必ずこれにします

公式ドキュメントには、グループポリシーで決められた実行ポリシーは、この指定で上書きできないとも書かれています。
会社のパソコンで止まった場合は、まず情報システムの担当に聞くのが安全です。

ネットから取った zip の中の .ps1 は、別の理由で止まることがあります。
ブラウザでダウンロードしたファイルには「インターネットから来た」印が付き、RemoteSigned では署名のない物が実行されません。
印は Unblock-File で外せます。

Get-ChildItem -Recurse . | Unblock-File

公式ドキュメントには、curl.exe や Invoke-WebRequest で取ったファイルには印が付かない場合がある、とも書かれています。
私の実機テストでは、scp で送ったので、印はありませんでした。
ブラウザでダウンロードした zip を展開した直後の実行は、まだ試していません。

3. ~ と %USERPROFILE%、パスの区切り(書き方の紹介)

3つ目は、パスです。

Mac 前提の手順書には、~/.claude/skills のような書き方が出てきます。
Windows では、~/ は %USERPROFILE%\ に読み替えます。PowerShell の中では $env:USERPROFILE\ です。
install.ps1 は、ホームフォルダを、次のように決めています。

$HomeDir = $env:USERPROFILE
if ([string]::IsNullOrEmpty($HomeDir)) {
  $HomeDir = [Environment]::GetFolderPath('UserProfile')
}

入れる先は、-Project でフォルダを指定すると、そのフォルダの .claude\skills になります。指定しない時は、ホームの .claude\skills です。
実機テストでは、VPS のホームにある Claude Code の設定を変えないよう、すべて -Project で、テスト用のフォルダの中に入れました。

function Get-SkillsRoot([string]$Side) {
  if ($Side -eq 'claude') { return (Join-Path $Base '.claude\skills') }
  if ($CodexLegacy) { return (Join-Path $Base '.codex\skills') }
  return (Join-Path $Base '.agents\skills')
}

フォルダやファイルの存在確認・解決・列挙には、-LiteralPath を使っています。
-LiteralPath は、文字を入力どおりに使い、ワイルドカードとして解釈しません([ や ] を含むフォルダ名でも、そのまま扱えます)。

そして、細かいけれど大事な1行があります。
入口スキルの中の {WORKSPACE}(一式の置き場)を、実際のパスに置き換えるとき、\ を / にそろえています。

$WS = (Resolve-Path -LiteralPath $Workspace).ProviderPath
$WS = $WS.TrimEnd('\')
# ...
$WSOut = $WS.Replace('\', '/')

入った SKILL.md には、C:/Users/<ユーザー名>/ai-team-test/ai-team/routing.md の形でパスが書かれます。
手順書の中の他のパスが / 区切りなので、そろえました。
Claude Code は、次の節の確認で、この / の形のパスを Grep できました。別の回では、\ の形のパスで Read していました。
\ のまま書いた場合との比較は、していません。

4. 日本語の出力は UTF-8 にそろえる(書き方の紹介)

4つ目は、画面に出す日本語です。

この機械のコンソールは、コードページ 932 です。
4本とも、冒頭の近くに、同じ2行を置いています。

# 日本語の出力が文字化けしないよう、出力を UTF-8 にそろえる(Windows PowerShell 5.1 でも動く書き方)
try { chcp 65001 | Out-Null } catch { }
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }

どちらも、失敗しても先へ進むように、try で包んであります。
この2行を入れた状態で、SSH 経由の実行でも、日本語の出力は読める形で返ってきました。
この2行を外した場合との比較は、していません。

check_env.ps1 の出力は、次のようになりました(実機の出力の抜粋です)。

[OK]       Windows の版           Microsoft Windows Server 2025 Standard(ビルド 26100)
[要確認]   メモリ                  3 GB(4GB 未満は、この一式の動作の目安を下回ります。…)
[参考]     PowerShell           5.1.26100.32995(このウィンドウで動いている版)
[OK]       git                  git version 2.55.0.windows.1

メモリが「要確認」になるのは、この機械の実際の値(3 GB)に対する、正しい出力です。

おまけ:同じ結果にするために入れた細部(改行と日付)

Mac の .sh と同じ結果にするために入れた細部が2つあります(外した場合との比較は、していません)。

1つ目は、書き出す改行です。
new-role.ps1 は、雛形の文字列に印を置き、置き換えてから、末尾に LF の改行を足して書きます。

function Fill([string]$t) {
  return $t.Replace('@@NAME@@', $RoleName).Replace('@@ID@@', $Id).Replace('@@SKILL@@', $SkillName)
}
[System.IO.File]::WriteAllText((Join-Path $SkillDir 'SKILL.md'), ((Fill $SkillTemplate) + "`n"), $Utf8NoBom)

作られた3つのファイルを、Mac の new-role.sh が作る物と diff -r で比べたところ、差はありませんでした。

2つ目は、日付の計算です。
memory-check.ps1 は、日誌のファイル名の日付を、カルチャーを固定して読みます。

$Culture = [System.Globalization.CultureInfo]::InvariantCulture
$FirstDay = [datetime]::ParseExact($First, 'yyyy-MM-dd', $Culture)

日誌を3枚置いて実行したところ、「日誌の抜け」の節の出力が、Mac の memory-check.sh と一字一句同じでした(最新が3日前・平日の抜け6日)。

claude -p で、スキルが呼ばれたかを確かめる

ここからが、Claude Code の確認です。

入口スキルを入れて、同じフォルダで claude -p(非対話)を実行しました。

cd C:\Users\<ユーザー名>\ai-team-test\ai-team
powershell -NoProfile -ExecutionPolicy Bypass -File .\install\install.ps1 -DryRun -Project . -Claude
powershell -NoProfile -ExecutionPolicy Bypass -File .\install\install.ps1 -Project . -Claude
claude -p "aiteam で routing.md を読んで、24の担当の名前を言って。" --max-turns 3

実際の実行では、-Project にフォルダの絶対パスを渡しました。結果は同じです。
入口スキルの名前は aiteam です。Claude Code には、この実行の前にログインしてあります。

結果は、終了コード 0 で、24の担当の名前が表で返りました(出力の1行目です。続きは担当の ID と役割の表でした)。

The routing.md file lists 24 roles. Here are their names (呼び出し語 / 担当ID):

ただ、1回目の質問は routing.md を名指ししています。作業フォルダに実物があるので、スキルが呼ばれなくても、ファイルを読めば答えられます。
そこで、2回目は、routing.md を名指ししない質問にして、呼び出しの記録そのものを見ました。

claude -p "aiteam を使って、LP を作りたい時はどの担当に振り分けるか、担当名だけ答えて。" --max-turns 4 --output-format stream-json --verbose

公式ドキュメントのとおり、stream-json は --verbose と一緒に使います。
出力は、1行ごとに1つのイベントの JSON です。

2回目の記録で見たのは、次の2つです。

  1. Skill ツールの呼び出し(tool_use)があり、aiteam を呼んでいる
  2. そのあとに、一式の中の routing.md を、C:/Users/<ユーザー名>/ai-team-test/ai-team/routing.md の形のパスで Grep している

4ターンで成功し、担当名が1つ返りました。
1回目も、stream-json で記録を見ました。起動時のスキルの一覧に aiteam があり、routing.md を Read していました(2ターン)。

確認のときに、気をつけた点が3つあります。

  • --bare を付けない。公式ドキュメントによると、--bare はスキルの自動発見を省くため、スキルの確認には使えません
  • 答えの文に加えて、tool_use を見る
  • 質問には、スキルの名前(aiteam)を入れ、読んでほしいファイルの名前は入れない

直したのは、私が SSH 経由でコマンドを組み立てたときの、引用符の入れ子の間違いだけです。
日本語を含む引用符の問題や、文字化けは、claude -p の呼び出しでは出ませんでした。

書き込みを伴う手順は、この確認では試していません。
公式ドキュメントによると、claude -p は、承認が要る操作を、許可の指定が無ければ拒否します。書き込みを伴う手順を非対話で動かすときは、--allowedTools か許可モードの指定が要ります。

公式ドキュメントの記述(Claude Code と Codex)

実機で確かめた範囲の外は、公式ドキュメントに書かれていることだけを、出典の URL つきで書きます。

Claude Code の Windows 対応

公式のセットアップのページには、次のことが書かれています。

  • 対応は Windows 10 1809 以降と Windows Server 2019 以降。メモリは 4 GB 以上
  • シェルは PowerShell と CMD に対応。Git for Windows は任意で、無いときは PowerShell をシェルのツールとして使う
  • PowerShell の行頭は PS C:\、CMD は C:\。PowerShell に CMD 用の && を貼ると構文エラー、CMD に irm を貼ると認識されない
  • WSL も使える

出典:https://code.claude.com/docs/en/setup
非対話の実行(-p):https://code.claude.com/docs/en/headless

この節と、前の節の --bare・承認の説明は、私が10月27日に読んだ公式ドキュメントの内容です。実機の Claude Code は 2.1.198 で、ドキュメントにはもっと新しい版の記述が含まれます。

私の機械には、Git for Windows が入っています。Git が無い環境は試していません。

Codex の Windows 対応

Codex は、実機で確かめていません。
公式ドキュメントに書かれていることだけを書きます。

  • Codex は、PowerShell で、Windows のサンドボックスを使って、ネイティブに動く。WSL や仮想マシンは必須でない
  • Windows 11 が推奨。Windows 10 は、更新済みの新しい版が「できる範囲」の扱い(1809 以降)
  • Linux 向けの道具が要るときなどには、WSL を使う選択もある
  • ユーザーのスキルは $HOME/.agents/skills、リポジトリのスキルは作業フォルダからリポジトリの根までの .agents/skills。.codex/skills は載っていない

出典:https://learn.chatgpt.com/docs/windows/windows-sandbox
出典:https://learn.chatgpt.com/docs/build-skills

読んだ範囲では、$HOME が Windows で何を指すかは、書かれていません。
通常は %USERPROFILE% と読めますが、確かめていません。
そのため、install.ps1 -Codex は %USERPROFILE%\.agents\skills\aiteam\SKILL.md に入れ、旧来の .codex\skills に入れたい人のために -CodexLegacy を用意しました。
実機で確かめたのは、-Project でフォルダを指定した場合に、そのフォルダの .agents\skills と .codex\skills の下にファイルができたところまでです。
ホーム(%USERPROFILE%)への実際の書き込みと、Codex がそれを読むかは、確かめていません。

確かめた範囲と、まとめ

確かめたこと(Windows Server 2025・PowerShell 5.1・日本語環境の1台)

  • .ps1 の4本が動く。memory-check・new-role・install の3本は、Mac の .sh の結果と一致する
  • -ExecutionPolicy を付けない実行と Bypass の実行が動く。Restricted では止まる
  • BOM なしで保存し直すと、構文エラーで動かない
  • install.ps1 の -DryRun、入れる、すでに在るときに止まる、-Force での上書き
  • install.ps1 -Project で入れた入口スキルが、そのフォルダの claude -p から呼ばれ、一式の中のファイルを読む

確かめていないこと

  • Codex を Windows で動かして、入口スキルが読まれるか
  • Windows 10・11 のパソコン、PowerShell 7(pwsh)、日本語以外のロケール
  • ブラウザでダウンロードした zip を展開した直後の実行
  • ホームの %USERPROFILE%\.claude\skills と %USERPROFILE%\.agents\skills への実際の書き込み、対話画面、各担当のスキルからの呼び出し
  • Git for Windows が入っていない環境
  • 担当の仕事の中身(朝の報告を出す、など)の実行

テストが終わったあと、テスト用のフォルダと zip は削除しました(削除後の確認で、フォルダが無いことを見ています)。

移す前に確かめる4つ

  1. .ps1 は BOM 付き UTF-8 で保存する(head -c 3 file | xxd で efbb bf を見る)
  2. 実行は -NoProfile -ExecutionPolicy Bypass -File で書き、最初は -DryRun
  3. パスは $env:USERPROFILE と -LiteralPath。スキルに書き込むパスは、区切りを1種類にそろえる(私は /)
  4. スキルが呼ばれたかは、stream-json の tool_use で見る

公開している物

  • AI社員一式(note):AI社員を作れるのは、これです。24の担当と会社の決まりを、139のファイルで渡す一式です。Mac と、Windows の PowerShell で使えます(確認した範囲は、上の節のとおりです)。
    https://note.com/kounkt/n/nf6640ae56f4e
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?