feat: add Codex CLI compatibility layer

- flake.nix: devShell with yq-go for config generation
- generate-codex.sh: generates Codex agent TOML, AGENTS.md, and
  config.toml from Claude source files (idempotent, yq-powered)
- install.sh: optional Codex symlinks when ~/.codex exists (skills
  shared via ~/.agents/skills/, agents/config/AGENTS.md to ~/.codex/)
- .gitignore: exclude generated codex/ directory
- README.md: document Codex compatibility setup and model mapping
This commit is contained in:
Bryan Ramos 2026-04-02 08:28:29 -04:00
parent 8d08f9650c
commit d812c7f49a
6 changed files with 287 additions and 0 deletions

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ settings.local.json
# OS noise
.DS_Store
Thumbs.db
# Generated Codex CLI config (derived from Claude source files via generate-codex.sh)
codex/

View file

@ -59,6 +59,35 @@ For simple tasks, agents can be invoked directly:
/agent worker Fix the broken pagination in the user list endpoint
```
## Codex CLI compatibility
This project also generates configuration for [OpenAI Codex CLI](https://github.com/openai/codex). Claude Code config is the source of truth; Codex config is derived from it.
### Setup
```bash
nix develop # enter devShell with yq
./generate-codex.sh # generate Codex config from Claude source files
./install.sh # installs both Claude and Codex (if ~/.codex exists)
```
### What gets generated
| Source | Generated | Codex location |
|---|---|---|
| `agents/*.md` | `codex/agents/*.toml` | `~/.codex/agents/` |
| `CLAUDE.md` + `rules/*.md` | `codex/AGENTS.md` | `~/.codex/AGENTS.md` |
| `settings.json` | `codex/config.toml` | `~/.codex/config.toml` |
| `skills/` | (shared as-is) | `~/.agents/skills/` |
### Model mapping
| Claude Code | Codex CLI |
|---|---|
| `opus` | `o3` |
| `sonnet` | `o4-mini` |
| `haiku` | `o4-mini` |
## Project-specific config
Each project repo can extend the team with local config in `.claude/`:

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1775095191,
"narHash": "sha256-CsqRiYbgQyv01LS0NlC7shwzhDhjNDQSrhBX8VuD3nM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "106eb93cbb9d4e4726bf6bc367a3114f7ed6b32f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

14
flake.nix Normal file
View file

@ -0,0 +1,14 @@
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in {
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
packages = [ pkgs.yq-go ];
};
});
};
}

184
generate-codex.sh Executable file
View file

@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
# generate-codex.sh — generates Codex CLI config from Claude source files.
# Claude source files are the source of truth; this script derives Codex equivalents.
# Idempotent: safe to run multiple times.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CODEX_DIR="$SCRIPT_DIR/codex"
CODEX_AGENTS_DIR="$CODEX_DIR/agents"
AGENTS_SRC="$SCRIPT_DIR/agents"
RULES_DIR="$SCRIPT_DIR/rules"
CLAUDE_MD="$SCRIPT_DIR/CLAUDE.md"
SETTINGS_JSON="$SCRIPT_DIR/settings.json"
# Create output directories
mkdir -p "$CODEX_DIR"
mkdir -p "$CODEX_AGENTS_DIR"
# Clean existing generated agent TOMLs
rm -f "$CODEX_AGENTS_DIR"/*.toml
# ---------------------------------------------------------------------------
# map_model — maps Claude model name to Codex model name
# ---------------------------------------------------------------------------
map_model() {
local model="$1"
case "$model" in
opus) echo "o3" ;;
sonnet) echo "o4-mini" ;;
haiku) echo "o4-mini" ;;
*) echo "o4-mini" ;;
esac
}
# ---------------------------------------------------------------------------
# map_effort — maps Claude effort level to Codex model_reasoning_effort
# ---------------------------------------------------------------------------
map_effort() {
local effort="$1"
case "$effort" in
low) echo "low" ;;
medium) echo "medium" ;;
high) echo "high" ;;
max) echo "xhigh" ;;
*) echo "medium" ;;
esac
}
# ---------------------------------------------------------------------------
# map_sandbox_mode — determines Codex sandbox_mode from agent frontmatter
# $1 = permissionMode value (plan / acceptEdits / "")
# $2 = tools list (comma-separated)
# ---------------------------------------------------------------------------
map_sandbox_mode() {
local permission_mode="$1"
local tools="$2"
# plan mode is read-only
if [ "$permission_mode" = "plan" ]; then
echo "read-only"
return
fi
# acceptEdits with Write or Edit tool → workspace-write
if [ "$permission_mode" = "acceptEdits" ]; then
if echo "$tools" | grep -qE '\b(Write|Edit)\b'; then
echo "workspace-write"
return
fi
fi
# Default: read-only
echo "read-only"
}
# ---------------------------------------------------------------------------
# generate_agent_toml — converts a single agent .md file to Codex .toml
# ---------------------------------------------------------------------------
generate_agent_toml() {
local src_file="$1"
local agent_basename
agent_basename="$(basename "$src_file" .md)"
local dst_file="$CODEX_AGENTS_DIR/${agent_basename}.toml"
# Extract YAML frontmatter using yq
local frontmatter
frontmatter="$(yq --front-matter=extract '.' "$src_file")"
# Extract individual fields from frontmatter
local name description model effort permission_mode tools disallowed_tools
name="$(echo "$frontmatter" | yq '.name // ""')"
description="$(echo "$frontmatter" | yq '.description // ""')"
model="$(echo "$frontmatter" | yq '.model // ""')"
effort="$(echo "$frontmatter" | yq '.effort // ""')"
permission_mode="$(echo "$frontmatter" | yq '.permissionMode // ""')"
tools="$(echo "$frontmatter" | yq '.tools // ""')"
disallowed_tools="$(echo "$frontmatter" | yq '.disallowedTools // ""')"
# Map to Codex equivalents
local codex_model codex_effort codex_sandbox
codex_model="$(map_model "$model")"
codex_effort="$(map_effort "${effort:-medium}")"
codex_sandbox="$(map_sandbox_mode "$permission_mode" "$tools")"
# Extract markdown body (everything after the closing frontmatter ---)
# The frontmatter block starts at line 1 with --- and ends at the second ---
local body
body="$(awk 'BEGIN{fm=0} /^---$/{if(fm==0){fm=1;next} if(fm==1){fm=2;next}} fm==2{print}' "$src_file")"
# Build developer_instructions: append disallowedTools note if present
local developer_instructions
developer_instructions="$body"
if [ -n "$disallowed_tools" ] && [ "$disallowed_tools" != "null" ]; then
developer_instructions="${developer_instructions}
You do NOT have access to these tools: ${disallowed_tools}"
fi
# Write TOML output
cat > "$dst_file" <<TOML
name = "${name}"
description = "${description}"
model = "${codex_model}"
model_reasoning_effort = "${codex_effort}"
sandbox_mode = "${codex_sandbox}"
developer_instructions = """
${developer_instructions}
"""
TOML
echo "Generated: $dst_file"
}
# ---------------------------------------------------------------------------
# Generate agents
# ---------------------------------------------------------------------------
echo "Generating Codex agent definitions..."
for agent_file in "$AGENTS_SRC"/*.md; do
[ -f "$agent_file" ] || continue
generate_agent_toml "$agent_file"
done
# ---------------------------------------------------------------------------
# Generate AGENTS.md — concatenate CLAUDE.md and rules/*.md (sorted)
# ---------------------------------------------------------------------------
echo ""
echo "Generating codex/AGENTS.md..."
{
echo "# Agent Team Instructions"
echo ""
echo "Agent-team specific protocols live in skills (orchestrate, conventions, worker-protocol, qa-checklist, message-schema, project)."
for rules_file in $(ls "$RULES_DIR"/*.md | sort); do
echo ""
cat "$rules_file"
done
} > "$CODEX_DIR/AGENTS.md"
echo "Generated: $CODEX_DIR/AGENTS.md"
# ---------------------------------------------------------------------------
# Generate config.toml — derive sandbox_mode from settings.json defaultMode
# ---------------------------------------------------------------------------
echo ""
echo "Generating codex/config.toml..."
default_mode="$(yq -r '.permissions.defaultMode // "acceptEdits"' "$SETTINGS_JSON")"
# Map Claude defaultMode to Codex sandbox_mode
case "$default_mode" in
plan) config_sandbox="read-only" ;;
acceptEdits) config_sandbox="workspace-write" ;;
*) config_sandbox="workspace-write" ;;
esac
cat > "$CODEX_DIR/config.toml" <<TOML
model = "o4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "${config_sandbox}"
approval_policy = "on-request"
TOML
echo "Generated: $CODEX_DIR/config.toml"
echo ""
echo "Done. Run ./install.sh to link generated files into ~/.codex/"

View file

@ -122,5 +122,35 @@ create_symlink "$RULES_SRC" "$RULES_DST" "rules"
create_file_symlink "$CLAUDE_MD_SRC" "$CLAUDE_MD_DST" "CLAUDE.md"
create_file_symlink "$SETTINGS_SRC" "$SETTINGS_DST" "settings.json"
# Codex CLI integration (optional — only if ~/.codex exists)
CODEX_DIR="$HOME/.codex"
CODEX_AGENTS_DIR="$HOME/.agents"
if [ -d "$CODEX_DIR" ]; then
echo ""
echo "Codex CLI detected at $CODEX_DIR"
# Skills shared via ~/.agents/skills/ (Codex discovery path)
mkdir -p "$CODEX_AGENTS_DIR"
create_symlink "$SKILLS_SRC" "$CODEX_AGENTS_DIR/skills" "codex skills"
# Generated agents
if [ -d "$SCRIPT_DIR/codex/agents" ]; then
create_symlink "$SCRIPT_DIR/codex/agents" "$CODEX_DIR/agents" "codex agents"
else
echo "Run ./generate-codex.sh first to generate Codex agent definitions"
fi
# Generated AGENTS.md (symlink to project root for Codex discovery)
if [ -f "$SCRIPT_DIR/codex/AGENTS.md" ]; then
create_file_symlink "$SCRIPT_DIR/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md" "codex AGENTS.md"
fi
# Generated config.toml
if [ -f "$SCRIPT_DIR/codex/config.toml" ]; then
create_file_symlink "$SCRIPT_DIR/codex/config.toml" "$CODEX_DIR/config.toml" "codex config.toml"
fi
fi
echo ""
echo "Done. Open Claude Code and load the orchestrate skill to begin."