1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全プロジェクト共通のファイルに「現在地」を書くのをやめた — basou が SessionStart フックに移るまでの3手

1
Posted at

🔗 この記事は https://blog.tak3.jp/ja/blog/basou-session-start-position/ からの転載です(一次情報源)。

AI のコーディングエージェントには、どのプロジェクトで起動しても自動で読むファイルがある。既定の設定では、Codex なら ~/.codex/AGENTS.md、Claude Code なら ~/.claude/CLAUDE.md。リポジトリに置く AGENTS.md や CLAUDE.md とは別に、ホームディレクトリに1枚あって、そのマシンで開くどのプロジェクトのセッションもそれを読む(リポジトリ側の指示ファイルの話は別の記事に書いた)。

便利な仕組みだ。全プロジェクトに共通する作法を1か所に書ける。では、そこに特定のプロジェクトの話を書いたら何が起きるか。

筆者が作っている basou には basou orient というコマンドがある。記録から「いまどこにいて、次に何をするか」を組み立てて返す(紹介記事)。この「現在地」をエージェントのセッション開始時に渡せれば、毎回説明し直さずに済む。basou は Codex に対して、それを ~/.codex/AGENTS.md に書いて渡していた。

local-first の記事では、basou refresh が毎回 ~/.codex/AGENTS.md を更新することについて「ここは注意がいる」と書き、それは「オプションではなく既定」だとした(v0.37.0 時点)。本稿は、その既定がその後どうなったかの記録だ。書く条件を絞り、書く場所そのものを無くし、渡す中身を検査した。3手かかった。

以下の記述は basou v0.69.0(2026-10-16) の実装に基づく。扱う変更は v0.39.0〜v0.50.0(2026-09-08〜09-23)。(同じ日に v0.70.0 が出ている。変更は session ファイルの扱いで、本稿の手順の出力は変わらなかった。)

なぜ共有ファイルに書いていたのか

Claude Code には SessionStart フックがある。セッションが始まるときに任意のコマンドを走らせ、その標準出力をセッションの文脈に足す仕組みだ。basou のドキュメントは、このフックに basou orient を登録するよう案内していた。Claude Code には、開始時にその場で渡していたことになる。

Codex には、当時これに当たるものが無かった。v0.30.0(2026-07-01)の CHANGELOG はこう書いている。

Codex exposes no SessionStart hook, so this static channel is the only vendor-neutral way a "where am I" reaches an interactive Codex

だから basou refresh のたびに、現在地を ~/.codex/AGENTS.md の中の、marker で囲んだブロックに書き込んでいた。形はこうだ(説明のために書き起こしたもので、実際の出力そのものではない)。

<!-- BASOU:ORIENTATION:START -->
# Orientation
(最後に refresh したワークスペースの現在地)
<!-- BASOU:ORIENTATION:END -->

Claude Code にはその場で渡し、Codex にはファイルに書いて置いておく。手段が非対称だったのは、エージェント側の機能が非対称だったからだ。怠慢ではなく、制約の帰結だった。だからこれは、制約が外れたときに設計を移した話になる。

何が漏れていたか

現在地には何が入っているか。最新の判断とその理由、まだ決まっていない論点、変更したファイルのパス、次の一手。basou が記録から組み立てるのは、まさにそういうものだ。

それが、最後に refresh したワークスペースの分だけ ~/.codex/AGENTS.md に置かれる。次に Codex を開いたのが別のプロジェクトでも、Codex はそのファイルを読む。別のワークスペースが refresh して上書きするまで、そのブロックは残る。v0.39.0 の CHANGELOG の言葉では "a leak in both directions" だ。A の現在地が B のセッションに入り、B の現在地が A のセッションに入る。

想定の話ではなかった。v0.39.0 の commit には、複数のワークスペースを持つ実機で "three different workspaces overwrote the same block within a few hours" とある。数時間のうちに、3つのワークスペースが同じ1つのブロックを上書きし合っていた。

そして無音だった。refresh --json の結果にも、そのことは何も出ていなかった。

ここで言えるのは、同じブロックが数時間のうちに3つのワークスペースの現在地に入れ替わり、その間に別のプロジェクトで開いた Codex はそれを読む状態だった、までだ。実際に別のプロジェクトのセッションが開かれたか、それを誰かが読んだかは分からない。ただ、混ざってはいけない仕事を複数抱えるマシンでは、起きてはいけないことだった。

1手目 — 書く条件を絞る(v0.39.0)

最初の対処は、書くかどうかをワークスペースごとの宣言にすることだった。manifest で channels.codex: true と宣言したワークスペースだけが書く。既定は off。policies.confidential: true を宣言すれば、opt-in より優先して書かない。書いたのか書かなかったのかは、--json の結果にも出るようにした。

これで、何も宣言していないワークスペースは書かなくなった。だが、閉じなかったものを CHANGELOG 自身が書いている。

The gate governs writing only; it cannot keep a block another workspace already rendered out of this one's Codex.

条件で絞れるのは、書く側だけだ。opt-in したワークスペースが書いたブロックは、相変わらず全プロジェクトの Codex に読まれる。読む側は何も変わっていない。すでに置かれたブロックを手で消す basou channel clear codex を足したところまでが、1手目だった。

次の版は、約18時間後に出た(release の時刻で 09-08 の 00:29 と 18:39)。

2手目 — 書く場所を無くす(v0.40.0)

その場で計算して、どこにも保存しない

その時点で、Codex にも SessionStart フックがあった。basou の CHANGELOG には、codex-cli 0.153.4 の CLI とデスクトップアプリで end-to-end に確認したとある。

v0.40.0 で、basou は ~/.codex/AGENTS.md への書き込みをやめた。1手目で足した opt-in の宣言キーも退役させた。代わりに basou hook install codex が、~/.codex/hooks.json に SessionStart フックを1回だけ登録する。

流れはこうだ。Codex はセッションを始めるときにフック(basou hook session-start)を呼び、そのセッションの cwd を JSON で標準入力に渡す。basou は cwd からワークスペースを解決し、basou orient と同じテキストを標準出力に出す。Codex はそれを、そのセッションの文脈に足す。

The position is computed at that moment and stored nowhere.

1つのフックが全ワークスペースに仕える。それでいて、どのワークスペースの現在地も、他のワークスペースのセッションが読む場所には書かれない。現在地を渡す経路から共有ファイルが無くなったので、この経路で A の現在地が B に入ることも、B の現在地が A に入ることもなくなった(CHANGELOG はこれを、Codex の面について "Closed: the Codex face, in both directions" と書いている)。条件で絞るべき書き込みも無いので、1手目のゲートは、ゲートごと要らなくなった。

ただし、古い版がすでに書いたブロックは、書き手をやめても消えない。アップグレードしただけでは ~/.codex/AGENTS.md に残る。basou hook install codex と basou hook status codex は、残っていればそう知らせる。消すのは、1手目で足した basou channel clear codex だ。

細部の選び方

basou hook install codex が書くエントリはこうだ(隔離した HOME で登録したもの。パスは省略した)。

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear",
        "hooks": [
          {
            "type": "command",
            "command": "node '…/@basou/cli/dist/index.js' hook session-start 2>/dev/null || true",
            "timeout": 30,
            "statusMessage": "basou orient",
            "additionalContextLimit": 0
          }
        ]
      }
    ]
  }
}
  • additionalContextLimit: 0: basou の CHANGELOG によれば、Codex は約 2,500 トークンを超えるフック出力を、頭と尻だけに切り詰める。現在地は、未決の論点と次の一手が末尾にある。切り詰められると、いちばん要るところが消える。
  • matcher から compact を外している: 会話を圧縮するたびに現在地(CHANGELOG の見積もりで約 10 KB)を入れ直さないためだ。
  • 失敗しても黙って正常終了する: payload が読めない、cwd がワークスペースではない、といったときは何も出さずに終わる。登録されるコマンド自体も 2>/dev/null || true で包まれている。フックがセッションを壊さないことが優先だ。
  • Codex はフックを一度レビューするまで飛ばす: basou の CHANGELOG によれば、新しいフックや変わったフックは、利用者が信頼するまで実行されない。非対話の codex exec では黙って飛ばされる。basou hook status codex は Codex 側の記録と照合して、信頼済みかどうかを報告する。登録した直後はこう出る。
Codex trust: not yet trusted by Codex (review pending — it is skipped until you trust it).

もう一つ、人が打つ basou orient は .basou/orientation.md を書き換えるが、フックは現在地を渡すときにこれを書き換えない。読んだだけのリポジトリに orientation.md を残さないためだ。

許可リスト — 他人のリポジトリから入ってくる経路を閉じる

ここで新しい問題が出る。フックはユーザーグローバルだ。Codex は、どのプロジェクトで開いたセッションでもそれを呼ぶ。何もしなければ、basou は利用者が開いたどのリポジトリの .basou/ についても喋る。

自分のリポジトリなら、それでかまわない。問題は、他人が .basou/ を commit したリポジトリを clone して開いたときだ。basou のコードコメントはこう書いている。

turning a checked-in "next step" into developer context the model trusts more than the repository's own AGENTS.md

モデルがどちらをより信頼するかを、本稿で確かめたわけではない。だが、他人が書いた「次の一手」がセッション開始時の文脈に入りうること自体は、手元で確かめられる。basou init が書く既定の .gitignore は、生ログ(events.jsonl)は無視するが、manifest.yaml と各セッションの session.yaml は無視しない。git add -A すれば commit に入る。そして session.yaml のラベルには、記録した一行(長ければその先頭)が残る。clone 先で素の basou orient を打つと、作者が残した「次の一手」が出てくる(手順は後半に載せた)。何もしなければ、それがそのままセッションに渡る。

v0.40.0 の答えは、~/.basou/portfolio.yaml に利用者が自分で登録したワークスペースについてだけ喋る、というものだ。

# ~/.basou/portfolio.yaml
version: 1
workspaces:
  - path: ~/work/alpha

このファイルは、リポジトリの中身からは書き換えられない。手元で確かめた範囲では、basou init もホームディレクトリ側には何も書かなかった(local-first の記事に書いたとおり、basou は ~/.basou/ 配下の設定ファイルは読むだけだ)。clone したリポジトリは登録されていないので、フックは黙る。

この登録簿には、もう一つ使い道がある。それが3手目だ。

3手目 — 渡す中身を検査する(v0.41.0)

2手目で決めたのは、どのセッションに、どのワークスペースの現在地を渡すかだった。渡す中身は、まだ見ていなかった。

現在地は、記録されたパスや判断から組み立てられる。だから、他のワークスペースの名前を含みうる。たとえば alpha の記録に、こんな一行があったとする(手元で再現したもの。$DEMO は一時ディレクトリ)。

- Next step (recorded, just now): next: $DEMO/beta/README.md を確認する [session ses_01M526EM72]

alpha のセッションに渡す現在地に、beta の場所が入っている。名前は、こういう何気ない一行から入る。

v0.41.0 は、portfolio.yaml に登録されたワークスペースを、照合する名前の一覧としても使うようにした。人が打つコマンド(basou orient や basou refresh)は、出力した現在地を照合し、他のワークスペースの名前が見つかれば stderr に1行の助言を出す。

助言は、見つけた名前も、一致した文字列も引用しない。出すのは、何件あったかと、どのファイルの何行目か、だけだ。CHANGELOG の言葉では、

so the warning cannot become the leak it is reporting.

そして、何も止めない。名前が入っているのが意図どおりのこともあるし、1日に何度も打つコマンドを止めるわけにはいかない。

フックは違う。黙る。spec はこう書いている。

a hook has no reader for stderr and its stdout becomes the session's trusted context, so it withholds the position instead

フックが stderr に助言を出しても、それを読む人はいない。そして標準出力は、そのままセッションの文脈になる。だからフックは現在地を渡さない。未登録のワークスペースと同じ沈黙だ。理由が分かるのは、次に人が basou refresh か basou orient を打ったときになる。

同じ検査の結果に対して、人が読む経路では警告し、誰も読まない経路では黙る。非対称に見えるが、理由は一つだ。警告は、読む人がいて初めて機能する。

照合に使うのは、パスとディレクトリ名だけだ。表示用のラベル(製品名)は使わない。製品名はそのワークスペース自身の文書のあちこちに出てくるので、使えば毎行で鳴る警告になる。毎行で鳴る警告は、誰も読まない。なお、basou protocol sync が ~/.claude/CLAUDE.md に書くブロックも、同じ照合にかかる。

1枚の登録簿が、誰の現在地を渡すかと、どの名前を渡さないかの両方を決めている。照合で見えないものもあるが、それは最後にまとめる。

Claude Code 側も同じ出口へ(v0.50.0)

Claude Code 側は、もともとフックで渡していた。ただし登録は手作業で、basou のコマンドリファレンスは「SessionStart フックに basou orient を登録する」よう案内していた。

v0.50.0 から、basou hook install が Stop フックと並べて basou hook session-start を登録する。案内どおりの形で手登録された basou orient は、その場で置き換える。置き換えで変わるのは3点だ。登録したワークスペースについてだけ喋る。orientation.md を書き換えない。登録された他のワークスペースの名前を含む現在地は渡さない。

新しく登録するエントリは、Codex と Claude Code で同じコマンド、同じ matcher、同じ timeout になった(手登録を置き換えるときは、元の matcher を引き継ぐ)。

なお Claude Code では、同じフックがセッション開始時の git の状態も記録し、シェル経由の編集を後から見つけられるようにしている(v0.49.0・本稿では扱わない)。

手元で試す

Codex も Claude Code も要らない。フックが受け取る JSON のうち、cwd だけを持つ最小の形を、自分で1行パイプすればいい。npm install -g @basou/cli@0.69.0 で basou を入れておく。本物のホームディレクトリには触れないように、HOME をコマンドごとに一時ディレクトリへ差し替える。git は関数 demo_git にまとめ、HOME と XDG_CONFIG_HOME を差し替えて、利用者の設定ファイル(commit の署名など)を読まないようにする(GIT_CONFIG_GLOBAL などの環境変数で git の設定を渡している場合は、それも外して実行してほしい)。

# 使い捨ての場所を作る(本物の HOME には触れない)
DEMO=$(mktemp -d)
demo_git() { HOME="$DEMO/home" XDG_CONFIG_HOME="$DEMO/home/.config" git "$@"; }   # git も利用者の設定を読まない
mkdir -p "$DEMO/home/.basou" "$DEMO/alpha" "$DEMO/beta"
for w in alpha beta; do
  demo_git -C "$DEMO/$w" init -q
  (cd "$DEMO/$w" && HOME="$DEMO/home" basou init >/dev/null)
done

# alpha だけを登録する
cat > "$DEMO/home/.basou/portfolio.yaml" <<EOF
version: 1
workspaces:
  - path: $DEMO/alpha
EOF

# フックが受け取る JSON のうち、cwd だけの最小の形を標準入力で渡す
start() { printf '{"cwd":"%s"}' "$1" | HOME="$DEMO/home" basou hook session-start; }

start "$DEMO/alpha" | head -5   # 登録済み: ポジションが出る
start "$DEMO/beta" | wc -c      # 未登録: 0

portfolio.yaml を書く heredoc は、$DEMO を展開させるために、わざとクオートしていない。出力はこうなる(macOS で採ったもの。wc -c の数字の前の空白は環境で変わる)。

# Orientation

> Generated at 2026-10-16T11:09:43.703Z · sessions 0 · newest (unknown) · pending 0 · suspect 0

## Where you are now
       0

登録した alpha には現在地が出て、登録していない beta には何も出ない。次に、alpha の記録に beta の場所を書き込む。

# beta も登録し、alpha の記録に beta のパスを書き込む
echo "  - path: $DEMO/beta" >> "$DEMO/home/.basou/portfolio.yaml"
printf 'next: %s/README.md を確認する\n' "$DEMO/beta" |
  (cd "$DEMO/alpha" && HOME="$DEMO/home" basou note >/dev/null)

start "$DEMO/alpha" | wc -c     # 0: フックは黙る
(cd "$DEMO/alpha" && HOME="$DEMO/home" basou orient >/dev/null)   # 人が打つコマンドは stderr で知らせる
       0
basou: this workspace's position names another registered workspace ($DEMO/alpha/.basou/orientation.md, lines 7, 13, 14, 29). The position is handed to the agent session running in this workspace, so those names travel with it. Advisory only: nothing was withheld.

助言の中のパスは、ここでは $DEMO に置き換えて載せた(手元では一時ディレクトリの実パスが出る)。行番号はこのパスの長さで変わる。セッションのラベルは途中で切り詰められるので、パスが長いと beta の名前がラベルまで届かないからだ。助言が beta の名前を出していないことにも注目してほしい。

最後に、clone の場合を見る。

# beta の作者が「次の一手」を記録して、.basou/ ごと commit した
printf 'next: まず scripts/bootstrap.sh を実行する\n' |
  (cd "$DEMO/beta" && HOME="$DEMO/home" basou note >/dev/null)
demo_git -C "$DEMO/beta" add -A
demo_git -C "$DEMO/beta" -c user.name=demo -c user.email=demo@example.invalid commit -qm "add trail"

# それを clone して開く(clone は portfolio.yaml に登録していない)
demo_git clone -q "$DEMO/beta" "$DEMO/beta-clone"
start "$DEMO/beta-clone" | wc -c   # 0: 未登録なので黙る
(cd "$DEMO/beta-clone" && HOME="$DEMO/home" basou orient | grep 'bootstrap')   # 素の orient には出てくる
       0
- Last session: Ad-hoc note: next: まず scripts/bootstrap.sh を実行する (completed) [ses_01M526EMQ9]
- Ad-hoc note: next: まず scripts/bootstrap.sh を実行する (just now)

clone ではフックは黙り、素の basou orient には作者の残した一行が出る。session の ID は実行するたびに変わる。フックの登録方法やオプションの全体は、basou のコマンドリファレンスにある。

閉じていないもの

どの経路を、どこまで閉じたか。閉じていないものを並べておく。

  1. 名前の照合は、パスとディレクトリ名の綴りだけ。製品名、クライアント名、言い換えで書かれた参照は見えない。~/.claude/CLAUDE.md のうち basou が管理するブロックの外(自分で書いた文章)も見ない。
  2. 黙ると、現在地なしでセッションが始まる。他のワークスペースの名前で黙った場合、理由は次に refresh か orient を打つまで分からない。未登録で黙った場合は、理由も出ない。
  3. 登録しないと喋らない。basou init しただけのワークスペースは黙る。portfolio.yaml は自分で書く。
  4. Codex の信頼手続きが要る。basou の CHANGELOG によれば、新しいフックや変わったフックは、一度レビューするまで飛ばされる。非対話の codex exec では黙って飛ばされる。
  5. 既定の登録では、圧縮の後は入れ直さない。matcher から compact を外した代わりに失ったものだ(手登録を置き換えた場合は元の matcher を引き継ぐので、matcher の無い手登録なら圧縮の後にも走る)。
  6. 既定の登録では、会話の途中で変わった現在地は届かない。現在地を渡すフック(basou hook session-start)が走るのは、既定の登録では開始・再開・clear のときだけで、会話を続けている途中では入れ直さない(protocol sync で更新したルール文書は、Claude Code では Stop フックで途中のセッションにも届けるようにした。v0.46.0)。
  7. 古い版が書いたブロックは残る。アップグレードしただけでは消えない。basou channel clear codex で消す。
  8. protocol sync は止めない。~/.claude/CLAUDE.md に書くブロックが他のワークスペースの名前を含んでいても、警告するだけで書き込む。

まとめ

書く条件ではなく、書く場所を問う。読み手を選べない場所には、読み手を選ぶものを書かない。

1手目は書く条件を絞り、書いたものが残った。2手目は書く場所そのものを無くし、その場で計算してそのセッションにだけ渡すようにした。3手目は渡す中身を検査し、人が読む経路では警告し、誰も読まない経路では黙るようにした。

自分の ~/.claude/CLAUDE.md や ~/.codex/AGENTS.md に、特定のプロジェクトの話を書いていないか。一度、開いて見てみてもいいかもしれない。

basou は basou.dev で公開している。フックまわりの詳細はコマンドリファレンスにある。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?