株式会社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」コードページの文字コードとして読む
この機械の 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 のパソコンでは、同じ結果になるとは限りません。
実機で、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つです。
-
Skillツールの呼び出し(tool_use)があり、aiteamを呼んでいる - そのあとに、一式の中の
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つ
-
.ps1は BOM 付き UTF-8 で保存する(head -c 3 file | xxdでefbb bfを見る) - 実行は
-NoProfile -ExecutionPolicy Bypass -Fileで書き、最初は-DryRun - パスは
$env:USERPROFILEと-LiteralPath。スキルに書き込むパスは、区切りを1種類にそろえる(私は/) - スキルが呼ばれたかは、
stream-jsonのtool_useで見る
公開している物
-
AI社員一式(note):AI社員を作れるのは、これです。24の担当と会社の決まりを、139のファイルで渡す一式です。Mac と、Windows の PowerShell で使えます(確認した範囲は、上の節のとおりです)。
https://note.com/kounkt/n/nf6640ae56f4e