src の TODO を探して
grep -rn TODO src --include=*.ts
src/app.ts:12: // TODO retry on timeout
SafeZsh for Claude
Claude Code の Bash ツールで起きる no matches found などの zsh のエラーを防ぎます。Claude は bash で書き、Bash ツールは zsh で実行します。このプラグインは zsh にそのコマンドを bash と同じように読ませます。
このプラグインがないと、上のコマンドは no matches found: --include=*.ts で止まり、grep は実行されません。
インストール
Claude Code のターミナルのプロンプトで:
/plugin install safe-zsh --marketplace M1nu0x0/claude-safe-zshAdd marketplace? に y と答え、scope は user を選びます。
シェルからは:
claude plugin install safe-zsh --marketplace M1nu0x0/claude-safe-zshClaude Desktop では、プロンプト横の + → Plugins → Add marketplace に M1nu0x0/claude-safe-zsh を入力し、safe-zsh を追加します。次のセッションから適用されます。
前と後
$ grep -rn TODO src --include=*.ts
(eval):1: no matches found: --include=*.ts
$ echo =====; ls
(eval):1: ==== not found
$ T="a.py b.py"; pytest $T
ERROR: file or directory not found: a.py b.py$ grep -rn TODO src --include=*.ts
src/app.ts:12: // TODO retry on timeout
$ echo =====; ls
=====
README.md src
$ T="a.py b.py"; pytest $T
2 passed作った理由
macOS ではログインシェルが zsh なので Bash ツールも zsh でコマンドを実行しますが、Claude は bash の書き方でコマンドを書きます。
anthropics/claude-code#103027 は 12 週間で Bash の呼び出し 61,040 回のうち 2,570 回の zsh エラーを数えました。9 月下旬からは 1,000 回あたり 51 回で、その 76% はコマンドが 0 で終わったため、エラーが出力の途中に埋もれたまま Claude が読み進めていました。
この issue が Claude Code で修正されれば、このプラグインは不要になります。CI が毎日 issue の状態を確認しています。
Claude Code の安全チェックにも zsh の問題があります。${(flags)...} を解析できず、そうしたコマンドを rm の危険として拒否します(#104925)。これが修正されれば PreToolUse フックとその案内も外します。CI が毎日この issue の状態も確認しています。
動作
セッション開始時(startup、resume、clear、compact)に SessionStart フックが CLAUDE_ENV_FILE に 1 行を追記します。Claude Code はこのファイルを Bash ツールのコマンドごとに先頭に付けるので、その行が同じシェルで実行されます。
if [ -n "${ZSH_VERSION:-}" ]; then setopt no_nomatch sh_word_split no_equals; fibash では条件が偽になり 0 で終わるので、後に続くコマンドもそのまま実行されます。
シェルが zsh のとき、フックはモデルにもう 2 行伝えます。コマンドの中で起動した zsh -c はこれらのオプションを引き継がないので、そのスクリプトの先頭に setopt no_nomatch sh_word_split no_equals; を付けるか bash -c を使うようにという案内と、zsh の ${(flags)...} 展開は安全チェックが実行前に拒否するので printf '%s\n' "$PATH" | tr ':' '\n' のように POSIX sh で書くようにという案内です。モデルが従わないことがあります。
PreToolUse フックは、Claude Code の安全チェックより先に Bash ツールのコマンドを読みます。シングルクォートの外に ${(s/:/)PATH} のような zsh の ${(flags)...} 展開があれば拒否し、モデルに理由を伝えます。安全チェックが実行前に rm の危険として拒否するので、printf '%s\n' "$PATH" | tr ':' '\n' のように POSIX sh で書くようにという内容です。フックは何も承認せず、チェックを避けるためにコマンドを zsh -c で包むこともしません。
防げるもの
no_nomatch--include=*.tsやls libs/*/README.mdのように一致しない glob が、コマンドを止めずにそのまま渡されます。sh_word_split- クォートしていない
$varが単語に分割され、for f in $listが項目ごとに 1 回ずつ回ります。 no_equals=で始まる単語をそのままにします。echo =====が出力され、=lsが/bin/lsに変わりません。${(flags)...}print -l ${(s/:/)PATH}のような zsh の${(flags)...}展開が、的外れなdangerous rm operationsのメッセージで拒否されなくなります。フックが本当の理由とともに拒否し、モデルが POSIX sh に書き直します。
コロン修飾子($B:path)、添字として読まれる $pat[...]、特殊変数の path と status、${!v} や compgen のような bash 専用の書き方は防げません。その場合は CLAUDE_CODE_SHELL=/bin/bash で Bash ツールを bash で実行できます。$${ のように安全チェックが解析できない他の書き方は、引き続き rm のメッセージで拒否されます。
関連 issue
バッジは各 issue の現在の状態です。
- #103027
Claude が zsh で bash の書き方を使い続ける。このプラグインを作った理由。
- #67146
同じ問題の以前の報告。活動がなく閉じられた。
- #103576
単語分割されず pytest がテストを 1 つも実行せず、空の結果がそのまま報告された。
- #74888
Monitor ツールの例が bash の単語分割を前提にしている。
- #101554
zsh で bash の組み込みコマンドを実行した。このプラグインでは防げない。
- #74746
Bash ツールを bash で実行したいという要望。
CLAUDE_CODE_SHELLで閉じられた。- #7490
Bash ツールのシェルを選ぶか、Claude Code を起動したシェルを引き継ぎたいという要望。
- #91498
Bash ツールはログインシェルを実行するので、名前を変えるかシェルのエラーを知らせてほしいという提案。
- #52521
Bashツールの名前変更の提案。- #2444
Bash ツールが bash ではなく既定のシェルで実行されるという報告。
- #24573
bash ユーザーなのに Bash ツールが zsh を選ぶ。
- #104925
rmのないコマンドでも、zsh の${(flags)...}のように解析に失敗するとrmの危険として拒否される。PreToolUse フックを作った理由。- #99249
zsh -cのスクリプトも bash の文法で判断されているように見える。- #100419
PermissionRequest フックの入力に拒否の理由がなく、原因を区別できない。
- #99899
サブエージェントではこの承認画面に自動拒否の時間がなく、無期限に止まる。
よくある質問
ターミナルや ~/.zshrc は変わりますか?
いいえ。オプションは Bash ツールのシェルにだけ適用されます。ターミナル、dotfile、コマンドの中で起動した zsh -c には適用されません。最後の場合、フックはモデルに起動のしかたを伝えるだけです。
bash を使っています。何かしますか?
ほとんど何もしません。bash では setopt の行の条件が偽になり 0 で終わります。PreToolUse フックは zsh の ${(flags)...} 展開を含むコマンドを拒否しますが、そうしたコマンドは bash でも実行できません。
CLAUDE_CODE_SHELL=/bin/bash を使えばよいのでは?
それでも動き、より多くの問題が解決しますが、Bash ツールで自分の zsh の関数や設定が使えなくなります。このプラグインは zsh のまま、最もよく壊れる 3 つだけをオフにします。
どこかにデータを送りますか?
送りません。SessionStart フックは Claude Code から渡されたファイルに 1 行を追記し、モデルに向けて固定の 2 行を出力します。PreToolUse フックは入力の Bash コマンドを読み、zsh の ${(flags)...} 展開があれば固定の拒否メッセージを出力します。どちらもネットワークリクエストはしません。
どの OS で確認していますか?
Ubuntu と macOS で CI が動き、毎日最新の Claude Code でも確認しています。
削除するには?
シェルで claude plugin uninstall safe-zsh を実行します。
(eval):1: no matches found はなぜ出るのですか?
(eval) は、Bash ツールの zsh がコマンドを eval で実行していて出たエラーという意味です。zsh では一致するファイルがない glob はエラーになり、コマンド全体がそこで止まりますが、bash はその単語をそのまま渡します。このプラグインは zsh でも bash と同じようにそのまま渡します(no_nomatch)。ターミナルでは同じエラーが zsh: no matches found と表示されます。
==== not found とは?
zsh は = で始まる単語を、その名前のコマンドのパスに置き換えます(=ls は /bin/ls)。そのため echo ===== は ==== というコマンドを探して失敗します。区切り線として = を並べて出力するコマンドがこれに当たります。このプラグインはこの動作をオフにします(no_equals)。
rm がないのに、なぜ could not be checked for dangerous rm operations で拒否されるのですか?
Claude Code の安全チェックはシェルが実行する前にコマンドを解析しますが、その bash の文法には ${(s/:/)PATH} のような zsh の ${(flags)...} 展開がありません。解析できないコマンドは、rm がなくても rm の危険として拒否されます(#104925)。zsh が起動すらしないので、setopt では防げません。このプラグインの PreToolUse フックが先に拒否し、printf '%s\n' "$PATH" | tr ':' '\n' のように POSIX sh で書くようモデルに伝えます。