Cursorのhookエラーは設定ファイルの共有を疑う

Cursorのhookエラーは設定ファイルの共有を疑う

Cursor がエージェントの実行を止め、hook の stdout が正しい JSON ではないと言ってきました。

そのメッセージが「直せ」と名指ししてきたスクリプトは、Windows に最初から入っている conhost.exe。

自分で書いた覚えのないプロセスが犯人扱いされている、という妙な状況でした。

結論から置いておくと、真因は conhost.exe でも自分の書いたスクリプトでもなく、別のツールが書き残していた設定ファイルにありました。

そしてこの一件から取り出せる教訓は、エラーが名指しするプロセス名は「実際に起動された結果」であって「設定に書かれた内容」とは限らない、という一点に尽きます。

前提:hook は JSON を返す外部プロセス

話の土台になる仕組みだけ、先に最小限そろえておきましょう。

hook は、エージェントがツールを実行する前後に走る外部プロセスのこと。

契約はきわめて単純で、stdin から JSON を受け取り、stdout へ JSON を返すだけです。

裏を返すと、stdout に JSON 以外が1バイトでも混ざれば、受け取る側はパースに失敗します

そしてもう一つ、今回の伏線になる仕様がありました。

Cursor は自前の設定ファイルだけでなく、Claude Code 形式の hook 設定も読み込んで実行します。

つまり2つのツールが同じ1つのファイルを見ている構図。

この点は公式ドキュメントに明記されています。 Hooks | Cursor Docs

Hooks | Cursor Docs

エラーが名指ししたのは Windows の標準プロセスだった

実際に出たのは、hook の stdout が正しい JSON ではないので安全のためアクションをブロックした、という趣旨のメッセージでした。

そこに添えられた「修正すべき hook スクリプト」のパスが、Windows のシステムフォルダにある conhost.exe。

conhost.exe は Windows のコンソールホストで、hook として自分が書いた覚えなど当然ありません

ここで「conhost.exe の挙動を調べよう」と進むと、完全に迷子になります。

まず stdout をバイト単位で見る

名指しされたパスを疑う前に、hook の出力に JSON 以外が混ざる典型パターンをつぶすのが定石です。

デバッグ用の出力、UTF-8 の BOM、色付けの ANSI エスケープ、バッチの echo 表示。

どれも目視では気づきにくいので、16 進で覗くのが手っ取り早いところ。

'{"hook_event_name":"preToolUse","tool_name":"Shell","tool_input":{}}' |
  & "C:\path\to\your\hook.cmd" | Format-Hex

# 先頭 EF BB BF -> UTF-8 BOM
# 1B 5B ...    -> ANSI エスケープ
# JSON の前後の余分な行 -> echo / console.log の混入

名前が出てきた意味のほうを先に考える

もう一つ、切り分けと並行してやるべきことがありました。

自分が書いていないプロセス名が出たのなら、設定側に自分が書いていない定義がある、という読み替えです。

そちらへ舵を切ったことで、調査は一気に短くなりました。

設定ファイルを開いたら、書いた覚えのない hook が並んでいた

そこで、読まれうる設定ファイルを片っ端から確認していきます。

プロジェクト直下の Cursor 用 hook 定義も、同じくプロジェクト直下の Claude Code 用設定も、どちらも存在しませんでした。

残る候補はユーザーレベルの設定ファイル1本だけ。

開いてみると、11 種類のイベントにびっしり hook が設定されていました

セッション開始、プロンプト送信、ツール実行の前後、権限要求。

書き込んだのは私ではなく、以前インストールしていたサードパーティ製のエージェント開発環境でした。

しかも、そのツールはすでにアンインストール済みで、設定ファイルだけが残っていたという状態。

中身はいずれもステータス表示と使用量トラッキングのためのもので、悪意のあるものではありません。

真因の推定:サポート外のフィールドは黙って落ちる

問題の定義は、こういう形をしていました。

{
  "type": "command",
  "command": "C:\WINDOWS\System32\conhost.exe",
  "args": [
    "--headless",
    "C:\WINDOWS\System32\cmd.exe",
    "/d", "/c",
    "%USERPROFILE%\...\claude-hook.cmd"
  ],
  "timeout": 10
}

command はランチャーにすぎず、本体の処理は args 配列の側に入っています

いっぽう Cursor ネイティブの hook 定義は、公式ドキュメントを読むかぎり次の形。

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      { "command": "./hooks/validate.sh", "matcher": "Shell" }
    ]
  }
}

2つの形式のズレが引き金になった

見比べると、Cursor 側の command は単一の文字列で、args というフィールドを持ちません。

ここから導けるのは、args が解釈されずに落ち、引数なしの conhost.exe だけが起動したという筋書きです。

引数を失ったランチャーは本来の処理を呼べず、何も出力しないまま終わる。

その空の stdout を JSON としてパースしようとして失敗した、と考えると全体がつながります。

ただしargs が無視されるという点は公式に明記された挙動ではなく、エラー文言と設定内容から組み立てた推定にすぎません。

なぜエラーメッセージが誤誘導するのか

ここまで来ると、conhost.exe を名指しした理由もはっきりしました。

エラーが表示していたのは「Cursor が実際に起動したプロセス」であって、設定に書かれた意図ではなかったわけです。

サポート外のフィールドをエラーにせず黙って落とす実装だと、こういう静かな壊れ方をします。

設定は残る、起動もする、けれど中身は空。

対処と、設定ファイル共有への教訓

打てる手は大きく2つありました。

1つは、Cursor 側でサードパーティ製の hook 設定の読み込みを止める方法。

もう1つは、書き込んだツール側で連携そのものを切る方法です。

前者なら他ツール側の機能を残したまま切り離せます。

逆に設定ファイルを手で書き換えるのは恒久対策になりません

書き込んだツールが動いているかぎり、次の起動で同じ内容が再生成されるためです。

アンインストールしたら設定の残骸も点検する

今回いちばん効く教訓は、ツールを消しても設定ファイルは残るという当たり前の事実でした。

ツールを整理したら、そのツールが書いた設定ファイルまで棚卸しする

このひと手間があれば、今回の調査そのものが不要だったはずです。

あわせて、残された設定に危険な操作の確認プロンプトを省略する種類のフラグが紛れていないかも見ておきたいところ。

意図せず安全側の仕組みが外れたまま別のツールに引き継がれるのは、hook が動かないことよりよほど怖い話です。

複数ツールが読む設定ファイルの種類と役割分担は、設定ファイルのガイド記事で整理しています。

AIエージェント設定ファイル完全ガイド:.cursorrules・CLAUDE.mdの違いと使い分け

なお Cursor の公式ドキュメントによれば、hook の失敗は既定ではアクションを通す fail-open の扱いとされています。

それでも今回は実行がブロックされたので、既定と違う挙動に見えた時点で設定を疑う、という手がかりにもなりました。 Hooks reference – Claude Code Docs

Hooks reference - Claude Code Docs

最後に

今回の調査は、出力を疑い、起動されたコマンドを疑い、最後に「誰がその設定を書いたのか」を疑う、という順番で降りていきました。

複数のツールが同じ設定ファイルを読む構成では、どのツールがどのフィールドまで解釈するかが食い違う余地が常に残ります

なお修正後の再現テストまでは終えていないので、ここで書けるのは「こう直した」ではなく「こう切り分けた」まで。

同じように身に覚えのないプロセス名を名指しされたときは、そのプロセスを調べる前に設定ファイルの出どころを確かめてみてください。

以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA