CLAUDE_CODE_SHELL_PREFIXでBashやhookをラッパー経由にする方法
CLAUDE_CODE_SHELL_PREFIXでBashツール・hook・ステータスライン・stdio MCPの起動コマンドを包む設定と、$1を再評価するラッパーの書き方、対象外の経路をまとめます。
CLAUDE_CODE_SHELL_PREFIXは、Claude Codeが起動するシェルコマンドの前にラッパーを挟む環境変数です。Bashツールの呼び出しだけでなく、hook、ステータスライン、stdio方式のMCPサーバー起動コマンドも対象になります。用途として想定されているのは、コマンドの記録や監査です。
落とし穴は1つに集約できます。ラッパーは実行したいコマンドを$1の1引数で受け取るので、$1をシェルで再評価しないと動きません。
CLAUDE_CODE_SHELL_PREFIXは何を包む変数か
この変数に値を入れると、Claude Codeが生成するシェルコマンドは、そのまま実行されずに指定したラッパーを経由します。包まれる経路と、包まれない経路は次のとおりです。
| 経路 | ラッパーを通るか |
|---|---|
| Bashツールの呼び出し | ラッパーを通るか通る |
| 設定したhookのコマンド(shell form) | ラッパーを通るか通る |
| ステータスラインのコマンド | ラッパーを通るか通る |
| stdio方式のMCPサーバーの起動コマンド | ラッパーを通るか通る |
| PowerShell hook | ラッパーを通るか通らない |
exec form(argsを指定した)hook | ラッパーを通るか通らない |
ここでいうexec formは、hook定義にargsを書いてシェルを介さず直接実行する形式です。argsを省いた従来のshell formとは別物で、後者だけがラッパーの対象です。
値の渡され方も決まっています。/path/to/logger.shのような実行ファイルのパスだけを設定すると、各コマンドは次の形で呼ばれます。
/path/to/logger.sh '<command>'コマンド全体がシェルでクォートされた1つの引数として、ラッパーの$1に入ります。
最小のラッパーは「記録して、bash -cで流す」
動くラッパーの最小形は2行です。$1をログに追記し、exec bash -c "$1"で元のコマンドを実行します。
#!/bin/bash
printf '%s\n' "$1" >> "$HOME/.claude/shell-audit.log"
exec bash -c "$1"実行権限を付けて、設定ファイルのenvブロックに書きます。
chmod +x ~/bin/shell-audit.sh{
"env": {
"CLAUDE_CODE_SHELL_PREFIX": "/Users/you/bin/shell-audit.sh"
}
}envの値はシェルを通さずそのままプロセス環境に入ります。~や$HOMEは展開されないので、パスは絶対パスで書きます。ラッパー側のスクリプトの中で$HOMEを使うのは問題ありません。そちらは通常のbashスクリプトだからです。
execを付けているのは、ラッパー自身のプロセスを残さず、元のコマンドに置き換えるためです。終了コードもそのまま呼び出し元に返ります。ログの出力先を変えたり、タイムスタンプを足したりする改造は、この3行の間に入れるだけで済みます。
実運用では、時刻と作業ディレクトリを足したくなります。$1に触れずに前後へ行を足すだけなので、再評価の条件は崩れません。
#!/bin/bash
LOG="$HOME/.claude/shell-audit.log"
printf '%s\t%s\t%s\n' "$(date +%FT%T%z)" "$PWD" "$1" >> "$LOG"
exec bash -c "$1"ログには、コマンド行に書かれた文字列がそのまま残ります。トークンをコマンドの引数に直接書く運用があると、その値もファイルに入ります。ログファイルの権限はchmod 600で自分だけに絞り、保管期間と肥大化への対処(ローテーション)も最初に決めておくと後で困りません。
$1を実行ファイルとして扱うとMCPが起動しなくなる
よくある間違いは、$1をそのまま実行しようとするラッパーです。
#!/bin/bash
exec "$1"$1にはnpx -y some-packageのような、空白を含むコマンド行全体が入ります。exec "$1"は、その全体を1つの実行ファイル名として探すので、見つかりません。手元のmacOS(bash 3.2)で./bad-wrap.sh 'npx -y echo-pkg hello'と呼ぶと、次のように失敗しました。
./bad-wrap.sh: line 2: exec: npx -y echo-pkg hello: not found終了コードは127です。同じラッパーをbash -c "$1"に直すと、npxが引数付きで実行されます。env-varsのページにも、$1を実行ファイルのパスとして扱うと、npx -y <package>のように引数を渡すstdio MCPサーバーが壊れるという記載があります。
MCPの起動で失敗するときは、ラッパーが$1をシェルに渡しているかを先に疑います。
引数に空白や記号が入るときの修正
引数に空白やシェルのメタ文字を含むMCPサーバーでは、引数が壊れた状態で届く不具合がありました。changelogでは、v2.1.128で「CLAUDE_CODE_SHELL_PREFIXが設定され、引数に空白やシェルのメタ文字があるとき、stdio MCPサーバーに壊れた引数が渡される問題」が修正されています。それより古いバージョンでMCPの引数が化ける場合は、ラッパーよりも先にバージョンを確認します。
claude --versionBashツールの$1は、実行したコマンドより長い
監査ログに残したいのは「モデルが何を実行したか」ですが、Bashツールで$1に入るのは、それだけではありません。env-varsのページは、Bashツールの呼び出しでは$1が、Claude Codeが組み立てたシェル呼び出し全体、つまり環境のセットアップを含む文字列であると説明しています。
ログに出る行は、手で打ったコマンドよりも長く、前後に付随する処理が混ざります。ここから実行内容だけを取り出したいなら、ログの後段でパースする処理が要ります。逆に、実際にシェルへ渡った文字列をそのまま残したい用途なら、この性質は都合がよい面もあります。
hookとステータスラインの場合は、設定したcommandの文字列が対象です。ログの行がどの経路から来たかは$1だけでは区別できないため、経路ごとに分けたいときは、ラッパーの中で$PPIDや親プロセスの情報を足すなど、ラッパー側の工夫が必要になります。
ラッパーが壊れると、包まれた経路がすべて止まる
Bashツール、shell formのhook、ステータスライン、stdio MCPのすべてが同じラッパーを通るので、ラッパー自体の不具合は全経路に波及します。ログの書き込み先が消えた、ディスクが満杯になった、といった理由でprintfが失敗しても、その後ろのexecまで到達させる書き方にしておくと、記録の欠落だけで済みます。
printf '%s\n' "$1" >> "$LOG" 2>/dev/null || true記録を落とすより実行を止めたい監査要件なら逆に|| exit 1とします。どちらを採るかはラッパーを書く時点で決めておきます。
どこに設定するか
設定場所は、シェルのexportと、設定ファイルのenvブロックの2通りです。envを使う場合は、ファイルによって効く範囲が変わります。
| 設定場所 | 効く範囲 |
|---|---|
~/.claude/settings.json | 効く範囲自分の全プロジェクト |
.claude/settings.json | 効く範囲プロジェクトの全員(リポジトリに入る) |
.claude/settings.local.json | 効く範囲自分のこのプロジェクトだけ |
| 管理設定(managed settings) | 効く範囲組織の全員 |
監査用途なら、管理設定に置くのが筋です。同じ変数が複数のファイルにあるとき、管理設定の値が優先されます。ユーザーが自分の設定でラッパーを書き換える余地を減らせます。
プロジェクトの.claude/settings.jsonに置くとリポジトリに入るため、パスはチーム全員の環境で同じ場所に存在する必要があります。個人のホームディレクトリを指す絶対パスをコミットすると、他のメンバーの環境では実行ファイルが見つからず、コマンドが動かなくなります。
シェルのexportで渡す場合は、claudeを起動する前に設定します。シェルの環境変数は起動時に読まれるので、既に動いているセッションには反映されません。
ラッパーを通らない経路があるので、網羅を前提にしない
ログに全コマンドが残ると考えると、見落とします。ラッパーを通らないことが明記された経路が2つあります。
- PowerShell hook: PowerShellで動くhookは、プレフィックスなしで実行される
- exec formのhook:
argsを指定して直接起動するhookも、プレフィックスなしで実行される
hookをshell formに揃えるには、hook定義からargsを外し、commandにコマンド行を書きます。exec formの定義は、commandが実行ファイル、argsが引数の配列という形です。
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/check.js"]
}これをshell formに直すと、次のようにクォート付きの1行になります。
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/check.js\""
}shell formではパス内の空白をクォートで守る責任が自分に戻ります。hookのドキュメントも、パスのプレースホルダを使うhookにはexec formを勧め、shell formならプレースホルダを二重引用符で囲むよう書いています。記録の網羅とクォートの安全性は両立しにくいので、どのhookをどちらに置くかは、監査の要件を見て決める判断になります。
つまり、CLAUDE_CODE_SHELL_PREFIXのログは、Claude Codeが起動した全プロセスの記録ではありません。hookの定義にexec formが混ざっていれば、その実行は記録から抜けます。網羅的な監査が目的なら、hookの定義側でshell formに揃えるか、別の手段で記録を補う設計が必要です。
もう1つ、役割の違いも押さえておきます。この変数のページが示す用途はloggingとauditingで、コマンドを隔離する仕組みではありません。コマンドの実行範囲を制限したい場合は、sandbox.enabledのようなOSレベルの隔離を別に用意します。
別の変数CLAUDE_CODE_PROCESS_WRAPPERとの違い
名前の似たCLAUDE_CODE_PROCESS_WRAPPERは、別の仕組みです。こちらは、Claude Codeが自分のバイナリから起動するプロセス(エージェントビューのセッションを抱えるバックグラウンドサービスなど)を、企業のランチャー経由で起動します。設定はenvブロックに書き、Windowsでは無視されます。起動エラーの切り分けはCLAUDE_CODE_PROCESS_WRAPPERの起動エラーで扱っています。
| 比較軸 | CLAUDE_CODE_SHELL_PREFIX | CLAUDE_CODE_PROCESS_WRAPPER |
|---|---|---|
| 包む対象 | CLAUDE_CODE_SHELL_PREFIXClaude Codeが起動するシェルコマンド | CLAUDE_CODE_PROCESS_WRAPPERClaude Code自身が起動するプロセス |
| 渡し方 | CLAUDE_CODE_SHELL_PREFIXコマンド全体が$1に入る | CLAUDE_CODE_PROCESS_WRAPPERargvの接頭辞として付く |
| 主な用途 | CLAUDE_CODE_SHELL_PREFIXログ・監査 | CLAUDE_CODE_PROCESS_WRAPPER企業が求める必須ランチャーの経由 |
動かないときの切り分け
症状とラッパー側の原因を、対応づけて並べます。
| 症状 | 疑う点 |
|---|---|
| stdio MCPサーバーが起動しない | 疑う点ラッパーが$1をシェルで再評価していない |
コマンドがnot foundで終わる(終了コード127) | 疑う点exec "$1"のように、$1を実行ファイル名として扱っている |
| MCPの引数が空白の位置で壊れる | 疑う点v2.1.128より古い。バージョンを更新する |
| ラッパーが一切呼ばれない | 疑う点設定ファイルのenvの綴り、実行権限、絶対パスの3点を確認する |
| 一部のhookだけログに出ない | 疑う点そのhookがexec formかPowerShellで、対象外の経路になっている |
| Bashのログに環境セットアップが混ざる | 疑う点仕様。$1が組み立て後の呼び出し全体を含む |
切り分けの最短手順は、ラッパーの先頭にprintf '%s\n' "$1" >> /tmp/prefix-debug.logを1行足すことです。ログに何も出ないなら、そもそも呼ばれていません。出るなら、中身を見て$1がどう渡っているかを確かめられます。
導入時は、Bashツールで簡単なコマンドを1つ実行させ、次にMCPサーバーを1つ接続し直す順で確かめます。Bashだけが動いてもMCPで壊れるラッパーは、$1の再評価が抜けている可能性が高く、先に引数付きのMCP起動で試すと問題が早く見つかります。
なお、Bashツールが使うシェル自体を切り替えたい場合は、別の変数CLAUDE_CODE_SHELLがbashまたはzshのパスを受け付けます。プレフィックスの有無とは独立した設定です。
まとめ
CLAUDE_CODE_SHELL_PREFIXは、exec bash -c "$1"を守れば小さなスクリプトで動く、ログ向けの仕組みです。ただし、PowerShell hookとexec formのhookはラッパーを通らないので、これだけで全実行の網羅記録にはなりません。組織で使うなら、管理設定に置き、hookの定義形式をそろえたうえで、MCPの起動で動作を確かめる流れになります。