Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@
"name": "clodex",
"version": "0.1.1",
"description": "Claude Code and Codex CLI dual-agent workflow orchestration.",
"skills": {
"clodex-workflow": {
"path": "skills/clodex-workflow/SKILL.md"
}
"author": {
"name": "9thLevelSoftware",
"url": "https://github.com/9thLevelSoftware"
},
"commands": {
"clodex-plan": "dist/.claude/commands/clodex/clodex-plan.md",
"clodex-build": "dist/.claude/commands/clodex/clodex-build.md",
"clodex-audit": "dist/.claude/commands/clodex/clodex-audit.md"
"homepage": "https://github.com/9thLevelSoftware/Clodex#readme",
"repository": "https://github.com/9thLevelSoftware/Clodex",
"keywords": [
"codex",
"mcp",
"agent-harness",
"workflow"
],
"commands": [
"./dist/.claude/commands/clodex/"
],
"mcpServers": {
"clodex": {
"command": "node",
"args": [
"${CLAUDE_PLUGIN_ROOT}/npm/clodex-mcp-server.js"
]
}
}
}
21 changes: 11 additions & 10 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
"name": "clodex",
"version": "0.1.1",
"description": "Claude Code and Codex CLI dual-agent workflow orchestration.",
"skills": {
"clodex-workflow": {
"path": "skills/clodex-workflow/SKILL.md"
}
"author": {
"name": "9thLevelSoftware",
"url": "https://github.com/9thLevelSoftware"
},
"mcp_servers": {
"clodex": {
"command": "python",
"args": ["-m", "clodex", "mcp-server"]
}
}
"homepage": "https://github.com/9thLevelSoftware/Clodex#readme",
"repository": "https://github.com/9thLevelSoftware/Clodex",
"keywords": [
"codex",
"mcp",
"agent-harness",
"workflow"
]
}
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,52 @@ agreement.
`clodex init` writes managed Clodex blocks to `CLAUDE.md`, `AGENTS.md`, and
`CLODEX.md`. By default it also configures the repo MCP server through
`.mcp.json` and `.codex/config.toml`; use `--no-mcp-config` when you want
instructions only.
instructions only. From a subdirectory, `clodex init` still works on the repository root.

Those two files are meant to be committed, so they use the portable `clodex mcp-server`
command (wrapped as `cmd /c clodex mcp-server` when `init` runs on Windows and only a `.cmd`
shim exists, because Windows MCP clients cannot start those directly). Re-run `clodex init`
if a teammate on another platform needs the other form.

### User-level install (`clodex init --global`)

Writes where each agent actually reads user-level files, not your home directory root:

| What | Where |
| --- | --- |
| Claude instructions | `~/.claude/CLAUDE.md` (or `$CLAUDE_CONFIG_DIR/CLAUDE.md`) |
| Codex instructions | `~/.codex/AGENTS.md` (or `$CODEX_HOME/AGENTS.md`) |
| Codex MCP server | `~/.codex/config.toml` |
| Claude MCP server | registered with `claude mcp add-json clodex ... --scope user` (Claude Code owns `~/.claude.json`) |

These use an absolute, environment-independent command (the Node launcher for npm installs,
otherwise your Python), so they work regardless of PATH or working directory. Claude Code starts
user-scope servers with `~/.claude` as the working directory, so Clodex finds your project from
`$CLAUDE_PROJECT_DIR`, which Claude Code sets. If the `claude` CLI is not on PATH, `init --global`
prints the command to run yourself.

### Claude Code hooks

```bash
clodex hooks install # .claude/settings.local.json (default, not committed)
clodex hooks install --scope user
clodex hooks uninstall
```

Records `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `SubagentStart/Stop`, `TaskCreated`,
`TaskCompleted`, `WorktreeCreate/Remove` and `Stop` events to
`.clodex/runs/<session-id>/events/claude-hooks.jsonl` and the trace. Your other hooks and settings
are left alone and re-running is safe. The hook command is silent and never exits with code 2
(Claude Code treats that as "block"). Tool events are deliberately not hooked: each hook is a
process, which would slow every tool call.

### Plugin

The repository is also a Claude Code plugin (`claude --plugin-dir <this repo>`): slash commands
`/clodex:clodex-plan`, `/clodex:clodex-build` and `/clodex:clodex-audit`, the `clodex-workflow`
skill, and the MCP server (needs `node`; the launcher finds Python). `claude plugin validate .
--strict` passes. `install.sh` installs a self-contained copy under `~/.clodex/plugin` and a
launcher in `~/.local/bin`; re-running replaces rather than nests, and `uninstall.sh` removes it.

Native coordination uses these MCP tools:

Expand Down Expand Up @@ -121,7 +166,7 @@ PowerShell:
| `clodex clean <run-id>` | Remove the kept git worktree of a finished run (artifacts and patch stay) |
| `clodex task start/get/cancel/list` | Manage durable async runs |
| `clodex trace export <run-id>` | Print a run trace as JSONL |
| `clodex hooks print/install/ingest` | Generate or ingest Claude Code hook events |
| `clodex hooks print/install/uninstall/ingest` | Manage the Claude Code hook integration (`install --scope local\|project\|user`) |
| `clodex eval run` | Run local harness smoke evals |
| `clodex queue add/list/update` | Manage the local task ledger |
| `clodex status` | Show recent tasks and runs |
Expand Down
35 changes: 24 additions & 11 deletions clodex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .config import ConfigError, resolve_repo_root
from .doctor import run_doctor
from .evals import run_local_evals
from .hooks import hook_config, ingest_hook_event
from .hooks import derive_run_id, hook_config, ingest_hook_event, install_hooks, parse_hook_payload
from .mcp_server import main as mcp_main
from .migrate import migrate_contract
from .models import ModelRetiredError
Expand Down Expand Up @@ -143,11 +143,15 @@ def _main(argv: list[str] | None = None) -> int:

hooks = sub.add_parser("hooks", help="Print or ingest Claude Code hook events")
hooks_sub = hooks.add_subparsers(dest="hooks_command", required=True)
hooks_sub.add_parser("print")
hooks_install = hooks_sub.add_parser("install")
hooks_install.add_argument("--dry-run", action="store_true", required=True)
hooks_ingest = hooks_sub.add_parser("ingest")
hooks_ingest.add_argument("--run-id", default="manual-hook-event")
hooks_sub.add_parser("print", help="Print the hooks block for a Claude Code settings file")
for name, help_text in (("install", "Add Clodex's hooks to a Claude Code settings file"), ("uninstall", "Remove Clodex's hooks again")):
hooks_edit = hooks_sub.add_parser(name, help=help_text)
hooks_edit.add_argument("--scope", choices=["local", "project", "user"], default="local", help="local: .claude/settings.local.json (default), project: .claude/settings.json, user: ~/.claude/settings.json")
hooks_edit.add_argument("--dry-run", action="store_true")
hooks_edit.add_argument("--force", action="store_true", help="Replace a settings file that is not valid JSON")
hooks_ingest = hooks_sub.add_parser("ingest", help="Record one hook event from stdin (used by the installed hooks)")
hooks_ingest.add_argument("--run-id", default=None, help="Default: $CLODEX_RUN_ID, else the session id from the payload")
hooks_ingest.add_argument("--verbose", action="store_true", help="Print the result (hooks must stay silent: stdout can reach Claude's context)")

sub.add_parser("status", help="Show recent tasks and runs")
sub.add_parser("mcp-server", help="Run the Clodex MCP stdio server")
Expand Down Expand Up @@ -316,14 +320,23 @@ def handle_trace(args: argparse.Namespace, workflow: ClodexWorkflow, as_json: bo

def handle_hooks(args: argparse.Namespace, as_json: bool) -> int:
if args.hooks_command == "print":
print_output(hook_config(resolve_repo_root()), as_json)
print_output(hook_config(), as_json)
return 0
if args.hooks_command == "install":
print_output({"dry_run": True, "config": hook_config(resolve_repo_root())}, as_json)
if args.hooks_command in {"install", "uninstall"}:
result = install_hooks(resolve_repo_root(), args.scope, dry_run=args.dry_run, remove=args.hooks_command == "uninstall", force=args.force)
print_output(result, as_json)
return 0
if args.hooks_command == "ingest":
payload = json.loads(sys.stdin.read() or "{}")
print_output(ingest_hook_event(resolve_repo_root(), args.run_id, payload), as_json)
# A hook that exits 2 blocks the action it is attached to, and its stdout can reach
# Claude's context: so never fail loudly, and stay silent unless asked.
try:
payload = parse_hook_payload(sys.stdin.read())
result = ingest_hook_event(resolve_repo_root(), derive_run_id(args.run_id, payload), payload)
except Exception as exc: # noqa: BLE001
print(f"clodex hooks ingest: {exc}", file=sys.stderr)
return 1
if args.verbose or as_json:
print_output(result, as_json)
return 0
return 2

Expand Down
14 changes: 11 additions & 3 deletions clodex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,17 @@ def tracing(self) -> dict[str, Any]:


def resolve_repo_root() -> Path:
"""The repo Clodex should work on: $CLODEX_REPO_ROOT if set (MCP clients start servers from anywhere), else the git root above the cwd."""
override = os.environ.get("CLODEX_REPO_ROOT")
return find_repo_root(Path(override)) if override else find_repo_root()
"""The repo Clodex should work on.

$CLODEX_REPO_ROOT wins. Claude Code sets $CLAUDE_PROJECT_DIR for the MCP servers and hooks it
starts, which matters because user-scope servers run with ~/.claude as their cwd. Otherwise
use the git root above the current directory.
"""
for variable in ("CLODEX_REPO_ROOT", "CLAUDE_PROJECT_DIR"):
override = os.environ.get(variable)
if override and Path(override).is_dir():
return find_repo_root(Path(override))
return find_repo_root()


def find_repo_root(start: Path | None = None) -> Path:
Expand Down
Loading
Loading