Claude Codeのsed -iがmacOSで失敗する理由 — BSDとGNUの書き分け
macOSのsedはBSD系で、-iの直後に接尾辞を必ず取ります。GNU形式のsed -iが失敗する仕組みと、環境判定の関数・perlへの置き換え・PreToolUseフックで止める設定をまとめます。
macOSでClaude Codeに設定ファイルの一括置換を頼むと、sed -i 's/foo/bar/' fileが失敗することがあります。原因はClaude Codeの不具合ではなく、macOS標準のsedがBSD系で、LinuxのGNU sedと-iの引数の取り方が違うことです。GNU形式で書かれたコマンドは、そのままではmacOSで通りません。
このページでは、失敗の型を実際の出力で示し、環境を判定して書き分ける関数、どちらでも動く代替、Claudeが書いたGNU形式をフックで止める設定までを順に並べます。
失敗の正体はClaude Codeではなくsedの方言差
-iは「ファイルをその場で書き換える」オプションです。同じ名前でも、引数の扱いがBSD sedとGNU sedで違います。
GNU sedのマニュアルでは、-i[SUFFIX]と書かれ、接尾辞は省略できます。接尾辞を付けるなら-i.bakのように-iへ直接つなぎます。マニュアルは「-iは省略可能な引数を取るので、ほかの短いオプションを続けない」とも述べ、sed -iE '...' FILEはEを接尾辞として扱うと説明しています。
macOS標準のsedは、-i extensionと書かれた使用法を表示し、-iの次の引数を必ず接尾辞として読みます。バックアップが不要なら空文字列を渡します。つまりsed -i ''と書くのがBSD式で、sed -iだけで終わる書き方はGNU式です。
Claude Codeは、ログインシェルの設定を読み込んだうえで、利用者のマシン上でBashコマンドを実行します。tools-referenceには、起動時に~/.zshrc・~/.bashrc・~/.profileのいずれかを読み込み、エイリアスやシェル関数を各コマンドに適用するとあります。Claudeが書いたsed -iも、動くのはmacOSの/usr/bin/sedです。モデルが何を想定して書いたかに関わらず、実行環境のsedが解釈します。
macOSで起きる症状を型で見分ける
macOS標準のsedで、次の4つを実行して比べた結果です。作業用の一時ディレクトリ内のa.txtとb.txtだけを対象にしました。
| 書いたコマンド | 結果 | なぜそうなるか |
|---|---|---|
sed -i 's/foo/baz/' a.txt | 結果エラーで終了、ファイルは未変更 | なぜそうなるかs/foo/baz/が接尾辞、a.txtがスクリプトとして解釈される |
sed -i -e 's/foo/zzz/' b.txt | 結果終了コード0、置換は成功、b.txt-eが残る | なぜそうなるか-eが接尾辞になり、バックアップが作られる |
sed -i '' 's/foo/baz/' a.txt | 結果成功 | なぜそうなるか空の接尾辞はバックアップなしの指定 |
sed -i.bak 's/baz/qux/' a.txt | 結果成功、a.txt.bakが残る | なぜそうなるか接尾辞を-iに連結した形 |
1行目のエラーはcommand a expects \ followed by textのような、一見sedと無関係に見える文面になります。a.txtの先頭のaが、行を追加するsedのaコマンドとして読まれるためです。ファイル名次第でメッセージが変わるので、文面から原因をたどるのは難しくなります。
2行目は特に厄介です。終了コードは0で、置換も成功します。Claudeはエラーなしと受け取り、作業を続けます。気づくのは、あとでgit statusにb.txt-eのような見慣れないファイルが並んだときです。
# 症状 2 の後始末: 末尾が -e のバックアップが残っていないか確認する
git status --short | grep -e '-e$'どちらでも動く書き方は3通りある
GNU sedとBSD sedの両方で同じ結果になる書き方は、次の3つです。
可搬性の高い順に選ぶ
- 1
接尾辞を付けて、あとで消す
sed -i.bak 's/foo/bar/' file && rm file.bakと書きます。-i.bakは接尾辞が-iに連結されているので、BSDでもGNUでも同じ意味です。macOSでの動作は上の表のとおりです。GNU側の挙動はマニュアルの「接尾辞は追記される」という記述に沿っています。 - 2
perlに置き換える
perl -pi -e 's/foo/bar/' fileと書きます。macOSにはperlが標準で入っており、-piはmacOSでもLinuxでも同じ書き方です。hooksのリファレンスにも、行末のCRを取り除く例としてperl -pi -e 's/\r$//' "$FILE"が載っています。 - 3
実行環境で分岐する関数を使う
次の節の関数を、スクリプトか
CLAUDE.mdに置きます。複数の置換を含むシェルスクリプトを書き直すより、呼び出し側を1つの関数にそろえるほうが差分が小さくなります。
どれを選ぶかは、書き換える対象の数で決まります。1回限りの手作業なら接尾辞付きが手軽です。スクリプトとして残るものは、perlか関数のどちらかに寄せます。
perlを選ぶときの落とし穴
perlの-iは、置換が1か所も起きなくてもファイルを書き直します。タイムスタンプが2020年のファイルに、マッチしないperl -pi -e 's/zzz/y/'をかけると、更新時刻が現在に変わりました。
hooksのリファレンスも、FileChangedフックの例で同じ点を警告しています。grepで変更の必要がある場合だけperlを呼ぶ守りを入れ、そうしないと、フックが書き換えのたびに再実行されて無限ループになると説明しています。ファイルの更新時刻でビルドやウォッチャーが動く環境では、perlを無条件にかけない書き方にします。保存のたびに整形を走らせるPostToolUseフックの組み方は、PostToolUse hookのLint自動修正にまとまっています。
実行環境を判定して書き分ける
GNU sedは--versionを受け付けます。macOS標準のsedに同じ引数を渡すと、illegal option -- -と使用法が表示されて失敗します。この差を判定に使えます。
# GNU sed なら -i、BSD sed なら -i '' で呼び分ける
sed_i() {
if sed --version >/dev/null 2>&1; then
sed -i "$@"
else
sed -i '' "$@"
fi
}
sed_i -e 's/foo/baz/' a.txtmacOSで上の関数を定義して実行すると、終了コード0で置換され、ディレクトリに余計なファイルは増えません。uname -sでDarwinかどうかを見る方法もありますが、GNU版のsedをPATHの先頭に置いたMacでは、OS名と実際のsedが食い違います。コマンドの挙動で判定するほうが、その食い違いを避けられます。
gsedを使う選択肢
GNU版のsedをMacに入れた環境では、gsedという名前で呼べることがあります。gsed -i 's/foo/bar/' fileと書けば、GNU形式のまま動きます。gsedはmacOSに標準では含まれず、GNU版を別途入れた環境でだけ使えます。チームで共有するスクリプトにgsedを前提にすると、入れていないメンバーの環境で失敗します。共有するものは、perlか関数のどちらかに寄せるのが無難です。
ClaudeのGNU形式をPreToolUseフックで止める
指示で「macOSではsed -i ''を使って」と伝えても、コマンドの組み立てのたびに守られる保証はありません。確実に止めたいなら、PreToolUseフックでBashのコマンドを検査し、GNU形式のときだけ拒否します。
hooksのリファレンスでは、PreToolUseフックがpermissionDecisionに"deny"を返すと、ツール呼び出しが止まり、permissionDecisionReasonがClaudeに見える形で渡されると説明されています。またifフィールドにBash(rm *)のような条件を書くと、該当するサブコマンドがあるときだけフックが起動します。この仕組みをsedに当てはめます。
設定は.claude/settings.jsonに置きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(sed *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-gnu-sed-i.sh"
}
]
}
]
}
}スクリプトは.claude/hooks/block-gnu-sed-i.shとして保存し、chmod +xで実行権限を付けます。JSONの解析にjqを使うので、PATHにあることを確かめてください。
#!/bin/bash
# macOS でだけ、GNU 形式の sed -i を拒否する
CMD=$(jq -r '.tool_input.command')
if [ "$(uname -s)" = "Darwin" ] \
&& echo "$CMD" | grep -Eq "(^|[;&|] *)sed +(-[A-Za-z]*i[A-Za-z]*|--in-place)( |$)" \
&& ! echo "$CMD" | grep -Eq "sed +-i *(''|\"\"|\.[A-Za-z]+)"; then
jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",
permissionDecision:"deny",
permissionDecisionReason:"macOS の sed は -i の直後に接尾辞が必要です。sed -i '"''"' か perl -pi -e を使ってください"}}'
fi手元で入力を変えて動かすと、次の結果になりました。
| Claudeが送ったコマンド | フックの判定 |
|---|---|
sed -i 's/a/b/' x | フックの判定拒否 |
sed -i -e 's/a/b/' x | フックの判定拒否 |
cd d && sed -i 's/a/b/' x | フックの判定拒否 |
sed -i '' 's/a/b/' x | フックの判定通す |
sed -i.bak 's/a/b/' x | フックの判定通す |
sed -n 1p x | フックの判定通す |
拒否の理由はClaudeに渡るので、次の試行でsed -i ''かperl -pi -eに直されます。フックは文字列の照合で判断する簡易版です。パイプの先やサブシェルの中に隠れたsed -i、xargs経由の呼び出しまでは拾えません。取りこぼしが問題になる環境では、Claudeに渡すコマンドの書き方自体をそろえる運用と併用します。
終了コード2で止め、stderrに理由を書く形でも、ツール呼び出しは止まり、理由はClaudeに渡ります。JSONで返す形にすると、permissionDecisionのdeny以外の値(askなど)にも同じ書式で切り替えられます。
CLAUDE.mdに1行書いて、そもそも誤りを減らす
フックは最後の砦です。手前で防ぐには、CLAUDE.mdにコマンドの書き方を決めておきます。ログインシェルがzshの環境でbash流の構文が失敗する問題は、Claude Codeがzshでbash構文を書いて失敗する原因と対処でも扱っており、同じCLAUDE.mdに並べて書けます。
## シェルコマンドの規約(macOS)
- ファイルのその場置換は `perl -pi -e` を使う。`sed -i` は使わない
- どうしても sed を使うときは `sed -i.bak ... && rm 対象.bak` と書く
- GNU 拡張(`\U` `\L`、`\+` など)を sed の置換で使わない最後の項目は-i以外の差から来ています。
-i以外にもあるBSDとGNUの違い
macOS標準のsedで確かめた、-i以外のつまずきを3つ挙げます。
\+が量指定子にならない。printf 'aaa\n' | sed 's/a\+/X/'のように基本正規表現で\+を書いても、aaaのまま変わりませんでした。-Eを付けてs/a+/X/と書くとXになります\Uが大文字化にならない。置換側の\U&は、大文字化ではなく文字Uとして扱われ、AaがAUaになりました- 1行の
iコマンドが通らない。sed -i '1i hello' fileはcommand i expects \ followed by textで失敗します。i\のあとで改行してhelloを書く形なら通ります
これらは-iの書き分けと違い、エラーにならず結果だけが変わるものがあります。置換の結果を目で見ずに「成功した」と判断しないよう、git diffで差分を読む習慣をCLAUDE.mdにも書いておくと安全です。
ここで示した結果は、あくまでmacOS(Darwin 25.3.0)標準のsedでの出力です。手元のバージョンで同じコマンドを試してから判断してください。
権限ルールはsed経由の書き換えにも効く
sed -iはファイルを書き換えるので、権限ルールにも関わります。permissionsのドキュメントによると、ReadとEditのdenyルールは、組み込みのファイルツールだけでなく、Bashの中で認識されるcat・head・tail・sed・teeにも適用されます。Editのdenyで.envへの書き込みを止めている設定なら、sed -i経由の書き換えも同じルールで止まります。
もう1点、sedのように書き込み可能なフラグを持つコマンドは、クォートなしのglobがあると確認が出ます。globが-deleteのようなフラグに展開されうるためです。sed -i '' 's/a/b/' src/*.tsと書くと、承認待ちで止まる場面があります。対象をgit grep -lで絞り、ファイル名を明示して渡す形なら、この確認を避けられます。置換の影響範囲を実行前に見える形にする意味でも、扱いやすい書き方です。
MacとLinuxを併用するチームの選び方
書き分けの結論は、コマンドが残る場所で変わります。手元のMacで1回きり実行する置換なら、sed -i ''でも困りません。リポジトリに残るスクリプトやCIで動くコマンドは、perl -pi -eかsed -i.bak ... && rmにそろえるのが安全です。
理由は、sed -i ''がGNU版で壊れるからです。GNU版の-iは引数を取らないため、続く空文字は接尾辞でなくsedのスクリプトとして読まれます。後ろの's/a/b/'はファイル名の扱いになり、Linux側で別の形のエラーになります。BSDとGNUのどちらでも同じ意味になるのは、接尾辞を-iにつなげる形だけです。
まとめ
sed -iの失敗は、モデルの腕前ではなく、実行環境のsedが何者かで決まります。1回限りの置換ならsed -i.bak ... && rm、残るスクリプトならperlか判定関数、Claudeにも守らせたいならPreToolUseフックとCLAUDE.mdの組み合わせが、この記事で試した範囲で最も確実でした。
特に危ないのは、エラーにならずにb.txt-eを残すsed -i -eです。コミット前にgit statusで見慣れないファイルを確かめるだけでも、被害は小さくできます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。