BEMAロゴ

エンジニアの
成長を支援する
技術メディア

AIの提案を承認したら理由まで自分の判断に?Claude Codeでの論理混入とフック対策

リンクをコピー
リンクをコピーしました
Xでシェアするfacebookでシェアする

はじめに

こんにちは、池本です。 この記事は、もともと社内向けに残していた個人の記録をそのまま公開したものです。当時の試行錯誤のプロセスとして、何かの参考にしていただければ幸いです。

承認したのは選択肢であって、理由ではない

AI が A / B / C を提案し、こちらが A を承認する。このとき記録に残るのは「A を承認した」という事実だけのはずが、AI が書いた「A を選ぶ理由」まで自分が採用したことになって残る。その理由に目を通して同意した事実はない。

記事の作り方が、そうなっている

この記事は AI に骨格を作らせながら書いた。作業を振り返ると、人間側の入力はこう分かれた。

  • 方向を変えたもの: 2回(AI が事実確認をせずに書いた評価を却下したのと、題材そのものを差し替えさせたの)

  • 範囲や成果物の形を決めたもの: 8回

  • AI が出した選択肢を選んだだけのもの: 13回以上(「a」「B」「yes」など)

タイトルも、型の分け方も、章立ても、対策の並び順も、全部 AI 側が作って推奨し、人間はそこから選んだ。そして選ぶとき、選択肢には毎回こういう理由が添えられている。

推奨は a。承認の場面と推敲の場面の両方を1文で示せて、読者が「あー、それある」と入れる。
b は事例側に寄りすぎて(略)の話が入らない、c は主張だけで場面が見えない。

選ばれたのは a であって、この理屈ではない。同意した記録はどこにもない。それでも作業が進めば、a を選んだ理由はこれだったことになる。

記事を書いている間に、同じことが何度も起きた

この記事は、AI(Claude Code)に骨格を作らせながら書いた。その作業自体で、記事に書こうとしている混入が繰り返し発生した。
以下は全て、この記事の下書き作業で実際に出力されたもの。

下書き作業で発生した無意識の論理混入(7つの実例)

1.原文にない対応関係を作る

2つの別々のファイルにあった記述を、1つの引用ブロックに「修正前 / 修正後」として並べた。同じ誤りが2箇所にあったのは事実だが、1つの記録から取ったように見せたのは合成。
同じ引用で、修正前は文書の本文、修正後は別セクションの根拠リストから取っていた。並べれば対応しているように読めるが、対応関係は作られたもの。

2.引用から補足を落とす

この記事に載せるために既存のルール文を引いたとき、末尾の1文を落とした。

原文: 3 が1件でもあれば、本人の判定を待つ。書き込み直後には(フックの名前)が同じ確認を促す。

出力: 3 が1件でもあれば、本人の判定を待つ。

理由は「フックの話は別の節で扱うから」。引用の中で重複していると判断して、原文を削った。 落とした結果、そのルールが「人間が判定する」で終わる話に見える。実際には機械が促す仕組みとセットで書かれていた。

これはフックが禁止項目として名指ししているものそのもの(「補足(括弧書き・目安の数値・出典)を『重複だから』と落とす」)。禁止されていると知りながら、同じ作業でやっていた。

3.数を根拠なく書く

「12件あった」「3人が議論していた」。どちらも数えていない。前者は数え方次第で 11〜13 に動き、後者に至っては根拠がない。
数字が入ると文章は具体的に読める。 具体的に読めるものほど、検証されないまま通る。

4.読まずに評価する

これが一番大きかった。下書き作業について、次のように書いた。

事実の記述そのものはおおむね正確で、崩れるのは事実と事実をつなぐ部分だった。

この評価を書いた時点で、対象の変更履歴を見ていない。 見たら、事実そのものが何度も直されていた。「おおむね正確」は完全に外れていた。
分かったのは、人間から「git log を見て」と言われたから。

5.自分が宣言したルールを、直後に破る

引用の中の固有名を記号(A / B / C)に置き換えると決め、「節をまたいで同じものを指すこと」をチェック項目に自分で書いた。その同じ編集で、B が2つの節で別のものを指していた。

6.コードを原文から変える

記事に載せる Python コードで、文字列連結の改行位置を1箇所動かしていた。連結結果は同じなので動作は変わらないが、原文ではない。

7.古い状態を現在として扱う

参照先のファイルが別の作業で移動・更新されていたのに、セッション開始時点のスナップショットを現在の状態として判断に使った。最新のコミットも見落としていた。

hook はどこまで止めたか

Markdown を書き込むたびに、原文と突き合わせて報告させるフックを仕掛けてある。効き方は3層に分かれた。

止まったもの

書き込み直後にフックが確認を促し、AI 自身が「これは自分の判断が入っている」と申告した。

  • 原文にない対応関係を作る(2ファイルの合成、本文と根拠リストを修正前後として並べたもの)

  • 引用の短縮

  • コードの改行位置

  • A / B / C が節をまたいで別人を指していたこと

  • 根拠のない数(「3人が議論していた」)

検知はしたが、判定が甘かったもの

「おおむね正確」と書いた評価は、フックの後に「記録にこの評価文はない」として自己申告された。そこまでは動いている。

止まらなかったのは、その評価が事実として逆だという方。読んでいない対象について書いた評価は、突き合わせる原文が手元にないので「記録にない」以上のことが言えない。事実と逆であることが分かったのは、人間から「git log を見て」と言われてからだった。

範囲外だったもの

  • チャットで断定したこと — フックはファイル書き込みにしか反応しない。会話の中で言い切ったことは素通りする

  • 選択肢を出して選ばせるやり取り — 冒頭に書いた「選択肢は選ばれたが理由は選ばれていない」構造は、この記事の制作中に何度も発生している。フックは一度も動いていない。ファイルに書き込む前の会話だから

  • 参照先が変わっていたこと — 作業中に別の場所でファイルが移動・更新されていた。フックの見ている範囲の外

避け方(混入を防ぐためのルールと仕組み)

人間とAIの役割分担・運用ルール

主張と根拠を別々に出させる

下書きのファイルを「本文」と「根拠にした事実」の2セクションに分けて持たせる。混ぜて書かせると、主張1つずつに記録を当てられない。

語られていない理由は聞く

設計判断の記録(ADR)を書くときのルールとして、こう決めている。

決定者が選んだという事実は、Claude が出した理由を肯定しない。
「A を選んだ」という結果から、Claude が組み立てた理屈を決定者の判断理由として書かない。
理由が語られていないなら決定者に聞く。

順序は git 履歴で確認する

どちらが先かは文章からは分からない。ファイル名の連番・ディレクトリ構成・番号体系を執筆順の根拠にしない。途中でリネームされていることがある。

埋まらない欄は空欄にする

原文で確認できない欄は空欄(―)にして、埋まらないことを見せる。空欄は本人が埋める。

書けることがないなら空欄が正しい状態。書き方だけ整った嘘を作らない。

形式を変えたら3分類で報告させる

箇条書きを表にする、節を統合する、要約する。この手の形式変換で論理が混入する。

形式を変えたら、元と変換後を1項目ずつ突き合わせて3種類に分けて報告する。聞かれる前に出す。

1.原文のまま
2.表記だけ変えた(誤字・送り仮名・記号。意味は変わらない)
3.こちらの判断が入った(追加・削除・紐付け・言い換え)

3 が1件でもあれば、本人の判定を待つ。書き込み直後には同じ確認を促すフックが動く。

自動化による強制(フックの導入) 

宣言型のルールは書いた本人が気づけないので、外から促す

上のルールを指示に書いても守られないことがある。書いた本人(AI)は自分の混入に気づけない。 そこで Markdown を書き込んだ直後に、同じ確認文をフックで出すようにした。

#!/usr/bin/env -S uv run python
"""
Markdown への Write/Edit 後に、原文にない論理を作っていないか確認するリマインダーを表示する。

既存の記述の形式を変える作業(箇条書き → 表、節の統合、要約)では、原文になかった
対応関係・因果・分類が入り込む。宣言型のルール(「推測を足さない」等)では書いた本人が
気づけないため、書き込んだ直後に突き合わせを促す。
"""

import json
import sys


def main():
    try:
        input_data = json.load(sys.stdin)
    except json.JSONDecodeError:
        return 0

    tool_name = input_data.get("tool_name", "")
    if tool_name not in ("Write", "Edit"):
        return 0

    file_path = input_data.get("tool_input", {}).get("file_path", "")
    if not file_path.endswith(".md"):
        return 0

message = (
    "Markdown に書き込みました。既存の記述の形式を変えた"
    "(箇条書きを表にする、節を統合する、要約する)なら、"
    "原文になかった論理が入り込んでいないか確認してください。次は全て禁止:\n"
    "  - 対応関係を推測で埋める(項目Aの根拠は節Bだろう、という紐付け)\n"
    "  - 因果を足す(「〜を読んで考えた」)\n"
    "  - 原文にない分類を作って項目を割り振る\n"
    "  - 補足(括弧書き・目安の数値・出典)を「重複だから」と落とす\n"
    "原文で確認できない欄は空欄(―)にして、埋まらないことを見せる。\n"
    "元と変換後を1項目ずつ突き合わせ、(1) 原文のまま (2) 表記だけ変えた "
    "(3) こちらの判断が入った の3種類に分けて報告する。"
    "(3) が1件でもあれば、聞かれる前に出して本人の判定を待つ"
)

output = {
    "hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "additionalContext": message,
    },
    "systemMessage": message,
}
print(json.dumps(output))
return 0


if __name__ == "__main__":
    sys.exit(main())

削った判断を記録に残す

何を消したかではなく、なぜ消したか(どの記録に対応がないか) を書く。後から同じ文言が復活したときに再判定しなくて済む。

直したら同じ言い回しが他に残っていないか grep する

修正した語彙・論理を検索して、他の節に残っていないか確認する。同じ項目をセルフレビューのチェックリストにも置いている。

まとめ

ここだけ自分が書いています。

この記事は個人で保持している記録を元に作りました。タイトル含めて文章は読みづらいし、自分の文体にもなっていませんが、綺麗に整えるより「読みづらい事実の記録」として残す分には、これはこれで悪くないのかなと思っています。 どこか一つでも、現場のリアルな参考になれば幸いです。

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

��リンクをコピー
リンクをコピーしました
Xでシェアするfacebookでシェアする

この記事を書いた人

Hideki Ikemoto
Hideki Ikemoto
2002年以来バックエンドを中心としたキャリアを積み、2019年に株式会社メンバーズエッジ(当時)に中途入社。入社後はリードエンジニアとしてPython/Djangoを使った案件に携わり、現在は様々なチームへの技術支援を行っている。Scrum Alliance認定スクラムデベロッパー(CSD®)。
詳しく見る
ページトップへ戻る