Claude Media
CLAUDE_CODE_SHELL_PREFIXでBashやhookをラッパー経由にする方法

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 --version

Bashツールの$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_PREFIXCLAUDE_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の起動で動作を確かめる流れになります。

この記事を共有:XはてブLinkedIn