エージェントごとに持たせる道具を変えることも、Claude Code をヘッドレスで何本も起こして仕事を分担させる技術検証で試したことの1つでした。たとえば、レビューをするエージェントには、自分では直せず指摘だけをさせるため、Bash を渡さないことにしました。私たちはこれを「--allowedTools に Bash を入れない」ことで実現したつもりでした。しかし測ってみると、レビューのエージェントは Bash を使っていました。

結論

  • エージェントは、--allowedTools に無い道具も使いました。--allowedTools は、使わせない境界にはなりませんでした。
  • --disallowedTools は効きました。しかも、そのエージェントが起こしたサブエージェントにも引き継がれました。
  • 設定の permissions.deny はフックより手前で効き、フックの allow では覆せませんでした。フックの deny は、--allowedTools での許可に勝ちました。
  • 使わせたくない道具は、--disallowedTools で名前を挙げて外すことにしました。

試した環境

  • Mac、Claude Code 2.1.170
  • エージェントは claude -p --output-format stream-json … で起こしていました。

一覧に無い道具が使われていた

サブエージェントの中で承認の関門が効くかを測っていたとき、渡した一覧に無い道具の呼び出しが、stream-json のログに出ているのに気づきました。

渡した --allowedTools実際に使われた道具
Task,Bash,ReadAgent、Skill、Glob
Read,Glob,Grep,Write(レビューのエージェント)Bash(ls -la)、Agent

レビューのエージェントには Bash を渡していないつもりでしたが、ls -la を打っていました。エージェントの定義には「渡さない道具」も書いてあったのに、それを --disallowedTools に渡していませんでした。つまり「レビューのエージェントに Bash を渡さない」という決めごとは、それまで一度も守られていなかったことになります。

--allowedTools という名前からは、「ここに書いた物だけが使える」と読みたくなります。けれども、私たちが測った版ではそうなりませんでした。書いた道具を許可する一覧ではあっても、それ以外を使わせない境界ではありません。

壁の道具掛けから決まった道具を一つずつ外して運び出す人と、残った道具で作業を続けるロボットたち

--disallowedTools は効き、サブエージェントにも引き継がれた

レビューのエージェントに --disallowedTools Bash,… を渡して ls -la を頼むと、エージェントは次のように答えました。

現在の設定では Bash ツールの使用が許可されていないため、ls -la の直接実行はできません

そのエージェントが起こしたサブエージェントも、Bash を使えませんでした。サブエージェントの中では承認の関門が成立しないことも分かっていたので、あわせて、すべてのエージェントから Agent・Task を --disallowedTools で外しました。

権限の層が効く順番

ほかにも、権限の層がどう効くかを測りました。下の図は、測って分かった前後の関係だけを描いています。

permissions.deny はフックより手前で効き、フックの deny は --allowedTools の許可に勝つ

  • 設定の permissions.deny は、フックより手前で効きました。フックが allow を返しても覆せません。私たちは Bash(rm:*) を deny に入れていたので、エージェントが rm で始まるコマンドを打つと、その場で拒否されました。承認の申請を出すフックにさえ届きませんでした。
  • フックの側には「/tmp の下の削除は通す」という例外を置いていました。しかし rm で始まるコマンドには deny が先に当たるので、この例外が効くのは cd … && rm … のように、別の語で始まるときだけでした。裏を返せば、Bash(rm:*) の deny は、別の語で始まるコマンドの中の rm には当たっていませんでした。前方一致の deny だけを禁止の境界として頼ることはできません。
  • フックの deny は、--allowedTools での許可に勝ちました。--allowedTools Bash で許可していても、フックが deny を返せば止まります。
  • PreToolUse フックは、dontAsk・bypassPermissions を含む、すべての権限モードで発火しました。
  • 同じ呼び出しに複数のフックが当たると、全部が走り終わってから結果がまとめられました。片方が deny しても、もう片方のフックの副作用は起きます。

エージェントからは、何が拒否されるかが見えない

permissions.deny で何が拒否されるかを、エージェントの側から前もって確かめる手段はありませんでした。実際に、エージェントが rm や chmod で後片付けをしようとして拒否され、2つのエージェントが1ターンずつ無駄にしていました。そこで、すべてのエージェントに配る指示に、何が拒否されるかを書いておくことにしました。

どう確かめるか

stream-json のログから、エージェントが呼んだ道具の名前を拾い、起こすときに渡した一覧と突き合わせます。私たちも、この方法で気づきました。使わせたくない道具を --disallowedTools で外したあとは、その道具を使うように頼んでみて、断られることを確かめます。

確かめていないこと

  • strictAllowlist(2.1.219 から)で、この問題が解けるかどうか。2.1.170 には無く、試していません。
  • 新しい版で、--allowedTools の振る舞いが変わったかどうか。

おわりに

--allowedTools に「使ってよい物」を書いても、それ以外を使わせない境界にはなりませんでした。使わせたくない道具は --disallowedTools で名前を挙げて外し、実際に使われた道具をログで確かめるのが確実でした。

※ この記事に書いた Claude Code の振る舞いは、執筆時点で私たちが測ったものです。いまは変わっている可能性があります。