Claude Code headless実行でPermissionRequestフックが発火しない——「ask」は即denyされる
claude -p --output-format stream-jsonでPreToolUseの「ask」判定がPermissionRequestフックを経由せず即denyされる不具合を検証。公式ドキュメントの権限評価フローと突き合わせ、headless自動化で今すぐ使える回避策を整理した。
エンジニアのゆとです。
Claude Codeをheadless(claude -p、対話画面なし)で自動化パイプラインに組み込んでいる人向けの話。PreToolUseフックで「このコマンドは一旦askにして、PermissionRequestフックで中身を見てから許可を出す」という構成を組んでいる場合、その構成がheadlessの--input-format stream-json --output-format stream-json実行だと機能しないケースが報告されている。
具体的には、PreToolUseフックがpermissionDecision: "ask"を返したあと、本来ならPermissionRequestフックに処理が渡って承認ロジックが走るはずが、headless実行だとPermissionRequestフックが一度も呼ばれないまま即座にdenyが確定する。ログファイルも空のまま。対話ターミナル(TTY)から同じ設定・同じプロンプトで動かすと、PermissionRequestフックはちゃんと発火して承認ログも残る——という非対称な挙動だ。
「askにしておけば、あとでフック側で細かく判断できる」という設計をheadless CI/自動化に組み込んでいる場合、この記事の対象に該当する可能性がある。2026年9月20日にGitHub Issueとして報告された内容をもとに、症状の切り分け方と、公式ドキュメントの権限評価フローに照らした原因、そして今すぐ使える回避策を整理する。
まず結論
| 症状 | 状態 | 原因(切り分けの結果) | 今すぐできる対処 |
|---|---|---|---|
headless(-p + stream-json入出力)でPreToolUseのaskが即denyになり、PermissionRequestフックが一度も呼ばれない | 未修正(2026年9月20日報告、Openのまま) | headless実行時、canUseToolコールバックや--permission-prompt-toolのような「host」が存在しないと、PermissionRequestフックへのディスパッチ自体が起きていない可能性が高い | PreToolUseフック側で最初からallow/denyを確定させる設計に変更する(askを経由しない) |
同じ設定を対話ターミナル(TTY)から動かすと問題なくPermissionRequestが発火する | 正常 | TTYセッションには暗黙的に「host」(ユーザー自身への確認導線)が存在するため | 対話実行では現状の設計のままで問題ない |
--permission-mode bypassPermissionsでも--permission-mode defaultでも同じ結果になる | 再現性あり | bypassPermissionsモードの特例(一部操作がcanUseToolに必ず落ちる仕様)とは別の、ディスパッチ経路そのものの欠落 | モード変更では回避できない。設計変更が必要 |
先に対処法を知りたい場合は「対処法」の章まで飛んでほしい。
再現条件
報告されている再現構成はこうだ。.claude/settings.jsonに、BashへのPreToolUseフックでaskを返す設定と、それを受け取って判定するPermissionRequestフックを両方仕込む。
{
"permissions": { "defaultMode": "bypassPermissions" },
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"test ask from hook\"}}'" }
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "cat >> /tmp/permreq.log; echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":\"allow\"}}'" }
]
}
]
}
}
この状態で、対話ターミナルからclaudeを素で起動して「Bashツールでecho hiを実行して」と頼むと、トランスクリプトに⏿ Allowed by PermissionRequest hookと表示され、/tmp/permreq.logにも1行残る。想定どおりの動きだ。
ところが同じディレクトリ・同じプロンプトを、headless形式で流すと結果が変わる。
claude -p "Use the Bash tool to run: echo hi. Report exactly what the tool returned." \
--permission-mode bypassPermissions \
--output-format stream-json --input-format stream-json --verbose \
<<< '{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Use the Bash tool to run: echo hi. Report exactly what the tool returned."}]}}'
/tmp/permreq.logは空のまま更新されず、ストリームには次のようなイベントが流れてくる。
{"type":"system","subtype":"permission_denied","tool_name":"Bash","decision_reason_type":"hook","decision_reason":"test ask from hook", ...}
--permission-mode defaultに変えても結果は同じで、PermissionRequestフックは一度も呼ばれない。設定ファイルは1文字も変えていないのに、起動方法(TTYかheadlessか)だけで結果が変わっている。
自己診断チェックリスト
自分の環境がこのケースに当てはまるか、次の3点で確認できる。
.claude/settings.jsonでPreToolUseフックがpermissionDecision: "ask"を返す設計になっているか- その
askの解決をPermissionRequestフック(またはcanUseToolコールバック)に委ねているか - 実行方法が
claude -p+--input-format stream-json --output-format stream-jsonで、かつSDK側のcanUseToolコールバックや--permission-prompt-toolを登録していないか
3つとも該当する場合、PermissionRequestフックのログが空のまま、ツール呼び出しだけがdecision_reason_type: "hook"で静かに拒否され続けている可能性が高い。対話実行では発生しないので、CI上でだけ原因不明の拒否が起きている、という形で気づくケースが多いはずだ。
対話からも同じ拒否が起きる場合は、このIssueの対象外で、フック自体の設定ミス(matcherの書き方、JSON構文、実行権限)を疑ったほうが早い。

なぜ起きるのか——公式ドキュメントの権限評価フローと突き合わせる
Claude Codeの権限判定は、公式ドキュメントによると次の順で評価される。
- Hooks(
PreToolUseが先に走る。denyすれば即終了、allowでも後続のdeny/ask ruleはスキップされない) - Deny rules(
settings.jsonのdisallowed_toolsなど) - Ask rules(
settings.jsonのaskルールに一致したらcanUseToolコールバックに回る) - Permission mode(
bypassPermissionsならここでほぼ自動承認) - Allow rules
canUseToolコールバック(ここまでで解決しなければ、最終的にここに落ちる)
そしてcanUseToolコールバックの章には、こう明記されている。TypeScript SDKでpermissionPrompts: 'none'を指定した場合、「canUseToolコールバックはこのステップで呼ばれない。ただしPermissionRequestフックには判断のチャンスが残り、それでも解決しなければClaude Codeはコールを拒否する」。
CLIの--permission-prompts noneの説明はもっと直接的だ。「フラグを付けなければ、実行はhost(canUseToolコールバックを持つAgent SDKアプリ、または--permission-prompt-toolで渡したMCPツール)の応答を待つ。フラグを付けると、hostには一切問い合わせない。プロンプトが必要になった操作は、PermissionRequestフックが許可しない限りdenyされる」。
この2箇所の記述を素直に読むと、「host(canUseToolコールバックや--permission-prompt-tool)が存在しない実行であっても、PermissionRequestフックには判断の機会が渡るはず」というのがドキュメント上の約束になっている。
ところが今回のIssueで再現している構成は、まさに「hostが存在しないheadless実行(canUseToolコールバックなし、--permission-prompt-toolなし、stream-jsonの入出力のみ)」で、しかも--permission-prompts noneすら指定していない。ドキュメントどおりならPermissionRequestフックに判断が渡るはずの場面で、渡らずに即denyされている——つまり「hostが存在しないheadless実行では、PermissionRequestフックへのディスパッチ自体が起きていない」という欠落が疑われる、というのがこのIssueの核心だ。
報告者はサードパーティ製のラッパー経由でも同じ現象を確認していて、「ラッパー側のPermissionRequestフックにもリクエストが届いていない」ことから、ラッパー側の実装ミスではなくClaude Code本体のディスパッチ側の問題だと切り分けている。
一般的な「headlessで権限プロンプトが出ない」設定との違い
紛らわしいので先に区別しておくと、headlessで権限プロンプトが出ずにdenyされること自体は、多くの場合は仕様どおりだ。
--permission-mode dontAskを指定している → 仕様。プロンプトが必要な操作はすべてdenyされる。canUseToolは一切呼ばれない--permission-prompts noneを指定している → 仕様。hostに問い合わせず、PermissionRequestフックが許可しない限りdenyされるcanUseToolコールバックも--permission-prompt-toolも渡していない、かつ--permission-prompts noneも付けていない → ここが今回のケース。ドキュメント上は「hostがいなくてもPermissionRequestフックには判断が渡る」はずなのに、渡っていない
自分の設定が上2つに該当するなら、それは意図どおりの拒否なので、この記事の対処法は不要だ。3つ目、つまり「PermissionRequestフックだけを唯一の解決手段として設計していて、それが空振りしている」場合だけがこのIssueの対象になる。
対処法1(推奨): PreToolUseフック側で判定を完結させる
現状で一番確実なのは、「askにしてPermissionRequestフックに丸投げする」設計をやめて、PreToolUseフック自身がallowかdenyを直接返す設計に変えることだ。権限評価フローの1番目(Hooks)で完結するので、PermissionRequestフックへのディスパッチが機能しているかどうかに依存しなくなる。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/path/to/your-approval-logic.sh" }
]
}
]
}
}
your-approval-logic.shの中で、これまでPermissionRequestフック側に書いていた判定ロジック(許可リストとの突き合わせ、危険なコマンドパターンの検出など)をそのまま実行し、最終的にpermissionDecision: "allow"か"deny"のどちらかを確定させて返す。"ask"を経由しない設計にすれば、headlessでもTTYでも同じ結果になる。
ロジックが複雑で1つのフックに収めにくい場合は、PreToolUseの中でさらに外部スクリプトやAPIを呼んで判定結果だけを受け取る、という構成でも成立する。要は「askという中間状態を作らない」のがポイントだ。
対処法2: 明示的なhostを用意する
設計をどうしてもask+PermissionRequestのまま保ちたい場合、hostが存在しない実行を避ける、という方向の回避策もある。
- Agent SDK(Python/TypeScript)を使っているなら、
canUseToolコールバックを実装して渡す - CLIから
claude -pを使っているなら、--permission-prompt-toolでMCPツールを1つ用意し、それをhostとして渡す
ドキュメント上、hostが存在する実行ではPermissionRequestフックの前段に別の解決経路(canUseToolまたは--permission-prompt-tool)が用意される。今回のIssueは「hostが存在しない」構成でのみ確認されているため、hostを用意することで少なくとも解決経路自体は復活する可能性がある。ただしこの記事の執筆時点でAnthropic側からの確認・修正コメントはついていないため、確実な回避策としてではなく「試す価値がある選択肢」として捉えてほしい。
対処法3: askを使わない権限設計に寄せる
そもそも論として、headless自動化・CI用途ではaskという「誰かの応答待ち」を発生させる設計自体が相性が悪い。--permission-mode dontAskを起点にして、許可したい操作をallowed_toolsやsettings.jsonのallowルールで先に列挙しておき、それ以外はすべて拒否される前提で設計する方が、今回のような中間状態に依存する不具合の影響を受けにくい。

すでにheadless/自動化パイプラインを組んでいて、スケジュール実行やAPI連携まで含めて設計を見直したい場合はこちらも参考になる。


FAQ
対話(TTY)実行では今すぐ困らない?
報告されている限りでは、対話ターミナルからの実行ではPermissionRequestフックは正常に発火する。影響が出るのはclaude -pかつ--input-format stream-json --output-format stream-jsonの組み合わせで、canUseToolコールバックや--permission-prompt-toolを渡していないケースに限られる。ただし今後の修正で挙動が変わる可能性はあるので、CI・自動化でask+PermissionRequestの構成を使っている場合は、対話実行でも一度動作を確認しておくと安心だ。
--output-format json(stream-jsonではない)なら大丈夫?
このIssueの再現報告は--input-format stream-json --output-format stream-jsonの組み合わせに限定されている。--output-format json単体やtext出力での挙動については報告に含まれていないため、この記事では断定を避ける。該当する構成を使っている場合は、まず/tmpなどに簡単なログを吐くPermissionRequestフックを仕込んで、実際に発火するかどうかを自分の環境で確認するのが確実だ。
PreToolUseのexit code 2でブロックする方法ではダメ?
PreToolUseはexit code 2でブロックできるが、これは「拒否」の一択で、対話的な「承認するかどうかの判断」を表現できない。今回のように「条件次第でallowもdenyもありえる」判定をしたい場合は、対処法1で触れたとおりPreToolUseフックの中で判定ロジック自体を完結させ、permissionDecisionフィールドでallow/denyを明示的に返す方が扱いやすい。
まとめ
PreToolUseフックでaskを返し、PermissionRequestフックで最終判断する設計は、対話実行では問題なく動く。しかしheadless(claude -p + stream-json入出力)で、かつcanUseToolコールバックや--permission-prompt-toolのようなhostを渡していない場合、PermissionRequestフックへのディスパッチが行われず、askが問答無用でdenyされる不具合が2026年9月20日時点で報告されている。
CI・自動化パイプラインで同じ症状に遭遇した場合、最も確実な対処はPreToolUseフック側でallow/denyの判定を完結させ、askという中間状態を経由しない設計に変えることだ。hostを明示的に用意する、あるいはask自体を使わない権限設計に寄せる、という選択肢もあわせて検討する価値がある。