AI

Claude Code の Auto mode では Edit ツールが使われなくなる

  • Claude Code
  • hooks
  • Bash
  • Auto mode

Claude Code の Auto mode では、ファイルの読み書きを Bash で済ませるよう指示するブロックがシステムプロンプトに入ります。これは A/B 実験のフラグで出し分けられていて、設定キーもドキュメントもありません。settings.json の env に次の1行を置くと消えます。

{
  "env": {
    "CLAUDE_CODE_THRIFTY_SONIC": "0"
  }
}

この記事では、この指示に気づいた経緯、壊れていたのが sed -i を止めるフックとの矛盾だけではなかったこと、--settings で3通りを試した結果、同じ層にある git の指示を外した話を書きます。

フックを入れた直後に、サブエージェントが編集のたびに親へ聞き返すようになった

前の記事で、Bash 向けの permissions.deny を PreToolUse フックへ移しました。sed -i を止めて、Edit ツールを使うようにメッセージで伝える作りです。

ブログPreToolUseフックで代替手段まで返すClaude Codeのpermissions.denyはブロックするだけで理由を返せないため、代替手段の探索をモデルに任せることになり、そのぶんトークンを使います。Bash向けのdenyをPreToolUseフックに移し、exit 2でstderrに書いた文を返すことで、ルールごとに止めた理由と代替手段を伝えられるようにしました。プレフィックス一致では見えなかった回避経路と、止める範囲を引き直した基準も書きます。

入れた直後から、サブエージェントが編集のところで迷うようになりました。フックに止められたあと Edit へ切り替えるのではなく、親エージェントに「sed -i を使えという指示と Edit を使えという指示のどちらが優先か」と聞き返して、返事を待ちます。

親は「フックが代替手段として示した方に従う」と返して進めさせるので、作業が止まりきるわけではありません。ただ、子が悩んで親に聞き、親が答えて子が動き直す、という本来要らない往復が、編集のたびに発生していました。

sed -i を使えという指示は、こちらの設定のどこにもありません。CLAUDE.md にも、スキルにも、フックのメッセージにもない文が、サブエージェントには確かに届いていました。

Auto mode のシステムプロンプトが Bash での編集を指示している

出どころは Auto mode でした。Auto mode system prompt causes /rewind to silently fail on Bash-edited files に、Auto mode のセッションでシステムプロンプトに入るブロックがそのまま引用されています。

While auto mode is active: Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.

anthropics/claude-code#87575

サブエージェントに届いていたのはこの文です。フックは sed -i を止めて Edit を案内し、システムプロンプトは Edit ではなく sed を使えと言っています。どちらもモデルにとっては従うべき指示なので、決めかねて親に聞くのは挙動として筋が通っています。

Choose a permission mode には Auto mode の説明がありますが、このブロックのことは書かれていません。Settings reference にも、これを切る設定キーはありません。

矛盾している2つの指示は、同じ層にはありません。フックは自分の settings.json に書いたものなので、直せます。システムプロンプトのブロックは Claude Code 本体が入れるもので、設定には触る場所がありませんでした。

矛盾する2つの指示の出どころ。上の層はシステムプロンプトで、Auto mode の Claude Code 本体が sed やヒアドキュメントで書けと入れる。下の層は自分の settings.json の PreToolUse フックで、sed -i を止めて Edit ツールを使えと返す。間にいるサブエージェントは決めかねて親エージェントに聞き、親がフックの代替手段に従えと答える往復が、編集のたびに起きる

anthropics/claude-code#88041 の報告者が CLI の本体を読んで、bashFirst というフラグが内部のゲートで決まっていて、設定キーも環境変数もないと書いています。

手元の 2.1.241 で同じ場所を確認すると、環境変数を見る分岐はありました。このブロックを出すかどうかは、環境変数 CLAUDE_CODE_THRIFTY_SONIC が設定されていればその値で決まり、設定されていなければ tengu_thrifty_sonic という実験フラグのコホート割り当てに落ちます。

つまり、何も設定していない状態は「切っている」ではなく「割り当て次第」です。今日出ていなくても、明日出る可能性があります。

壊れていたのはフックとの矛盾だけではなかった

フックと矛盾するだけなら、フックのメッセージを直せば済む話です。実際には、Edit と Write を前提に組んである仕組みが、まとめて空振りしていました。

空振りしていたのは、自分の設定にある PostToolUse フック、/rewind のチェックポイント、pre-commit のチェックが集める変更ファイル一覧の3つです。

同じファイル変更を Edit や Write で書いた場合と sed やヒアドキュメントで書いた場合の2経路。Edit|Write|MultiEdit の PostToolUse フック、/rewind のチェックポイント、Edit|Write で集めた変更ファイル一覧の3つは前者にだけ反応し、後者では3つとも空振りしてエラーも出ない

まず自分の設定です。Swift ファイルを編集するたびに swift-format を掛ける PostToolUse フックを入れてあります。

PostToolUse = [
  {
    matcher = "Edit|Write|MultiEdit";
    hooks = [
      {
        type = "command";
        command = "jq -re '.tool_input.file_path | select(endswith(\".swift\"))' | xargs xcrun swift-format --in-place";
        timeout = 10;
      }
    ];
  }
];

matcherEdit|Write|MultiEdit なので、sed やヒアドキュメントで書かれたファイルには何も起きません。エラーも出ず、フォーマットされていないファイルがそのまま残ります。

自分の設定の外でも、同じ原因の報告が2件ありました。ひとつは /rewind です。Checkpointing は Bash コマンドによる変更を追跡しないと明記しています。

Checkpointing does not track files modified by bash commands.

Checkpointing

Issue の報告者は、却下した変更を /rewind で戻したつもりで作業を続け、あとからコードがそのまま残っていたことに気づいています。/rewind は成功したと表示し、ファイルは変わらないままでした。

もうひとつは pre-commit のチェックです。別の報告者が、Lint と型チェックとテストの一式を "matcher": "Edit|Write" のフックで集めた変更ファイル一覧に対して走らせる構成で確かめています。

The failure is silent in the worst way: the battery exits green, because grading zero files is indistinguishable from grading everything.

anthropics/claude-code#87575 (comment)

Bash で書かれたファイルは一覧に入らないので、チェック対象が0件になります。0件をチェックして全部通るのと、全部をチェックして全部通るのは、結果だけ見ると区別がつきません。

書く側だけではありません。読み取りの deny も同じ構図です。.env*.pemRead(**/.env) のような deny で止めてあり、Configure permissions によれば、この denycatheadsed のような Bash のファイルコマンドにも効きます。効かないのは、そこから先です。

They don’t apply to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself.

Configure permissions

Auto mode の指示は short scripts も推奨しています。cat で止まれば、次は短いスクリプトで開こうとします。止めたい対象は変わっていないのに、止まらない経路へ誘導している形です。

ここまで並べると、壊れ方に共通点があります。どれもエラーを出しません。フックは発火せず、/rewind は成功と言い、チェックは通ったと言い、deny は別経路で抜けられます。

矛盾に気づいて聞き返してきたサブエージェントだけが、唯一こちらに見える形で壊れてくれていました。

settings.json の env"0" に固定した

切り方は同じ Issue のコメントにありました。

for anyone in the future reading this CLAUDE_CODE_THRIFTY_SONIC=0 claude disables this A/B experiment that anthropic is doing for this undocumented “feature”.

anthropics/claude-code#87575 (comment)

シェルで毎回付けるのではなく、settings.json の env に入れました。Environment variables によれば、env の値は起動時にプロセス環境へ書き込まれ、サブプロセスにも届きます。

サブエージェントで起きた問題なので、サブプロセスまで届くことが条件でした。設定は Nix で管理しているので、書いたのはこの形です。

programs.claude-code.settings.env = {
  CLAUDE_CODE_THRIFTY_SONIC = "0";
};

効いているかは、値を変えた Settings ファイルを --settings で渡して確かめました。-p で1問だけ投げ、Auto mode の指示がコンテキストに入っているかを一語で答えさせます。

claude -p --permission-mode auto --settings "$TMPDIR/variant.json" "$QUESTION"

3通りの結果です。

env の値指示
未設定PRESENT
"1"PRESENT
"0"ABSENT

未設定で PRESENT なのは、このマシンがコホートに入っているからです。"1" は明示的に入れる値なので当然 PRESENT で、"0" だけで消えます。

別の報告者が 2.1.233 / Linux で、Settings ファイルを消した場合まで含めて、未設定なら PRESENT、"0" なら ABSENT という同じ結果を出しています。

確かめるときにひとつ引っかかりました。最初は CLAUDE_CODE_THRIFTY_SONIC=0 claude -p ... のようにプロセスの環境変数で渡したのですが、値を変えても結果が変わりません。原因はドキュメントに書いてありました。

When the same variable is set in both your shell and a settings file env block, the settings file value applies.

Environment variables

ユーザー設定の settings.json に同じ変数が入っていると、シェルから渡した値はそちらで上書きされます。

試したい値を Settings ファイルの側に書いて --settings で渡せば、Settings の優先順位でユーザー設定より優先されます。3通りの差が見えたのは、そこからです。

同じ層にある git の指示も外した

システムプロンプトを見ている流れで、もうひとつ外しました。Claude Code は組み込みのコミットと PR の手順、それに起動時の git status のスナップショットをシステムプロンプトに入れます。

Settings reference には includeGitInstructions というキーがあり、説明はこうです。

Remove the built-in commit and PR instructions from the system prompt

Settings reference

手元の 2.1.241 では、このキーより先に環境変数 CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS を見ています。値が 1 / true / yes / on なら指示を外し、0 / false / no / off なら入れ、どちらでもなければ includeGitInstructions に落ちて、それも無ければ入れる、という順です。

環境変数が常に勝つので、両方書く意味はありません。

programs.claude-code.settings.env = {
  CLAUDE_CODE_THRIFTY_SONIC = "0";
  CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS = "1";
};

外したのはプロンプトの層だけです。Bash から git を実行できることは変わらず、git commitgit pushask を付けてある設定もそのまま効きます。

コミットメッセージの書き方は自分の CLAUDE.md で指定しているので、組み込みの手順と二重に持つ理由がありませんでした。

確かめていないこと

コホートにどれだけの環境が入っているかは分かりません。手元のデータは 2.1.241 / macOS の1点で、Issue に載っている他の報告者も 2.1.233 と 2.1.234 の Linux です。設定していない環境のうちどれだけが指示を受け取っているのかは、この範囲からは言えません。分かっているのは、2.1.241 ではコホートの抽選に進むのがメインモデルを claude-opus-5 にしているときだけで、他のモデルではこのブロックが最初から出ない、というところまでです。

tengu_thrifty_sonic は Auto mode の指示だけのフラグではなく、実験そのものの名前です。"0" にすると同じ実験に入っている他の挙動もまとめて切れるはずですが、何が入っているかは確かめていません。Auto mode の指示が消えたことだけを確かめています。

それから、フックのメッセージはまだ直していません。止めた理由と代替手段を返す作りは、代替手段がシステムプロンプトと矛盾しない前提で書いてあります。

今回は矛盾の元を消す側で対処しましたが、コホートの割り当てが変われば同じことが起きるので、メッセージの側でも「システムプロンプトがどう言っていても Edit を使う」と言い切っておく手は残っています。