はじめに
AIコーディングエージェントが増えて、同じskillsやrulesを複数の場所に置いていませんか?
更新するたびに全部直す運用では、どこかが古いまま残ります。
5人ほどの開発チームで、この問題に直面しました。
Claude Codeを使うメンバーもいれば、Codexを使うメンバーもいます。
同じファイルをエージェントごとにコピーする運用は、更新箇所を増やすだけでした。
そこで、実体を.agentsへまとめました。
skillsはsymlinkで共有できますが、rulesはCodex側に接続部分が必要です。
この記事では、pathsのないrulesをセッション開始時に読み込み、pathsのあるrulesを対象ファイルに応じて絞り込むところまで扱います。
結論
実体と参照方法を整理すると、次の構成になります。
| 対象 | 実体 | Claude Code | Codex |
|---|---|---|---|
| skills | .agents/skills |
.claude/skillsからsymlink |
直接読む |
| 常時読む指示 | AGENTS.md |
CLAUDE.mdからsymlinkまたはimport |
直接読む |
pathsのないrules |
.agents/rules |
.claude/rulesからsymlink |
SessionStartで読み込む |
pathsのあるrules |
.agents/rules |
.claude/rulesからsymlink |
resolverで対象パスと照合する |
skillsとrulesの実体を.agentsへ集約し、各エージェント固有の方法で参照する構成。画像をクリックすると拡大できます。
共通化の目的は、すべてのエージェントに同じディレクトリ名を読ませることではありません。
編集する実体を1箇所にして、エージェントごとの差を薄い接続部分へ閉じ込めることです。
skillsはsymlinkで共有する
Codexは、リポジトリの.agents/skillsとユーザーの~/.agents/skillsを読みます。
Claude Codeのプロジェクトskillsは.claude/skillsです。
そこで、実体を.agents/skillsに置き、Claude Code側からディレクトリごと参照します。
プロジェクトルート/
├── .agents/
│ └── skills/ # 実体
└── .claude/
└── skills -> ../.agents/skills
新しく作る場合は、プロジェクトルートで次を実行します。
ln -s ../.agents/skills .claude/skills
Claude Codeはskillディレクトリのsymlinkをサポートしています。
Codexは.agents/skillsを直接読むため、以降は.agents/skillsだけを編集すれば両方へ反映されます。
AGENTS.mdとCLAUDE.mdも実体を揃える
プロジェクト全体で常に読ませたい指示は、CodexではAGENTS.md、Claude CodeではCLAUDE.mdに置きます。
ここもコピーは不要です。
プロジェクトルート/
├── AGENTS.md # 実体
└── CLAUDE.md -> AGENTS.md
ln -s AGENTS.md CLAUDE.md
Claude Code専用の追記が必要なら、symlinkではなくCLAUDE.mdからimportします。
@AGENTS.md
# Claude Codeだけの指示
- ...
この併用方法は、Claude Codeの公式ドキュメントでも案内されています。
rulesは接続方法が異なる
Claude Codeは.claude/rules/*.mdを読み、YAML frontmatterのpathsで対象ファイルを絞れます。
pathsのないrulesはセッション開始時に読み込まれます。
rulesディレクトリのsymlinkも公式にサポートされています。
.agents/rules/ # 実体
.claude/rules -> ../.agents/rules
Codexの.codex/rulesへ同じMarkdownを置くことはできません。
.codex/rules/*.rulesは、コマンドをsandbox外で実行してよいか制御するための仕組みです。
Claude Codeの.claude/rules/*.mdとは、役割も形式も異なります。
この差を、Codex Hooksとresolverで吸収します。
pathsのないrulesはSessionStartで読み込む
Codex HooksのSessionStartは、startup、resume、clear、compactで実行できます。
hookが返したadditionalContextは、追加の開発者向けコンテキストとしてCodexへ渡されます。
このタイミングで.agents/rulesを走査し、frontmatterにpathsがないrulesの本文を返します。
SessionStart
└── .agents/rules/*.mdを走査
├── pathsなし:本文をadditionalContextへ追加
└── pathsあり:この時点では追加しない
.codex/hooks.jsonでは、次のようにhookを登録します。
{
"description": "Load shared workspace rules for Codex.",
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "command",
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/session-start-rules.mjs\"",
"statusMessage": "Loading workspace rules"
}
]
}
]
}
}
プロジェクト配下のhookは、信頼済みのリポジトリでのみ動きます。
追加後は、新しいセッションを開始して/hooksから登録内容を確認します。
SessionStart hookの実装
session-start-rules.mjsはresolverを呼び、pathsのないrulesをadditionalContextとして返します。
#!/usr/bin/env node
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { resolveRules } from '../../.agents/scripts/resolve-rules.mjs'
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
let input = ''
for await (const chunk of process.stdin) input += chunk
JSON.parse(input || '{}')
const rules = await resolveRules(repositoryRoot)
const contents = rules
.map(({ name, body }) => `## .agents/rules/${name}\n\n${body}`)
.join('\n\n')
const instruction = [
'以下はpaths指定のない常時適用規則です。作業前にすべて従ってください。',
'出力が上限でファイルへ退避された場合は、そのファイルを全文読んでから作業してください。',
'対象パスが判明したら `node .agents/scripts/resolve-rules.mjs --paths <対象パス...>` を実行し、追加規則も確認してください。',
].join('')
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: `${instruction}\n\n${contents}\n\n${instruction}`,
},
}))
指示を本文の前後へ置いているのは、hook出力が長い場合への対策です。
Codexはモデルから見えるhook出力を約2,500トークンに制限し、超過分を一時ファイルへ保存して先頭と末尾のプレビューを渡します。
そのため、規則が多いリポジトリでは、Codexに保存先の全文を読ませる必要があります。
pathsのあるrulesはresolverで絞り込む
たとえば、次のruleがあるとします。
---
paths:
- 'apps/*/src/**/*.tsx'
---
# UIルール
- UI部品は共通パッケージから利用する
編集対象がapps/client/src/pages/users.tsxなら、このruleが一致します。
resolverは、対象パスを--paths以降で受け取ります。
node .agents/scripts/resolve-rules.mjs \
--paths \
apps/client/src/pages/users.tsx
resolverの責務は次の3つです。
-
.agents/rules/*.mdを読み込む - YAML frontmatterの
pathsを解析する - 対象パスとglobが一致したrulesだけを標準出力する
--pathsを付けない場合は、pathsのないrulesだけを返します。
--pathsを付けた場合は、一致したパス別rulesだけを返します。
開始時に読んだ常時規則を再出力しないため、同じ本文でコンテキストを増やさずに済みます。
YAMLとglobを独自実装する場合は、配列、複数行、**、*.{ts,tsx}など、リポジトリで使う記法をテストしてください。
設定ファイルを正しく解析する必要があるなら、YAMLとglobの既存ライブラリを使うほうが安全です。
PreToolUseだけでは最初の編集に間に合わない
現在のCodex HooksにはPreToolUseがあり、shell、apply_patch、MCPなどの呼び出しを検知できます。
additionalContextを返すことも、ツール呼び出しを拒否することもできます。
ただし、PreToolUseが動く時点では、モデルは最初の編集内容をすでに生成しています。
追加規則を返すだけでは、その編集内容を規則に合わせて作り直せません。
最初の編集より前にパス別rulesを強制したい場合は、未読のruleがある編集をPreToolUseで拒否し、ruleを読んだ後に再実行させる設計が必要です。
公式ドキュメントもtool hookを完全な強制境界ではなく、補助的な防護として扱うよう説明しています。
必ず作業開始前から守らせたい規則は、AGENTS.mdへ置くほうが単純です。
パス別rulesは、対象外の作業へ余計な指示を混ぜないための補助として使います。
resolverをテストする
最低限、次の3ケースを固定します。
-
pathsを付けない実行では、常時規則だけを返す - 対象パスに一致するパス別rulesだけを返す
- 対象パスに一致しないrulesは返さない
Node.jsの組み込みテストを使う場合は、次のコマンドで検証できます。
node --test .agents/scripts/resolve-rules.test.mjs
hook本体は、標準入力を渡してJSONを解析できることまで確認します。
printf '{"source":"startup"}' \
| node .codex/hooks/session-start-rules.mjs \
| jq -e '.hookSpecificOutput.hookEventName == "SessionStart"'
最終的な構成
プロジェクトルート/
├── .agents/
│ ├── skills/ # skillsの実体
│ ├── rules/ # rulesの実体
│ └── scripts/
│ ├── resolve-rules.mjs # Codex用resolver
│ └── resolve-rules.test.mjs
├── .claude/
│ ├── skills -> ../.agents/skills
│ └── rules -> ../.agents/rules
├── .codex/
│ ├── hooks.json
│ └── hooks/
│ └── session-start-rules.mjs
├── AGENTS.md # 常時読む共通指示
└── CLAUDE.md -> AGENTS.md
Windowsでsymlinkを復元するには、開発者モードなどの設定が必要です。
設定を揃えにくいチームでは、CLAUDE.mdの@AGENTS.md importなど、symlinkを使わない接続方法も選べます。
実体と接続部分を分ける
skillsはsymlinkで共有できます。
常時読む指示はAGENTS.mdへまとめられます。
rulesは.agents/rulesを実体とし、Claude Codeはsymlink、Codexはhookとresolverから参照します。
エージェントごとに違うのは、実体へつなぐ接続部分だけです。
規則の本文を直す場所は.agentsの1箇所に絞れます。
