# Claude Code / Codex を Windows で回すための初期設定一式

日本語環境の Windows 11 で、Claude Code を毎朝の自動実行として本番運用している構成から、
環境依存の対策だけを抜き出したものです。そのままコピーして使えます。
ライセンスは自由（帰属表示も不要）。

出典: https://aidia.pengi-n.co.jp/articles/claude-code-windows/
実測環境: Windows 11 Home 26200 / Python 3.11.1 / Git Bash / タスクスケジューラで定期実行

---

## 0. 潰す順番

**この順番を変えないでください。1と2を先に潰さないと、あとの検証が全部あてになりません。**

1. 標準出力の文字コード（§1）
2. 外部CLIの解決とプロンプトの渡し方（§2）
3. ファイル書き込みの改行（§3）
4. ファイルロックのAPI（§4）

---

## 1. 標準出力を UTF-8 にする

日本語版 Windows のコンソールは既定が CP932 です。APIから返った日本語をそのまま書くと壊れます。
**環境変数だけでは足りません。**呼び出し元が引き継がない経路（別プロセス起動、タスクスケジューラ）があります。

新しいスクリプトを作るたびに先頭へ入れてください。

```python
import sys

for s in (sys.stdout, sys.stderr):
    if s.encoding and s.encoding.lower() != "utf-8":
        s.reconfigure(encoding="utf-8")
```

外部プロセスを呼ぶときは環境変数も明示的に渡します。

```python
import os, subprocess, sys

r = subprocess.run(
    [sys.executable, "child.py"],
    capture_output=True, text=True,
    encoding="utf-8", errors="replace",
    env={**os.environ, "PYTHONIOENCODING": "utf-8"},
)
```

`errors="replace"` は、壊れた1文字で処理全体を落とさないためです。
ログ用途なら潰れた文字が残るほうが追いやすくなります。

---

## 2. 外部CLIはフルパスで持ち、プロンプトは標準入力で渡す

### 2-1. フルパス

Git Bash から名前で呼ぶと `command not found` になることがあります。PATH に載っていても解決されません。

```python
from pathlib import Path

CLI = Path(r"C:\Users\<user>\bin\codex.cmd")   # 自分の環境のパスに置き換える

if not CLI.exists():
    raise SystemExit(f"[ERROR] CLI がありません: {CLI}")
```

`.cmd` / `.bat` のラッパーはシェルによって解決の仕方が変わります。
**「自分の端末では通る」で書くと、タスクスケジューラから起動した時に落ちます。**

### 2-2. 長いプロンプトは標準入力から

**これが一番厄介です。** 長い日本語の指示をコマンド引数で渡すと、シェルを通る途中で壊れます。
エラーは出ません。モデルが指示を無視したように見えますが、実際には指示が到達していません。

```python
import subprocess, tempfile
from pathlib import Path

def run_cli(cli: Path, prompt: str, timeout: int = 420) -> str:
    with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False,
                                     encoding="utf-8") as f:
        f.write(prompt)
        pf = Path(f.name)
    try:
        with pf.open("r", encoding="utf-8") as fh:
            r = subprocess.run(
                [str(cli), "exec", "-"], stdin=fh,
                capture_output=True, text=True,
                encoding="utf-8", errors="replace", timeout=timeout)
        return (r.stdout or "") + (r.stderr or "")
    finally:
        pf.unlink(missing_ok=True)
```

**見つけ方が本題です。** エラーが出ないので、出力を目で見るまで気づけません。
生成物を毎回目視で確認する手順を運用に入れてください。
「エラーが出ていないから通っている」と扱わないこと。

---

## 3. ファイル書き込みの改行

Windows では既定で改行が `CRLF` に変換されます。CSVやSVGを書くと余分な改行が入ります。

```python
import io

io.open(path, "w", encoding="utf-8", newline="").write(text)
```

`csv` モジュールを使うときも同じです。

```python
import csv, io

with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
    csv.writer(f).writerows(rows)
```

`utf-8-sig` にしているのは、Excel で開いたときに文字化けしないようにするためです。

---

## 4. ファイルロック（OS依存）

複数のジョブが同じリポジトリを触るなら排他制御が要ります。**取れなければ待たずに落とす**のが要点です。

### Windows

```python
import os
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator

@contextmanager
def named_lock(lock_path: Path) -> Iterator[None]:
    import msvcrt
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    handle = lock_path.open("a+b")
    try:
        handle.seek(0, os.SEEK_END)
        if handle.tell() == 0:
            handle.write(b"0"); handle.flush()
        handle.seek(0)
        try:
            msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
        except OSError as exc:
            raise RuntimeError("別のジョブが実行中です") from exc
        yield
    finally:
        try:
            handle.seek(0)
            msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
        except OSError:
            pass
        handle.close()
```

### Linux / macOS

`msvcrt` の2行だけ差し替えます。

```python
import fcntl

fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)   # 取得
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)                    # 解放
```

### 呼び出し側

```python
EXIT_LOCKED = 75   # 別ジョブ実行中。異常ではない

def main() -> int:
    try:
        with named_lock(Path("locks/job.lock")):
            return do_work()
    except RuntimeError:
        print("[skip] 別ジョブが実行中のため今回はスキップします")
        return EXIT_LOCKED
```

終了コード75を「失敗」ではなく「スキップ」として集計してください。
混ぜると、あとから「ロックの取り合いが何回起きたか」が数えられなくなります。

---

## 5. 自作ガードを入れるときの注意

危険な操作を止めるガードを入れる場合、**スクリプトは相対パスで呼ばないでください。**

ガードが対象スクリプトの中身を読んで判定する設計だと、相対パスはガード側の作業ディレクトリを
基準に解決されて読めません。読めないときに安全側へ倒す設計なら、正常な処理がブロックされます。

- 呼び出しは `python C:\full\path\to\script.py` のようにフルパスで書く
- ガードを緩めるのではなく、呼び出し方を直す

---

## 6. WSLに移すべきかの判断

次のいずれかに当てはまるなら、**Windowsのままのほうが安く済みます。**

- ファイルロックにOS固有のAPIを使っている
- `.cmd` / `.bat` のラッパーからジョブを起動している
- Windows側のGUIアプリや、ドライブ同期フォルダのファイルを触る

判断は「どちらが優れているか」ではなく、**上の3つに当てはまる箇所が自分のコードに何箇所あるか**で
決めてください。参考値として、当メディアは `msvcrt` 3箇所と `.cmd` 5本の合計8箇所でした。

---

作成: 株式会社PENGIN / AIディア編集部
