エンジニアリング

AIエージェントの作業記録:何を残せば後から調べられるか

誰も見ていないところで動くエージェントには、足跡を残させる必要があります。お店の防犯カメラのたとえで、残すべきこと、残してはいけないこと、簡単なJSON Lines形式、秘密情報の隠し方、記録から時系列を組み立てる方法を解説します。25件の操作で実際に動かしたスクリプト付きです。

  • #作業記録
  • #AIエージェント
  • #監督
  • #JSON Lines
  • #障害調査
  • #チェックリスト

スライド

スライドをクリックし、矢印キーでページを移動します。Lキーで言語、Fキーで全画面表示を切り替えます。

全画面で開く

AIエージェントに仕事を任せる人なら誰でも、初心者の方でも読めます。監視やセキュリティの知識は不要です。用語にはどれも身近なたとえを添えています。コードの部分は印をつけてあるので、今は不要なら読み飛ばせます。

数字は公開情報から取っており、末尾に一覧があります。「例」と書いたものは作った例で、実在の人物や会社には触れていません。実際に動かした例は、私がPythonで書いた模擬であり、市販ツールの記録ではありません。AIが内部でどう行動を決めているかは、まだ誰にも完全には分かっていません。以下のたとえは覚えやすくするための補助です。


まず要点#

  • エージェントに仕事を任せて会議に出ると、1つひとつの手順は見えません。足跡がないと、何か起きたときに長い会話を読み返して推測するしかありません。
  • 操作の記録とは、エージェントがしたことを1つにつき1行で残したものです。いつ、どのツールやコマンドで、どんな結果になり、どのファイルが変わったか。1行に1つのJSONを置く形式(JSON Lines)なら、数行のコードかjqコマンドで検索できます。
  • 残すのは必要な分だけ。 秘密情報、個人データ、ファイルの全文は残しません。書く前に隠します。鍵が漏れる記録は、鍵が漏れるコードと同じくらい危険だからです。
  • 記録する層はエージェントの外側に置き、 追記だけを受け付けます。エージェントが編集できる記録は、もう証拠ではありません。
  • 実際に動かした例では、25件の操作で7,170バイトのファイルができました。3つの調査の問い(時系列、最初の失敗、変更ファイル)はスクリプトを動かすとすぐに答えが出て、書き換えられた行も検知できました。
  • 記録は何が起きたかを示すもので、それが正しかったかは示しません。証拠を求めることやテストの代わりではなく、それらを補います。

この記事を読むと説明できること#

  1. 誰も見ていないセッションにも足跡が必要な理由と、足跡が保存した会話とどう違うか。
  2. 記録の1行にどんな項目を持たせるか、そして絶対に残してはいけないもの。
  3. 問題が起きた後の使い方:時系列の組み立て、最初の失敗の特定、変更ファイルの一覧、書き換えられた行の検知。

むずかしい言葉をやさしく#

用語 やさしい意味
Audit trail(作業記録、監査の足跡) したことを時間順に書き残したノート。お店のレシート帳のようなものです。
Log(ログ) そのノートの1行。
JSON Lines ファイルの各行が1つの完結した記録になっている形式。記録を足すときは最後に1行足すだけです。
Redaction(秘匿処理) 書き写す前に秘密の部分を黒塗りすること。レシートでカード番号を隠すのに似ています。
Hook(フック) ツールが用意している、出来事に合わせてコマンドを走らせる差し込み口。たとえばエージェントがコマンドを実行した直後に動かせます。
Timeline(時系列) 起きたことを順に並べた一覧。どの出来事がどれにつながったかが見えます。
Append-only(追記専用) 最後の行の下にだけ書けて、古いページは消せないノート。
Hash chain(ハッシュ連鎖) 各行が直前の行の「指紋」を持つ仕組み。1行を書き換えると、後ろの行の指紋が合わなくなります。

1. まずイメージ:お店の防犯カメラとレシート帳#

お店を開き、とても手の早い店員を雇ったとします。今日は留守にしました。戻ると、レジが20万ドン足りません。何も記録がなければ、店員に聞くしかありません。店員は正直に答えても、記憶違いをしているかもしれません。

今度は、お店に2つのものがあるとします。カウンターの様子を撮るカメラと、取引を1件ずつ書くレシート帳です。何時に、何を売って、いくら受け取ったか。1日分の映像を全部見る必要はありません。帳面を開いて午後3時の取引を探せば、ずれがすぐ見つかります。飛行機も同じです。フライトレコーダーが操作を残しているので、事故のあとに、聞ける人が残っていなくても何が起きたかを組み立てられます。

エージェントは、この手の早い店員に似ています。数分で何十ものコマンドを動かします。会話はエージェントが語る話です。操作の記録がレシート帳です。言ったことではなく、したことが残ります。

ポイント: 記録は店員を捕まえるためのものではありません。何かあったとき、何時間も推測する代わりに数分で原因を見つけるためのものです。


2. 後から見直さないセッションに足跡が必要な理由#

任せる仕事が増えるほど、1つひとつの手順を見なくなります。Anthropicの2026年版エージェント型コーディングのトレンド報告書の紹介ページには、技術リーダーが生産性と、監督、品質、セキュリティの間でバランスを取っていること、複数エージェントの連携が話題に含まれることが書かれています。私が読めたのは紹介ページだけです。全文は登録フォームの先にあるため、報告書の本文は読んでおらず、そこからはこれ以上何も引いていません。

もう1つの数字です。Stack Overflowの2026年の調査では、回答者の69.9%が未知の課題に直面したときAIエージェントに聞くと報告されています(要約を通じて読んだ数字で、各表とは照合していません)。エージェントは日常の一部になっていて、その作業はしばしば、あなたが見ていないところで進みます。

すると2種類の損失が出ます。1つ目:問題が起きたのに、エージェントがどのコマンドを走らせたか誰も分からず、調査が遅くなる。2つ目:問題はないのに、同僚が「なぜテストファイルが変わったの?」と聞き、あなたが長い会話を読み返して答える。記事「AIの結果に証拠を添えさせる」は、エージェントの回答の中に証拠を求めます。記録は残りを補います。回答の外にあり、エージェントが書けない別の層が書いた証拠です。

ポイント: 任せるほど足跡が必要になります。足跡は、読み返すより安く済みます。


3. 何を残し、何を残さないか#

3.1 残したい7つの情報#

レシートを思い浮かべてください。誰が売り、何を、いつ、いくら、どの注文で。

項目 内容 何のため
ts タイムゾーンつきの時刻 順序づけ、時系列の再現
session セッションID 並行して動いた実行の切り分け
actor 実行者(エージェント、人、自動ジョブ) 誰の責任かを知る
request その操作のきっかけになった元の依頼 なぜ行われたかを知る
tool と args ツールまたはコマンドと引数(秘密は隠す) 何をしたかを知る
exit_code と summary 終了コード、結果の要約 失敗した場所を見つける
files_changed 変更したファイルの一覧 影響の範囲を知る

書き換えられた行を見つけたいなら、prev(直前の行の指紋)を足します。第5節ではそうしています。

3.2 残してはいけない3つ#

  1. 秘密情報とパスワード。 書く前に隠します。記録は設定ファイルより多くの人に読まれがちなので、記録の中の鍵は.envの中の鍵より遠くまで広がります。
  2. 利用者の個人データ。 残すのは、隠した識別子か社内のIDだけです。
  3. ファイルの全文や長いコマンド出力。 要約(行数、合格したテスト数)と、見直す場所だけを残し、中身は残しません。記録は小さく、リスクも小さく保てます。

チームにすでに「1ページのAI利用ポリシー」があるなら、この3つを「禁止」の列に入れ、記録を保管する期間の見直し日を決めてください。

3.3 1行はこんな見た目#

{"seq": 12, "ts": "2026-10-31T09:01:27+07:00", "session": "s-001",
 "actor": "agent", "request": "R2: make the tests pass",
 "tool": "run_command", "args": {"cmd": "python -m pytest -q"},
 "exit_code": 1, "summary": "ran 20 tests, 18 passed, 2 failed",
 "files_changed": [], "prev": "5d5c9ffa0389"}

(実際のファイルでは、1つの記録が1行に収まります。ここでは読みやすいように折り返しています。)

ポイント: 7つの情報が、調査の5つの問い(誰が、何を、いつ、なぜ、どこまで)に答えます。3つの禁止は、ノートが新しい漏えいの場所にならないようにするためです。


4. 仕組み:4つのつながり#

生産ラインを思い浮かべてください。操作、記録する層、保管、検索。

  1. エージェントが操作する。 コマンドを走らせ、ファイルを編集し、ツールを呼ぶ。
  2. 記録する層が操作を捉える。 この層はツールやエージェントを動かす枠組みの中にあり、エージェントへの指示文の中ではありません。「自分で記録しておいて」と頼むだけでは、忘れたり間違えて書いたりします。
  3. 秘密を隠し、追記専用の場所に書く。 行を足すだけで、古い行は開き直して編集しません。
  4. 必要なときに検索する。 スクリプトかjqで読みます。

4.1 お使いのツールにつなぐ場所はあるか#

Claude Codeの現行のフックの文書(このページは開いて確認しました)によると、ツール呼び出しの前後でコマンドを走らせられます。コマンドは標準入力(stdin)でJSONのオブジェクトを受け取ります。文書に挙がっている項目は、session_id、transcript_path、cwd、hook_event_name、そして呼び出し後のイベントにはtool_name、tool_input、tool_response、tool_use_id、任意のduration_msです。ツールが失敗した場合には別のイベントがあり、errorの項目がつきます。また文書には、終了コード0で何も判断を出力しないフックはツール呼び出しを変えず、終了コード2は呼び出しを止めるとあります。したがって、記録の用途に終了コード2は使わないでください。

セッションの記録について:文書の例のパスは.jsonlで終わっているので、記録はJSON Lines形式で保存されていると考えられます。形式を文章で明言してはいないため、これは例からの推測です。また、記録ファイルは遅れて書かれることがあるとも書かれています。

次は、私が書いた記録用のスクリプトです。jqで項目を取り出し、鍵を隠します。

#!/bin/sh
# Append one redacted JSON line per tool call. Always exit 0.
jq -c '{
  ts: (now | todate),
  session: .session_id,
  event: .hook_event_name,
  tool: .tool_name,
  input: (.tool_input | tostring | gsub("sk-[A-Za-z0-9]{8,}"; "[REDACTED]")),
  ms: .duration_ms
}' >> "${TRAIL_FILE:-hook-trail.jsonl}"
exit 0

このスクリプトを、文書と同じ形の偽のJSONオブジェクトで動かしたところ、1行が書き出され、偽の鍵sk-test1234567890abcdは[REDACTED]に置き換わりました。実際のClaude Codeのセッションの中では動かしていません。 フックの宣言の書き方(matcherというキーや、設定ファイルの中の置き場所)はバージョンによって違うので、つなぐ前にお使いのバージョンの文書を読んでください。

4.2 共通の標準:OpenTelemetry#

項目名を自分で決めるより標準を使いたい場合、OpenTelemetryプロジェクトには、生成AIの処理(エージェントの操作やツール呼び出しを含む)の命名規約があります。私が読んだ解説によると、この規約はまだ「Development」の段階(安定版ではない)で、属性名はバージョン間で変わる可能性があります。私が開いた公式ページは、規約が別のリポジトリに移ったと伝えているだけなので、「Development」という状態は二次情報によるものです。公式のリポジトリで確認してください。 初心者は、まず自分で決めたJSON Linesの名前で始め、後で移行しても構いません。

ポイント: 記録する層はエージェントの外に置き、操作のたびに書き、保管する前に秘密を隠します。


5. 実際に動かした例:25件の操作、1つの失敗、1つの偽の鍵#

これは私が書いた、エージェントのセッションの模擬です(Python 3.14.8、jq 1.7.1)。操作そのものは用意したデータですが、記録する層、秘匿処理、ハッシュ連鎖、検索は実際に動かしたコードで、以下の結果は実際の出力です。2つの完全なスクリプトは末尾の付録にあります。

筋書き(例):エージェントが2つの依頼を続けて受けます。R1はチェックアウトに10パーセントの割引を足すこと。R2はテストを通すこと。セッションの途中で、エージェントは偽の鍵(API_KEY=sk-test1234567890abcd)を含む.envを読み、テストを1回失敗させ(操作12)、コードを直し、再実行して合格し、2回コミットします。

5.1 偽の鍵は隠れていた#

操作7が.envの読み取りです。残った行はこうです。

{"actor": "agent", "args": {"path": ".env"}, "exit_code": 0, "files_changed": [], "prev": "2efa822af966", "request": "R1: add a 10 percent discount to checkout", "seq": 7, "session": "s-001", "summary": "loaded [REDACTED] (3 variables)", "tool": "read_file", "ts": "2026-10-31T09:00:13+07:00"}

ファイル全体でsk-testを探すと、一致する行は0行でした。秘匿処理は単純な正規表現なので、予想した形の鍵しか捕まえられません。最初の防御線であって、唯一の防御線ではないと考えてください。

この記事を書くときに出会ったバグ: 私の最初の版は、引数をJSONの文字列に変えてから秘密を隠していました。password=abc123を含むコマンドを試すと、\S+というパターンが閉じの引用符と括弧まで飲み込み、行が途中で切れて、検索スクリプトが読み込み時にエラーになりました。直し方は、文字列の値を1つずつ、JSONに変える前に隠すことです。付録のスクリプトのredact関数がそうしています。

5.2 時系列#

== timeline ==
 1 09:00:03 read_file    exit=0 src/checkout.py
 2 09:00:05 read_file    exit=0 src/pricing.py
 3 09:00:07 search       exit=0 discount
 4 09:00:09 read_file    exit=0 tests/test_discount.py
 5 09:00:10 run_command  exit=0 git status --short
 6 09:00:11 run_command  exit=0 git switch -c agent/discount
 7 09:00:13 read_file    exit=0 .env
 8 09:00:33 edit_file    exit=0 src/pricing.py
 9 09:00:45 edit_file    exit=0 src/checkout.py
10 09:00:54 run_command  exit=0 python -m pytest -q
11 09:01:19 edit_file    exit=0 tests/test_discount.py
12 09:01:27 run_command  exit=1 python -m pytest -q
13 09:01:29 read_file    exit=0 tests/test_discount.py
14 09:01:44 edit_file    exit=0 src/pricing.py
15 09:01:52 run_command  exit=0 python -m pytest -q
16 09:01:53 run_command  exit=0 git diff --stat
17 09:01:54 run_command  exit=0 git add -A
18 09:01:56 run_command  exit=0 git commit -m 'feat: add 10 percent discount'
19 09:01:58 read_file    exit=0 README.md
20 09:02:08 edit_file    exit=0 README.md
21 09:02:09 run_command  exit=0 git add README.md
22 09:02:11 run_command  exit=0 git commit -m 'docs: mention discount'
23 09:02:19 run_command  exit=0 python -m pytest -q
24 09:02:20 run_command  exit=0 git log --oneline -3
25 09:02:24 final_answer exit=0 -

5.3 3つの調査の問い#

== first failure ==
12 2026-10-31T09:01:27+07:00 run_command ran 20 tests, 18 passed, 2 failed
== files changed ==
README.md: 1 edit(s)
src/checkout.py: 1 edit(s)
src/pricing.py: 2 edit(s)
tests/test_discount.py: 1 edit(s)
== edits to test files ==
11 R2: make the tests pass ['tests/test_discount.py']

読み方:最初の失敗は、09:01:27の操作12です。その直前(操作11)で、エージェントはテストファイルに22行を足しました。失敗のあとは、もう一度テストファイルをいじるのではなくsrc/pricing.pyを編集しています(操作14)。つまり記録は、エージェントがテストをコードに合わせたのではなく、テストが通るようにコードを直したことを示しています。操作14がテストファイルの編集だったら、そこを念入りに見るべき場所になります。

同じ問いをjqで(Pythonなしで):

jq -r 'select(.exit_code != 0) | "\(.seq) \(.ts) \(.summary)"' trail.jsonl
# 12 2026-10-31T09:01:27+07:00 ran 20 tests, 18 passed, 2 failed

jq -r '.files_changed[]' trail.jsonl | sort | uniq -c
#    1 README.md
#    1 src/checkout.py
#    2 src/pricing.py
#    1 tests/test_discount.py

5.4 書き換えられた行を見つける#

記録ファイルをコピーし、12行目の終了コードを1から0に変えて、失敗を「消し」ました。そのうえで連鎖の確認を再実行します。

== chain check ==
line 13: chain broken, an earlier line was changed
chain NOT ok
== first failure ==
none found

13行目には元の12行目の指紋が入っているため、12行目が変わると指紋が合わなくなります。最初の失敗の欄の「none found」は、連鎖の確認を飛ばすと、このセッションは問題なく動いたと結論してしまうことを示しています。

正直な限界: ハッシュ連鎖は検知するだけで、防止はしません。記録を書き換える人は、書き換えた行から後ろの連鎖をすべて計算し直せます。連鎖に意味を持たせるには、最後の行の指紋を、書き換える人の手が届かない場所(別のサービス、追記専用のシステム、第二の人)に保管する必要があります。


6. 保管量の計算(例、読み飛ばしてもよい)#

25行のファイルは7,170バイト、1行あたり約286.8バイトです。あるチームが1日に200セッション、各100操作だとします(計算のための仮定の数字です)。

per_line = 7170 / 25               # 286.8 bytes
per_day = 200 * 100 * per_line     # 5,736,000 bytes
print(round(per_day / 1e6, 2), round(per_day * 30 / 1e6, 1))
# 5.74 172.1   (MB per day, MB per 30 days)

1日約5.74MB、30日で172.1MBです。要約した記録なら、容量は大きな問題ではありません。もっと大きな問題は、誰が読めるかとどれだけ保管するかです。200と100は私の仮定で、286.8バイトはこの模擬で測った値です。出力が長い実際の記録はずっと重くなります。中身の全文を残さない理由はそこにあります。


7. 何かあった後に足跡を読み返す#

調査員がレシート帳を開くところを思い浮かべてください。順に5つの問いです。

  1. 帳面は無傷か。 ハッシュ連鎖を確認するか、別の場所の写しと比べます。
  2. いつ始まったか。 最初の失敗を探し、その直前の操作を見ます。
  3. 誰が、何が指示したか。 requestとactorの項目を見ます。
  4. どこまで及んだか。 変更ファイルと、外に効果が出るコマンド(コミット、プッシュ、削除)を一覧にします。
  5. 再発を防ぐものは何か。 これは記録の外にあります。障害の振り返りを書くことです。2017年にGitLabで誤ってデータを消した日の話は、あるチームが出来事の後に経緯を語り直した一例です。

記録が助けるのは1から4です。5は人間の仕事です。


8. 記録が壊れやすい場所#

ページが抜けたり、書き間違いや消しゴムの跡があるノートを思い浮かべてください。

よくある失敗 結果 防ぎ方
記録が少なすぎる(言葉だけでコマンドがない) エージェントが何をしたか分からない ツール、引数、終了コード、変更ファイルを残す
鍵や個人データを残す 記録が新しい漏えい元になる 書く前に隠し、偽の鍵で試し、読める人を絞る
誰も検索できない自由な文章 あっても無いのと同じ JSON Linesで1行に1記録、項目名を固定
エージェントが自分の記録を書く 抜け、間違い、書き換えが起こりうる 記録する層をエージェントの外に置き、追記専用に
エージェントが記録ファイルを編集、削除できる 必要なときに足跡が消える 追記専用の権限、または別の場所へ写しを送る、連鎖を確認
固定のパターン1つで秘密を隠す 別の形の鍵がすり抜ける いろいろな形の鍵で試し、秘匿は第1の層と考える
すべてを永久に保管する 容量を食い、プライバシーのリスクが増す 保管期間を決め、日付で見直す

9. 例:ある午前、2つの展開#

これは想定した状況で、実在の人物や製品には触れていません。

操作の記録がない場合:

  • 09:00。 開発者がエージェントに2つの仕事を任せ、11:30まで会議に入ります。
  • 11:35。 開発者はブランチに2つのコミットを見つけます。同僚が聞きます。「なぜテストファイルが変わったの?」
  • 11:40。 開発者は長い会話を開いてスクロールします。エージェントは「直しました」と言っていますが、どのファイルをどの順で直したかは書いていません。
  • 12:20。 開発者は自信がないまま推測で答えます。

操作の記録がある場合:

  • 09:00。 同じ2つの仕事。記録する層はエージェントの外で動いています。
  • 11:35。 同僚が同じことを聞きます。
  • 11:37。 開発者が「編集されたテストファイル」を検索します。操作11(09:01:19)が新しいテストを足し、操作14は、テストではなくコードを直しています。
  • 11:40。 操作番号つきで答えが返ります。

どちらの場合も大事なこと:答えを記憶ではなく、番号のついた操作に結びつける。これは例なので、頻度については何も証明しません。根拠は末尾の出典にあります。


10. 手を動かす練習(答えつき)#

付録の2つのスクリプトを動かし、次の4つをしてみてください。

  1. 失敗したrun_commandは何件で、何番の操作でしたか。
  2. 2回以上編集されたファイルはどれですか。
  3. stepsの一覧の末尾に、echo password=abc123とecho pwd=abc123の2つの操作を足します。再実行して、abc123を含む行がファイルに何行あるか数えてください。
  4. ファイルの途中の1行を手で書き換え、連鎖の確認を動かします。どの行が壊れていると報告されますか。

答え:

  1. 失敗は1件で、操作12です。
  2. src/pricing.pyで、2回編集されています(操作8と14)。
  3. abc123を含む行は、ちょうど1行です。password=abc123のコマンドはecho [REDACTED]に隠れ、pwd=abc123は、パターンにpwdという語がないためすり抜けます(私が実際に動かして、結果は1行でした)。固定のパターンによる秘匿の限界を示す例です。
  4. 書き換えた行の直後の行です。その行が元の行の指紋を持っているからです。

11. 使った数字と文書#

出典 内容 読み方 注意
Anthropic、2026年版エージェント型コーディングのトレンド報告書の紹介ページ 生産性と、監督、品質、セキュリティのバランス。複数エージェントの連携を含む 監督が話題になっている 読めたのは紹介ページだけで、全文はフォームの先。報告書からは数字を取っていない
Stack Overflow Developer Survey 2026 69.9%が未知の課題にエージェントに聞く エージェントは日常の道具 要約を通じて読んだもの。回答者は回答を選んだ人
Claude Codeのフックの文書 ツール呼び出しの前後のフック、JSONの項目、終了コード0と2、.jsonlの記録 第4.1節の根拠 JSONLは例のパスからの推測。実際のセッションでは未実行
OpenTelemetry GenAIに関する二次的な記事 規約はDevelopmentの段階で、属性名はまだ変わる 第4.2節の根拠 二次情報。公式ページは、リポジトリが移ったとだけ伝えている

正しく読むために: 操作の記録が調査時間をどれだけ短縮するかを測った研究は、見つけられませんでした。この記事の主張は、記録の仕組みから導いた良い実践であり、実験結果ではありません。


12. チェックリスト:エージェントの記録#

  1. エージェントの外に記録する層がありますか。それとも、エージェントに「自分で記録して」と頼んだだけですか。
  2. 各行に、時刻、セッション、実行者、元の依頼、ツール、結果、変更ファイルがありますか。
  3. 書く前に秘密を隠していますか。偽の鍵で試しましたか。
  4. 個人データやファイルの全文を残していませんか。
  5. 記録は追記専用ですか。それともエージェントが編集できますか。
  6. 最後の指紋か写しを、別の場所に保管していますか。
  7. 3つの問い(時系列、最初の失敗、変更ファイル)の検索例がありますか。
  8. 誰が読めて、どれだけ保管し、見直しはいつですか。

13. バランスよく:記録でできること、できないこと#

できること: 出来事の再現を短くし、「エージェントは何時に何をしたか」に答え、同僚に番号つきの根拠を渡し、連鎖を確認すれば書き換えられた行を見つけられます。

できないこと:

  • 記録は、結果が正しいかどうかを示しません。成功したコマンドが間違ったことをしている場合もあります。正しさには、「AIの結果に証拠を添えさせる」のように回答に添えた証拠、テスト、人間の読み手が引き続き必要です。
  • 被害を防ぎません。後から知る助けになるだけです。リスクの高い作業には、権限の制限と戻る手段も必要です。「エージェントを使う人のためのGit入門」のセーブポイントがその例です。
  • 固定パターンの秘匿は、まだ思いついていない形の鍵を見逃します。
  • 記録は容量を使い、誰が読むか、どれだけ保管するかというプライバシーへの配慮が要ります。
  • 実際に動かした例は、1つのセッションの模擬にすぎません。実際のツールの記録には、もっと多くの項目と例外があります。

誇張しないでください: 記録があることは、安全であることを意味しません。軽く見ないでください: 最も必要なとき、つまり問題が起きたときに、頼れる最後のものになることが多いのです。


14. まとめ#

エージェントに仕事を任せて会議に出るのは普通のことです。普通でないのは、戻ってきたときに何をしたか分からないことです。別の層が書くレシート帳を持たせましょう。必要な分だけ残し、秘密を隠し、追記だけを許し、読み返し方を知っておく。記録は証拠やテストの代わりにはなりませんが、調査の午後を数分に縮めてくれます。


付録:2つの完全なスクリプト(読み飛ばしてもよい)#

agent_trail_demo.py:セッションを模擬して記録を書きます。

import hashlib
import json
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

LOG = Path(sys.argv[1] if len(sys.argv) > 1 else "trail.jsonl")
SECRET = re.compile(r"sk-[A-Za-z0-9]{8,}|(password|token|api_key)\s*[=:]\s*\S+", re.I)


def redact(value):
    """Hide secrets in every string inside a value, before it is turned into JSON."""
    if isinstance(value, str):
        return SECRET.sub("[REDACTED]", value)
    if isinstance(value, dict):
        return {k: redact(v) for k, v in value.items()}
    return value


class Trail:
    """Append-only JSON Lines trail. Each line carries the hash of the previous line."""

    def __init__(self, path, session):
        self.path, self.session, self.prev, self.seq = path, session, "start", 0
        self.clock = datetime(2026, 10, 31, 9, 0, 0, tzinfo=timezone(timedelta(hours=7)))
        path.write_text("")

    def record(self, request, tool, args, exit_code, summary, files=(), seconds=2):
        self.clock += timedelta(seconds=seconds)
        self.seq += 1
        line = {
            "seq": self.seq,
            "ts": self.clock.isoformat(),
            "session": self.session,
            "actor": "agent",
            "request": request,
            "tool": tool,
            "args": redact(args),
            "exit_code": exit_code,
            "summary": redact(summary),
            "files_changed": list(files),
            "prev": self.prev,
        }
        text = json.dumps(line, ensure_ascii=False, sort_keys=True)
        self.prev = hashlib.sha256(text.encode()).hexdigest()[:12]
        with self.path.open("a") as f:
            f.write(text + "\n")


trail = Trail(LOG, "s-001")
R1 = "R1: add a 10 percent discount to checkout"
R2 = "R2: make the tests pass"
steps = [
    (R1, "read_file", {"path": "src/checkout.py"}, 0, "120 lines", (), 3),
    (R1, "read_file", {"path": "src/pricing.py"}, 0, "80 lines", (), 2),
    (R1, "search", {"pattern": "discount"}, 0, "4 matches in 2 files", (), 2),
    (R1, "read_file", {"path": "tests/test_discount.py"}, 0, "45 lines", (), 2),
    (R1, "run_command", {"cmd": "git status --short"}, 0, "clean", (), 1),
    (R1, "run_command", {"cmd": "git switch -c agent/discount"}, 0, "switched to new branch", (), 1),
    (R1, "read_file", {"path": ".env"}, 0, "loaded API_KEY=sk-test1234567890abcd (3 variables)", (), 2),
    (R1, "edit_file", {"path": "src/pricing.py"}, 0, "+14 -2 lines", ("src/pricing.py",), 20),
    (R1, "edit_file", {"path": "src/checkout.py"}, 0, "+6 -1 lines", ("src/checkout.py",), 12),
    (R1, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 18 tests, 18 passed", (), 9),
    (R2, "edit_file", {"path": "tests/test_discount.py"}, 0, "+22 -0 lines", ("tests/test_discount.py",), 25),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 1, "ran 20 tests, 18 passed, 2 failed", (), 8),
    (R2, "read_file", {"path": "tests/test_discount.py"}, 0, "67 lines", (), 2),
    (R2, "edit_file", {"path": "src/pricing.py"}, 0, "+3 -1 lines", ("src/pricing.py",), 15),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 20 tests, 20 passed", (), 8),
    (R2, "run_command", {"cmd": "git diff --stat"}, 0, "3 files changed, 45 insertions, 4 deletions", (), 1),
    (R2, "run_command", {"cmd": "git add -A"}, 0, "staged 3 files", (), 1),
    (R2, "run_command", {"cmd": "git commit -m 'feat: add 10 percent discount'"}, 0, "1 commit created", (), 2),
    (R2, "read_file", {"path": "README.md"}, 0, "30 lines", (), 2),
    (R2, "edit_file", {"path": "README.md"}, 0, "+5 -0 lines", ("README.md",), 10),
    (R2, "run_command", {"cmd": "git add README.md"}, 0, "staged 1 file", (), 1),
    (R2, "run_command", {"cmd": "git commit -m 'docs: mention discount'"}, 0, "1 commit created", (), 2),
    (R2, "run_command", {"cmd": "python -m pytest -q"}, 0, "ran 20 tests, 20 passed", (), 8),
    (R2, "run_command", {"cmd": "git log --oneline -3"}, 0, "3 commits listed", (), 1),
    (R2, "final_answer", {}, 0, "reported done, branch agent/discount", (), 4),
]
for step in steps:
    trail.record(*step)
print(f"wrote {trail.seq} lines to {LOG}")

query_trail.py:連鎖を確認して検索します。

import hashlib
import json
import sys
from collections import Counter

path = sys.argv[1] if len(sys.argv) > 1 else "trail.jsonl"

print("== chain check ==")
prev, ok = "start", True
for i, line in enumerate(open(path), 1):
    if not line.strip():
        continue
    if json.loads(line)["prev"] != prev:
        print(f"line {i}: chain broken, an earlier line was changed")
        ok = False
        break
    prev = hashlib.sha256(line.rstrip("\n").encode()).hexdigest()[:12]
print("chain ok" if ok else "chain NOT ok")

rows = [json.loads(line) for line in open(path) if line.strip()]

print("== timeline ==")
for r in rows:
    args = r["args"]
    arg = args.get("cmd") or args.get("path") or args.get("pattern") or "-"
    print(f'{r["seq"]:>2} {r["ts"][11:19]} {r["tool"]:<12} exit={r["exit_code"]} {arg}')

print("== first failure ==")
first = next((r for r in rows if r["exit_code"] != 0), None)
print(f'{first["seq"]} {first["ts"]} {first["tool"]} {first["summary"]}' if first else "none found")

print("== files changed ==")
counts = Counter(f for r in rows for f in r["files_changed"])
for name, n in sorted(counts.items()):
    print(f"{name}: {n} edit(s)")

print("== edits to test files ==")
for r in rows:
    if any(f.startswith("tests/") for f in r["files_changed"]):
        print(r["seq"], r["request"], r["files_changed"])

実行:python3 agent_trail_demo.py trail.jsonl、続けてpython3 query_trail.py trail.jsonl。模擬の時計は固定なので、このスクリプトをそのまま使えば、あなたのファイルの指紋(prevの項目)は私のものと一致します。


参考資料#

数字は2026年10月時点の公開情報によります。二次情報も含まれるため、引用する前に原典を開いて確認してください。

よくある質問

操作の記録は、保存した会話と何が違いますか?
会話は、エージェントが何を言ったかを示します。操作の記録は、何をしたかを示します。どのコマンドが走り、どのファイルが変わり、どれが失敗し、何時だったかです。両者は補い合いますが、構造のある記録だけが素早く検索できます。
1回の操作にどんな項目を残すべきですか?
最低限、時刻、セッション、実行者、元の依頼、ツールまたはコマンド、機微な値を隠した引数、終了コードか結果の要約、変更したファイルの一覧です。秘密情報、個人データ、ファイルの全文は残してはいけません。
Claude Codeには記録をつなぐ場所がありますか?
現行の文書によると、Claude Codeにはツール呼び出しの前後に動くフックがあり、tool_name、tool_input、tool_response、tool_use_id、session_id、transcript_pathなどのJSONを標準入力で受け取ります。文書の例では、セッションの記録は.jsonlファイルに保存されます。私は記録用の断片を偽の入力でだけ試し、実際のClaude Codeのセッションの中では動かしていません。
エージェントが自分の記録を書き換えていないと、どう確かめますか?
追記だけできて編集や削除ができない場所に置くか、別の場所へ送ります。各行に前の行の指紋を持たせるハッシュ連鎖は、書き換えられた行を見つけられますが、最後の指紋を別の場所に保管している場合に限ります。これは検知の方法であり、防止の方法ではありません。
記録があれば、エージェントが正しいことをしたと証明できますか?
できません。記録は何が起きたかを示すだけで、結果が正しいかどうかは示しません。正しさの確認には、回答に添えた証拠、テスト、人間の読み手が引き続き必要です。