專案開發時往往是會多個 AI Agent 一起混用,不過每家的 Cli 除 skills 是固定會存取外,其餘都有格式上的差異尚未一統。這邊筆記下如何用一份 .agents 下定義的 roles 資料夾,給 codex 和 claude 同時使用。
內容
以下操作都是在「專案根目錄」下進行。
核心目的:新增編輯 .agents/roles/*.md 下的角色 md 檔,這是源頭。
Claude CLI
Claude Code 使用 .claude/agents/*.md 定義 sub agent。因此直接用 symlink 綁定
Codex CLI
.codex/agents/*.toml 只保留一層薄 adapter,要求 agent 執行前讀取對應的 .agents/roles/*.md。
Sync-agents.sh
新增 .sync-agents.sh ,內容如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
#!/usr/bin/env bash set -euo pipefail repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" roles_dir="${repo_root}/.agents/roles" claude_dir="${repo_root}/.claude/agents" codex_dir="${repo_root}/.codex/agents" check_only=false if [[ "${1:-}" == "--check" ]]; then check_only=true elif [[ -n "${1:-}" ]]; then echo "Usage: $0 [--check]" >&2 exit 2 fi if [[ ! -d "${roles_dir}" ]]; then echo "Error: missing ${roles_dir}" >&2 exit 1 fi mkdir -p "${claude_dir}" "${codex_dir}" extract_frontmatter_value() { local file="$1" local key="$2" awk -v key="${key}" ' BEGIN { in_frontmatter = 0 } NR == 1 && $0 == "---" { in_frontmatter = 1 next } in_frontmatter && $0 == "---" { exit } in_frontmatter && $0 ~ ("^" key ":[[:space:]]*") { sub("^" key ":[[:space:]]*", "", $0) print exit } ' "${file}" } toml_escape() { local value="$1" value="${value//\\/\\\\}" value="${value//\"/\\\"}" printf '%s' "${value}" } render_codex_adapter() { local filename="$1" local name="$2" local description="$3" local escaped_name local escaped_description escaped_name="$(toml_escape "${name}")" escaped_description="$(toml_escape "${description}")" cat <<TOML # Generated by scripts/sync-agents.sh. # Canonical role: .agents/roles/${filename} # Do not edit this file directly. name = "${escaped_name}" description = "${escaped_description}" developer_instructions = """ 開始任何工作前,必須先讀取並完整遵守: - AGENTS.md - .agents/roles/${filename} `.agents/roles/${filename}` 是此 agent 的 canonical role definition。 其中的角色職責、工作流程、限制與輸出要求均適用。 若角色定義與 AGENTS.md 衝突,以 AGENTS.md 為準。 """ TOML } has_error=false for role_file in "${roles_dir}"/*.md; do [[ -e "${role_file}" ]] || continue filename="$(basename "${role_file}")" # README.md documents this directory; it is not a role definition. [[ "${filename}" == "README.md" ]] && continue slug="${filename%.md}" name="$(extract_frontmatter_value "${role_file}" "name")" description="$(extract_frontmatter_value "${role_file}" "description")" if [[ -z "${name}" ]]; then echo "Error: ${role_file} is missing frontmatter 'name'." >&2 has_error=true continue fi if [[ -z "${description}" ]]; then echo "Error: ${role_file} is missing frontmatter 'description'." >&2 has_error=true continue fi if [[ "${name}" != "${slug}" ]]; then echo "Error: role name '${name}' must match filename '${slug}'." >&2 has_error=true continue fi # ---------------------------------------------------------- # Claude symlink # ---------------------------------------------------------- claude_link="${claude_dir}/${filename}" claude_target="../../.agents/roles/${filename}" if [[ "${check_only}" == true ]]; then if [[ ! -L "${claude_link}" ]]; then echo "DRIFT: .claude/agents/${filename} is missing or not a symlink." has_error=true elif [[ "$(readlink "${claude_link}")" != "${claude_target}" ]]; then echo "DRIFT: .claude/agents/${filename} points to the wrong target." has_error=true else echo "OK: .claude/agents/${filename}" fi else if [[ -L "${claude_link}" ]]; then current_target="$(readlink "${claude_link}")" if [[ "${current_target}" == "${claude_target}" ]]; then echo "OK: .claude/agents/${filename}" else rm "${claude_link}" ln -s "${claude_target}" "${claude_link}" echo "Linked: .claude/agents/${filename} -> ${claude_target}" fi elif [[ -e "${claude_link}" ]]; then echo "Skip: .claude/agents/${filename} exists and is not a symlink." else ln -s "${claude_target}" "${claude_link}" echo "Linked: .claude/agents/${filename} -> ${claude_target}" fi fi # ---------------------------------------------------------- # Codex adapter # ---------------------------------------------------------- codex_file="${codex_dir}/${slug}.toml" expected_file="$(mktemp)" trap 'rm -f "${expected_file}"' EXIT render_codex_adapter \ "${filename}" \ "${name}" \ "${description}" > "${expected_file}" if [[ "${check_only}" == true ]]; then if [[ ! -f "${codex_file}" ]]; then echo "DRIFT: .codex/agents/${slug}.toml is missing." has_error=true elif ! cmp -s "${expected_file}" "${codex_file}"; then echo "DRIFT: .codex/agents/${slug}.toml is out of date." has_error=true else echo "OK: .codex/agents/${slug}.toml" fi else mv "${expected_file}" "${codex_file}" echo "Built: .codex/agents/${slug}.toml" fi rm -f "${expected_file}" trap - EXIT done if [[ "${has_error}" == true ]]; then exit 1 fi if [[ "${check_only}" == true ]]; then echo "Agent definitions are in sync." fi |
結論
1. .agents/roles/ → 唯一角色來源
2. .claude/agents/ → symlink
3. .codex/agents/ → generated adapter
參考資料
Subagents Use subagents in ChatGPT and Codex, and configure custom Codex agents
按讚加入粉絲團
![[指南] 一份 AI 角色定義供 Claude 和 Codex 在專案中使用 [指南] 一份 AI 角色定義供 Claude 和 Codex 在專案中使用](https://smlpoints.com/wp-content/uploads/shared-agent-roles-for-codex-and-claude-1.jpeg)