[ Token Economy ]

出力先規約と paths SSOT — AI が成果物を散らかさないための設計

AI の自動実行による成果物散乱を防ぐには、出力パスを 1 ファイルで一元管理し、ディレクトリ規則を明示し、hook でブロックする 3 層設計が効果的である。

Author: 森本拓見 Updated: May 11, 2026
#claude-code #ai-driven-dev #ssot #code-quality

課題:出力先のカオス化

AI は速く動く。速く動くほど、成果物が散らかる。「出力先どこにする?」と聞かれれば AI は何かしら答える。問題は、その「何かしら」が /tmp~/Desktop、プロジェクト直下、場合によっては ./output/output/ のような二重ディレクトリだったりすることだ。パイプラインが複雑になるほど、誰も指定しなければ成果物は分散していく。

八雲の動画制作パイプライン(Montage)は複数のエージェントがリレー形式で成果物を引き渡す。Claude Code で自律実行させると、3 つの典型的な詰まりパターンに直面した。

/tmp 汚染 エージェントが中間ファイルを /tmp 以下に書く。OS 再起動で消える。パイプラインの途中から再開しようとしても、前ステップの出力がない。ディスク節約を意図した合理的な判断だが、パイプラインの連続性を破壊する。

プロジェクト直下への散乱 指示が曖昧なとき、AI はカレントディレクトリ直下に書く。input.jsonanalysis.json がプロジェクトルートに転がり、コミット事故が発生しやすくなった。パスの不一致 あるエージェントが output/{topic-id}/final.json に書き、次が outputs/{topic-id}/final.json を読もうとして失敗する。outputoutputs の 1 文字差で詰まる。パスが 2 箇所以上に定義されていれば、必ずいつか乖離する。

根本原因は一つだ。成果物のパスが SSOT(Single Source of Truth)として管理されておらず、各エージェントが独自に決定している。

アプローチ:paths SSOT を 1 ファイルに集約

解決策として導入したのが src/config/paths.ts だ。全アウトプットのパスをここで定義し、パイプライン全体がこのファイルを参照する。

// src/config/paths.ts より抜粋
export const MONTAGE_ROOT =
 process.env.MONTAGE_ROOT ?? path.join(homedir(), "Desktop", "montage");

export const topicDir = (channelId: string, topicId: string) =>
 path.join(MONTAGE_ROOT, channelId, topicId);

export const tempDir = (sub?: string) => path.join(MONTAGE_ROOT, "_temp", sub ?? "");
export const cacheDir = (sub?: string) => path.join(MONTAGE_ROOT, "_cache", sub ?? "");

topicDir("yakumo-tech", "2026-05-001") と呼べば正しいパスが返る。呼び出し側は「どこに書くか」を考える必要がなくなる。CLAUDE.md でこの SSOT を明示することで、エージェントは「パスを自分で考えず src/config/paths.ts を参照する」という動作を安定して取るようになった。

ディレクトリ規則:_ プレフィックスで意味を持たせる

パスの定義と合わせて _ プレフィックス規則を導入した。

~/Desktop/montage/
├── {channelId}/{topic-id}/ ← 永続成果物(保護)
├── _temp/ ← 一時作業領域
├── _cache/ ← 再生成可能なキャッシュ
└── _archive/{YYYY-MM}/ ← 過去散乱物の退避先

_ プレフィックスなし = 保護対象。_ 付き = 一時・キャッシュ・作業領域。 ファイルブラウザで保護対象と一時領域が視覚的に分かれるため、人間オペレーターの判断コストが下がる。エージェントもキャッシュの置き場所に迷わなくなり、/tmp に手を出すケースが削減された。さらに rm -rf ~/Desktop/montage/_* で一括掃除できるため、メンテナンスの手軽さも向上した。

ガードレール:PreToolUse hook でブロック

設計だけでは不十分だ。AI は指示された規則を忘れることがある。特に長いコンテキストの後半では、CLAUDE.md に書いたルールが薄まりやすい。

これに対処するため、pre-no-tmp.sh という PreToolUse hook を導入した。Write ツールが発火する直前に実行され、パイプライン成果物にあたるファイルが /tmp 以下に書かれようとしたときブロックする。

# pre-no-tmp.sh より抜粋
file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null)
case "$file_path" in /tmp/*) ;; *) exit 0 ;; esac

blocked=""
case "$file_path" in
 */input.json|*/final.json) blocked="pipeline artifact" ;;
 *.mp4) blocked="rendered video" ;;
 *-thumb.png) blocked="thumbnail" ;;
esac
[ -z "$blocked" ] && exit 0

echo "Blocked: $blocked must go under ~/Desktop/montage/{channelId}/{topicId}/" >&2
exit 2

ここで重要なのは「ブロックするだけでなく、正しいパスを stderr で示す」設計だ。AI がブロックされたとき、次の手が分かる状態にしないと、エージェントが詰まって処理が止まる。エラーメッセージに「置き先はどこか」を明示することで、リカバリーが自動化される。

運用してわかった効果と落とし穴

効果。 /tmp への成果物書き込みが事実上ゼロになった。「あのファイルはどこ?」の答えが ~/Desktop/montage/{channelId}/{topicId}/ に一本化され、掃除コマンドが rm -rf ~/Desktop/montage/_* 一発で済む。MONTAGE_ROOT 環境変数でルートを差し替えられるため、CI でも MONTAGE_ROOT=/tmp/montage-test と向けるだけで本番ディレクトリを汚染しない。

落とし穴。 hook のブロック条件が広すぎると、テスト用フィクスチャまで止まる false positive が出た。*.json を全部ブロックすればクリーンになるが実用にならない。条件は「止めたい操作を十分に特定できる最小パターン」に絞るのが鉄則だ。保護と開発速度のトレードオフ、および誤検知のリスクは、条件を慎重に設定することで初めて両立する。

もう一点。paths.ts を SSOT にしたつもりでも、Python・Bash スクリプトが別のパス定義を持つケースが出た。言語ごとに paths.ts / paths.py / paths.sh を用意し、いずれも MONTAGE_ROOT 環境変数を読む設計が必要だった。

# paths.sh の例
MONTAGE_ROOT="${MONTAGE_ROOT:-$HOME/Desktop/montage}"
TOPIC_DIR() { echo "$MONTAGE_ROOT/$1/$2"; }
TEMP_DIR() { echo "$MONTAGE_ROOT/_temp/${1:-}"; }
CACHE_DIR() { echo "$MONTAGE_ROOT/_cache/${1:-}"; }

「単一言語のプロジェクトなら SSOT は簡単」という思い込みが落とし穴だった。言語をまたいだ SSOT は単一言語のプロジェクトより一段難しく、各言語での実装コスト・メンテナンス負荷・同期の手間が増える。

まとめ

AI が成果物を散らかさないための設計は、「指示する」より「散らかせない構造を作る」ほうが実効性が高い。

paths SSOT で「どこに書くか」を一元化し、_ プレフィックス規則で「安全に消せるものを明示」し、hook で「違反を事前ブロック」する 3 層の設計が、八雲での運用をシンプルに保てている理由だ。

AI エージェントによる自動実行が増えるほど、成果物の所在地不明は開発体験を損なう。スケール前のいま、パスの SSOT 化を習慣づけておくことをお勧めする。

ShareShare on X