feat(plugin): /claude-to-agents — convert CLAUDE.md to AGENTS.md for codex / gemini users

Cross-tool adoption. Codex, Gemini Code Assist, and any AI tool honouring
the AGENTS.md convention can now read the same instructions as Claude,
without the user maintaining two files by hand.

hooks/claude-to-agents.py (new, standalone, idempotent):
  - argparse: --mode={symlink,copy,inline-chain} (default symlink),
    --source (default CLAUDE.md), --output (default AGENTS.md), --force.
  - Symlink mode: ln -s CLAUDE.md AGENTS.md. Windows falls back to
    --copy with a stderr notice.
  - Copy mode: byte-for-byte snapshot via shutil.copyfile.
  - Inline-chain mode: depth-first walk of @path imports, recursive,
    cycle-safe (each file read at most once). Output flattens every
    chained sub-file under <!-- inlined from <rel> --> markers and
    strips two flavours of Claude-only scaffolding:
      • The @path import lines themselves (other tools don't resolve
        them).
      • Backlink quote-blocks ("> Parent context: ..." /
        "> Chained import: `@../CLAUDE.md`") that we emit on every
        sub-CLAUDE.md.
  - Safety: existing AGENTS.md (file or symlink) is renamed to
    AGENTS.md.backup.<UTC-ts> before overwrite. --force skips the
    backup. Microsecond timestamp precision so back-to-back writes
    don't collide.
  - Exit codes: 0 success, 1 user error (missing source / unknown
    mode), 2 filesystem error (symlink failure).

command/claude-to-agents.md (new slash command):
  - allowed-tools: Read, Write, Glob, Bash(python3:*), Bash(ls:*),
    Bash(test:*), Bash(readlink:*).
  - disallowedTools: WebFetch, WebSearch (no exfiltration vector).
  - argument-hint and when_to_use surface the three modes.
  - Body specifies a heuristic: default to --symlink for single-file
    projects, recommend --inline-chain when find . -name CLAUDE.md
    returns more than one. Documents per-mode verification commands.

.claude-plugin/plugin.json:
  Registers ./command/claude-to-agents.md alongside the existing two.

install.sh + install.ps1:
  Banner and uninstall sections list the new command. The command
  install loop already iterates command/*.md so the file itself is
  copied automatically.

README.md:
  /claude-to-agents added to "What's Included" under Slash commands
  with a one-paragraph description covering all three modes.
  Quick Stats: "2 slash commands" -> "3 slash commands".

CHANGELOG.md:
  New "wave 5" entry under [Unreleased].

Verified (7/7 integration checks):
  - Plugin manifest registers the command; all 9 referenced paths
    resolve on disk.
  - Slash command frontmatter has all required fields including
    WebFetch in disallowedTools.
  - Script has executable bit set.
  - Both install scripts list claude-to-agents.md.
  - install.sh passes bash -n.
  - End-to-end against a synthetic chained CLAUDE.md tree with a
    deliberate back-import cycle: symlink mode creates valid symlink;
    copy mode produces bytewise-identical AGENTS.md and backs up the
    prior symlink (1 backup); inline-chain mode inlines both
    sub-files, strips backlinks AND chain-import lines, handles the
    cycle (each file read once), backs up the prior file (2 backups
    total).
  - Missing source returns rc=1 with stderr message.
This commit is contained in:
Claude
2026-05-19 01:50:18 +00:00
parent e45065e8ba
commit 7d22a50c37
7 changed files with 251 additions and 2 deletions
+2 -1
View File
@@ -27,7 +27,8 @@
],
"commands": [
"./command/enhance-claude-md.md",
"./command/sync-claude-md.md"
"./command/sync-claude-md.md",
"./command/claude-to-agents.md"
],
"agents": [
"./agent/claude-md-guardian.md"
+11
View File
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added (wave 5 — CLAUDE.md → AGENTS.md conversion for Codex / Gemini)
Cross-tool adoption: every project using ClaudeForge can now share its instructions with non-Claude tools (OpenAI Codex, Gemini Code Assist, anything honouring the AGENTS.md convention) without maintaining two files.
- **`/claude-to-agents`** slash command (`command/claude-to-agents.md`) with three modes:
- `--symlink` (default on macOS / Linux) — `AGENTS.md` becomes a symlink to `CLAUDE.md`. One source of truth; edits propagate instantly. Windows falls back to `--copy` automatically with a stderr notice.
- `--copy` — byte-for-byte snapshot. Use when the user wants to fork the instructions or when their VCS/build pipeline doesn't follow symlinks.
- `--inline-chain` — recursively walks every `@path/.../CLAUDE.md` import and writes a single flat AGENTS.md with all sub-file content inlined. **Recommended for Codex / Gemini in modular projects** because those tools don't auto-resolve `@`-imports.
- **`hooks/claude-to-agents.py`** — standalone, idempotent script. Backs up an existing `AGENTS.md` to `AGENTS.md.backup.<UTC-timestamp>` before overwrite (unless `--force`). Strips Claude-only scaffolding (backlink lines, `@`-import lines) from `--inline-chain` output. Cycle-safe (each file read at most once).
- Plugin manifest registers the new command. Both installers list it in their banner and uninstall instructions.
### Added (wave 4 — forked task-style audit skills)
Three new task-style skills under `skill/`, each using Anthropic's `context: fork` + `agent: Explore` so they run in an isolated subagent context (no caller chat history, ≤500-token summary back to the main session):
+2 -1
View File
@@ -56,6 +56,7 @@ ClaudeForge is a comprehensive toolkit that eliminates the tedious process of ma
- **`/enhance-claude-md`** (`command/enhance-claude-md.md`) — multi-phase init/enhance workflow with `argument-hint`, `when_to_use`, `allowed-tools`, and `disallowedTools` (blocks `WebFetch` / `WebSearch`). Delegates deep codebase scans to the Explore subagent.
- **`/sync-claude-md`** (`command/sync-claude-md.md`) — inventory → prune stale refs → enforce the 150-line cap → repair root ↔ sub chain. **New `--weekly` flag** orchestrates the three audit skills in parallel before doing sync work.
- **`/claude-to-agents`** (`command/claude-to-agents.md`) — convert the project's CLAUDE.md tree into an `AGENTS.md` for Codex / Gemini Code Assist / any tool honouring the AGENTS.md convention. Three modes: `--symlink` (one source of truth, default on macOS/Linux), `--copy` (snapshot), `--inline-chain` (flattens the `@path` chain into one self-contained file — recommended for modular projects since Codex/Gemini don't auto-resolve `@` imports). Backs up an existing AGENTS.md before overwrite.
### Agent
@@ -299,7 +300,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## 📊 Quick Stats
- **5** skills (`claudeforge-skill`, `karpathy-guidelines`, plus three forked audit skills)
- **2** slash commands (`/enhance-claude-md`, `/sync-claude-md` with `--weekly`)
- **3** slash commands (`/enhance-claude-md`, `/sync-claude-md` with `--weekly`, `/claude-to-agents`)
- **1** agent (`claude-md-guardian`, fail-closed contract)
- **3** hook scripts wired across `PostToolUse`, `InstructionsLoaded`, `Stop`
- **5** Python modules under `skill/` (analyzer, validator, generator, template_selector, workflow)
+61
View File
@@ -0,0 +1,61 @@
---
description: Convert the project's CLAUDE.md (and chained sub-files) into an AGENTS.md for Codex / Gemini Code Assist / other tools that follow the AGENTS.md convention. Three modes — symlink for one source of truth, copy for a snapshot, inline-chain for a self-contained flat file that doesn't depend on @-import resolution.
argument-hint: "[--symlink | --copy | --inline-chain] [--force]"
when_to_use: |
Use when the user asks "make an AGENTS.md", "support codex", "support gemini",
"convert CLAUDE.md", "share my instructions with non-Claude tools", or when
adopting ClaudeForge in a repo that already has cross-tool contributors.
allowed-tools:
- Read
- Write
- Glob
- "Bash(python3:*)"
- "Bash(ls:*)"
- "Bash(test:*)"
- "Bash(readlink:*)"
disallowedTools:
- WebFetch
- WebSearch
permissions:
allow:
- Read
- Write
- Glob
- "Bash(python3:*)"
- "Bash(ls:*)"
- "Bash(test:*)"
- "Bash(readlink:*)"
---
# /claude-to-agents — convert CLAUDE.md → AGENTS.md
Wraps `hooks/claude-to-agents.py` so non-Claude tools (OpenAI Codex, Gemini Code Assist, anything else honouring the `AGENTS.md` convention) can read the same instructions as Claude.
## Mode selection
Decide first by asking which guarantee the user wants:
- **`--symlink`** (default on macOS / Linux): `AGENTS.md` becomes a symlink to `CLAUDE.md`. One source of truth — edits to CLAUDE.md show up in AGENTS.md instantly. Codex/Gemini read it transparently. On Windows the script falls back to `--copy` and prints a notice.
- **`--copy`**: byte-for-byte snapshot. Use when the user wants to fork the instructions for non-Claude tooling (Codex/Gemini reading a different policy) or when their VCS / build pipeline doesn't follow symlinks.
- **`--inline-chain`**: walk every `@path/.../CLAUDE.md` chain import recursively and write a single flat AGENTS.md with all sub-file content inlined. **Recommended for Codex/Gemini in modular projects** — those tools don't resolve `@`-imports, so without inlining they'd only see the root file.
If the user is silent on mode, default to `--symlink` for simple projects and recommend `--inline-chain` for projects with > 1 CLAUDE.md (run `find . -name CLAUDE.md -type f -not -path '*/.git/*' -not -path '*/node_modules/*' | wc -l` first to decide).
## Execution
1. **Pre-flight.** `test -f CLAUDE.md` — if missing, tell the user `/enhance-claude-md` is the right command first.
2. **Run the script** with the chosen flags from the repo root:
```bash
python3 "${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/hooks/claude-to-agents.py" --mode <mode>
```
3. **Report.** Echo whether AGENTS.md was created or backed up, its size, and which mode produced it.
4. **Verify** the result:
- For `--symlink`: `readlink AGENTS.md` should print `CLAUDE.md`.
- For `--copy`: `diff -q CLAUDE.md AGENTS.md` should return clean.
- For `--inline-chain`: AGENTS.md must contain content from every chained sub-file; the script strips backlinks and `@`-import lines automatically.
## Safety
- An existing `AGENTS.md` is renamed to `AGENTS.md.backup.<UTC-timestamp>` before overwrite. Pass `--force` to skip the backup (destructive).
- The script never writes outside the current directory tree.
- Read-only modes (`--symlink`) leave CLAUDE.md untouched.
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""ClaudeForge: convert a CLAUDE.md tree into an AGENTS.md for Codex / Gemini users.
Three modes:
* ``--symlink`` (default on macOS/Linux): create ``AGENTS.md`` as a symlink to
``CLAUDE.md``. Single source of truth forever. Windows falls back to
``--copy`` with a warning printed to stderr.
* ``--copy``: byte-copy ``CLAUDE.md`` → ``AGENTS.md``. Snapshot, not live-linked.
* ``--inline-chain``: walk every ``@path/.../CLAUDE.md`` import recursively and
produce a single flat ``AGENTS.md`` with every chained file inlined under a
header that names its relative path. The right choice for Codex / Gemini
because those tools don't auto-resolve Claude's ``@`` imports.
An existing ``AGENTS.md`` is backed up to ``AGENTS.md.backup.<UTC-timestamp>``
before overwrite, unless ``--force`` is passed.
Exit codes: ``0`` success, ``1`` user error (missing source, unsafe overwrite,
unknown mode), ``2`` filesystem error (permission denied, symlink unsupported).
"""
from __future__ import annotations
import argparse
import datetime as _dt
import os
import re
import shutil
import sys
from pathlib import Path
CHAIN_IMPORT = re.compile(r"^\s*@([^\s]+)\s*$")
BACKLINK_LINE = re.compile(
r"^\s*>\s*(Parent context:|Chained import:).*$", re.IGNORECASE
)
def _ts() -> str:
# Microsecond precision so two backups in the same second don't collide
# (the smoke test for the inline-chain workflow re-backs-up within ms).
return _dt.datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f")
def _backup(dest: Path) -> Path | None:
if not dest.exists() and not dest.is_symlink():
return None
backup = dest.with_name(f"{dest.name}.backup.{_ts()}")
# ``shutil.move`` preserves symlinks-as-symlinks across same-filesystem.
shutil.move(str(dest), str(backup))
return backup
def _resolve_imports(root: Path, seen: set[Path]) -> list[tuple[Path, str]]:
"""Depth-first walk of ``@path`` chain imports rooted at ``root``.
Returns a list of ``(relative_path, content)`` tuples in encounter order.
Cycles are short-circuited (a file is read at most once).
"""
root = root.resolve()
if root in seen:
return []
if not root.exists():
return []
seen.add(root)
lines = root.read_text(encoding="utf-8").splitlines()
cleaned: list[str] = []
children: list[tuple[Path, str]] = []
parent_dir = root.parent
for line in lines:
m = CHAIN_IMPORT.match(line)
if m:
target = (parent_dir / m.group(1)).resolve()
if target.suffix == "" and target.is_dir():
target = target / "CLAUDE.md"
children.extend(_resolve_imports(target, seen))
# Drop the @-import line from the inlined parent body.
continue
if BACKLINK_LINE.match(line):
# Backlinks like "> Parent context: see the root [CLAUDE.md]..."
# are Claude-specific scaffolding. Drop in inline-chain output.
continue
cleaned.append(line)
rel = str(root.relative_to(Path.cwd().resolve())) if root.is_relative_to(
Path.cwd().resolve()
) else root.name
return [(rel, "\n".join(cleaned).strip() + "\n")] + children
def _do_symlink(src: Path, dest: Path) -> int:
if os.name == "nt":
print(
"claude-to-agents: symlinks on Windows require admin / dev mode; "
"falling back to --copy",
file=sys.stderr,
)
return _do_copy(src, dest)
try:
dest.symlink_to(src.name)
except OSError as exc:
print(f"claude-to-agents: symlink failed ({exc}); use --copy or --inline-chain", file=sys.stderr)
return 2
return 0
def _do_copy(src: Path, dest: Path) -> int:
shutil.copyfile(src, dest)
return 0
def _do_inline_chain(src: Path, dest: Path) -> int:
chain = _resolve_imports(src, set())
if not chain:
print(f"claude-to-agents: source {src} not found", file=sys.stderr)
return 1
out: list[str] = [
"<!-- Generated by ClaudeForge /claude-to-agents --inline-chain -->",
"<!-- Source: CLAUDE.md (this file flattens the @path chain for tools that don't auto-import). -->",
"",
]
for rel, body in chain:
out.append(f"<!-- inlined from {rel} -->")
out.append("")
out.append(body.strip())
out.append("")
dest.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="claude-to-agents", description=__doc__.split("\n\n")[0])
parser.add_argument("--source", default="CLAUDE.md")
parser.add_argument("--output", default="AGENTS.md")
parser.add_argument(
"--mode",
choices=("symlink", "copy", "inline-chain"),
default="symlink",
)
parser.add_argument("--force", action="store_true", help="overwrite without backup")
args = parser.parse_args(argv)
src = Path(args.source)
dest = Path(args.output)
if not src.exists():
print(f"claude-to-agents: source {src} not found", file=sys.stderr)
return 1
if (dest.exists() or dest.is_symlink()) and not args.force:
backup = _backup(dest)
if backup is not None:
print(f"claude-to-agents: backed up existing {dest} -> {backup}", file=sys.stderr)
elif args.force and (dest.exists() or dest.is_symlink()):
dest.unlink()
handlers = {
"symlink": _do_symlink,
"copy": _do_copy,
"inline-chain": _do_inline_chain,
}
rc = handlers[args.mode](src, dest)
if rc == 0:
kind = "symlink" if args.mode == "symlink" and dest.is_symlink() else "file"
print(f"claude-to-agents: wrote {dest} ({args.mode}, {kind})", file=sys.stderr)
return rc
if __name__ == "__main__":
sys.exit(main())
+3
View File
@@ -183,6 +183,7 @@ Write-Host " • Skill: $skillsDir\claude-md-link-check\"
Write-Host " • Skill: $skillsDir\claude-md-dependency-rescan\"
Write-Host " • Command: $commandsDir\enhance-claude-md.md"
Write-Host " • Command: $commandsDir\sync-claude-md.md"
Write-Host " • Command: $commandsDir\claude-to-agents.md"
Write-Host " • Agent: $agentsDir\claude-md-guardian.md"
Write-Host ""
@@ -403,6 +404,7 @@ if ($scope -eq "user-level") {
Write-Host " Remove-Item -Recurse -Force ~\.claude\skills\claude-md-dependency-rescan"
Write-Host " Remove-Item -Force ~\.claude\commands\enhance-claude-md.md"
Write-Host " Remove-Item -Force ~\.claude\commands\sync-claude-md.md"
Write-Host " Remove-Item -Force ~\.claude\commands\claude-to-agents.md"
Write-Host " Remove-Item -Force ~\.claude\agents\claude-md-guardian.md"
} else {
Write-Host " Remove-Item -Recurse -Force .\.claude\skills\claudeforge-skill"
@@ -412,6 +414,7 @@ if ($scope -eq "user-level") {
Write-Host " Remove-Item -Recurse -Force .\.claude\skills\claude-md-dependency-rescan"
Write-Host " Remove-Item -Force .\.claude\commands\enhance-claude-md.md"
Write-Host " Remove-Item -Force .\.claude\commands\sync-claude-md.md"
Write-Host " Remove-Item -Force .\.claude\commands\claude-to-agents.md"
Write-Host " Remove-Item -Force .\.claude\agents\claude-md-guardian.md"
}
Write-Host ""
+3
View File
@@ -164,6 +164,7 @@ echo " • Skill: $SKILLS_DIR/claude-md-link-check/"
echo " • Skill: $SKILLS_DIR/claude-md-dependency-rescan/"
echo " • Command: $COMMANDS_DIR/enhance-claude-md.md"
echo " • Command: $COMMANDS_DIR/sync-claude-md.md"
echo " • Command: $COMMANDS_DIR/claude-to-agents.md"
echo " • Agent: $AGENTS_DIR/claude-md-guardian.md"
echo ""
@@ -360,6 +361,7 @@ if [ "$SCOPE" == "user-level" ]; then
echo " rm -rf ~/.claude/skills/claude-md-dependency-rescan"
echo " rm -f ~/.claude/commands/enhance-claude-md.md"
echo " rm -f ~/.claude/commands/sync-claude-md.md"
echo " rm -f ~/.claude/commands/claude-to-agents.md"
echo " rm -f ~/.claude/agents/claude-md-guardian.md"
else
echo " rm -rf ./.claude/skills/claudeforge-skill"
@@ -369,6 +371,7 @@ else
echo " rm -rf ./.claude/skills/claude-md-dependency-rescan"
echo " rm -f ./.claude/commands/enhance-claude-md.md"
echo " rm -f ./.claude/commands/sync-claude-md.md"
echo " rm -f ./.claude/commands/claude-to-agents.md"
echo " rm -f ./.claude/agents/claude-md-guardian.md"
fi
echo ""