diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd3a97..aaa2d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- **Codex (OpenAI) support — `axme-code setup --ide=codex`.** Writes `.codex/config.toml` (`[mcp_servers.axme]` with an absolute, PATH-independent command), `.codex/hooks.json` (SessionStart + PreToolUse + PostToolUse + SessionEnd, Codex's Claude-Code-compatible hook protocol) and the `## AXME Code` section in `AGENTS.md` (Codex's CLAUDE.md analogue, including the `#!axme` git gate). New `codex` hook adapter normalizes Codex's `apply_patch` edits (one call, several files) into the IDE-agnostic file tracking and safety cores; new `hook session-start` subcommand prints the "call axme_context" nudge as Codex developer context. The background auditor keeps running on the Claude Agent SDK; Codex's own transcript is not yet parsed. +- **Codex (OpenAI) support — `axme-code setup --ide=codex`.** Writes `.codex/config.toml` (`[mcp_servers.axme]` with an absolute, PATH-independent command), `.codex/hooks.json` (SessionStart + PreToolUse + PostToolUse + SessionEnd, Codex's Claude-Code-compatible hook protocol) and the `## AXME Code` section in `AGENTS.md` (Codex's CLAUDE.md analogue, including the `#!axme` git gate). New `codex` hook adapter normalizes Codex's `apply_patch` edits (one call, several files) into the IDE-agnostic file tracking and safety cores; new `hook session-start` subcommand prints the "call axme_context" nudge as Codex developer context. The background auditor keeps running on the Claude Agent SDK; Codex's own transcript is not yet parsed. Verified live on codex-cli 0.166: the generated `[mcp_servers.axme]` table sets `default_tools_approval_mode = "approve"` so `axme_*` calls run without per-call prompts (`"auto"` still prompts and fails under `codex exec`); the SessionStart hook emits the documented `hookSpecificOutput.additionalContext` JSON (plain text starting with `[` is mis-parsed as JSON by Codex); project hooks are skipped until trusted via `/hooks`, and setup says so. - **Shell-side denied-path enforcement.** `checkBash` now blocks shell reads of denied filesystem paths (`cat ~/.ssh/id_rsa`, `/etc/shadow`), quoted or chained. Codex routes every file read through the shell, so without this the filesystem deny rules did not apply there at all; on Claude Code and Cursor it closes the same gap for `Bash`. Only absolute and `~`-relative deny patterns participate — bare basename patterns (`.env`, `*.pem`) are deliberately excluded so commit messages and heredocs that mention such a file are not blocked. ## [0.6.3] - 2026-06-25 diff --git a/docs/MULTI_CLIENT.md b/docs/MULTI_CLIENT.md index bac8030..f102843 100644 --- a/docs/MULTI_CLIENT.md +++ b/docs/MULTI_CLIENT.md @@ -75,9 +75,11 @@ This writes three project-level files and leaves Claude Code's files alone: - `.codex/hooks.json` — `SessionStart` (prints the "call axme_context" nudge as developer context), `PreToolUse` (safety + git gate), `PostToolUse` (file tracking from `apply_patch`), `SessionEnd` (spawns the detached auditor; Codex caps this hook at 3s, which is enough). - `AGENTS.md` — the `## AXME Code` section (the CLAUDE.md analogue), including the `#!axme` commit/push gate rule. -Two Codex-specific notes: +Three Codex-specific notes: - **Trust the project.** Codex loads a project's `.codex/` layer only for trusted projects. Accept the trust prompt the first time you run `codex` in the repo, or add `[projects.""]` with `trust_level = "trusted"` to `~/.codex/config.toml`. +- **Trust the hooks.** Codex runs non-managed hooks only after you review them: in your first session run `/hooks` and trust the four AXME entries. Trust is bound to each hook's hash, so after anything changes the hook command (a release that moves the binary, re-running setup from another install) `/hooks` asks again. Until then the hooks are skipped silently — no safety gate, no file tracking. `codex exec` never prompts; it has `--dangerously-bypass-hook-trust` for automation you have vetted. +- **MCP approvals are pre-granted.** The generated `[mcp_servers.axme]` table sets `default_tools_approval_mode = "approve"` (`"auto"` still prompts), so `axme_*` calls run without a per-call prompt (and work under `codex exec`, whose approval policy is `never`). The tools only read and write the project's `.axme-code/`. - **Shell reads are policed.** Codex has no `Read` tool — every file read is a shell command — so the `PreToolUse` hook also blocks shell reads of denied paths (`cat ~/.ssh/id_rsa`, `/etc/shadow`, ...). Only absolute and `~`-relative deny patterns apply to the shell; bare basename patterns such as `.env` do not, so commit messages and heredocs that merely mention a file are not blocked. The same `.axme-code/` knowledge base is shared with Claude Code, so you can switch between the two on one project. diff --git a/src/cli.ts b/src/cli.ts index b3ffe7a..353d370 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -728,7 +728,8 @@ async function main() { console.log("\nDone! Run 'codex' in this project — AXME tools are now available."); console.log(" Codex loads the project's .codex/ config and hooks only for TRUSTED projects:"); console.log(" accept the trust prompt on first launch (or set projects.\"\".trust_level = \"trusted\""); - console.log(" in ~/.codex/config.toml), then start a new session."); + console.log(" in ~/.codex/config.toml). Then, in that first session, run /hooks and trust the"); + console.log(" four AXME hooks — Codex skips project hooks silently until they are reviewed."); } else { console.log("\nDone! Run 'claude' to start using AXME tools."); } diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index f2ed2bf..2c44a03 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -8,8 +8,11 @@ * written here — AGENTS.md already carries the full instructions; this is * the per-session reminder that survives even when the agent skims it. * - * Output is plain text on purpose (not the hookSpecificOutput JSON form): - * both reach the model, and plain text cannot be mis-parsed. + * Output uses the documented JSON form + * (`hookSpecificOutput.additionalContext`). Plain text would also reach the + * model — except that ours starts with "[", which Codex takes as the start + * of a JSON document, fails to parse, and reports the hook as Failed with + * no context delivered (seen live on codex-cli 0.166). */ import { configExists } from "../storage/config.js"; @@ -28,12 +31,11 @@ export async function runSessionStartHook(workspacePath?: string): Promise } const root = workspacePath ?? cwdFromStdin ?? process.cwd(); - if (configExists(root)) { - process.stdout.write("[AXME Code] Knowledge base ready. Call axme_context now.\n"); - } else { - process.stdout.write( - "[AXME Code] Project not initialized. Ask the user to run `axme-code setup --ide=codex` " + - "in a terminal, then start a new Codex session.\n", - ); - } + const additionalContext = configExists(root) + ? "[AXME Code] Knowledge base ready. Call axme_context now." + : "[AXME Code] Project not initialized. Ask the user to run `axme-code setup --ide=codex` " + + "in a terminal, then start a new Codex session."; + process.stdout.write( + JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } }) + "\n", + ); } diff --git a/src/setup/codex-writers.ts b/src/setup/codex-writers.ts index 2df4ccd..ad11e63 100644 --- a/src/setup/codex-writers.ts +++ b/src/setup/codex-writers.ts @@ -11,6 +11,19 @@ * ~/.codex/config.toml). Codex offers to set that the first time it opens * the repo; setup prints a reminder. * + * Two more Codex gates, both verified live on codex-cli 0.166 (2026-11-02): + * - Hook trust. Non-managed hooks run only after the user reviews and + * trusts them via `/hooks` in an interactive session. Trust is recorded + * against the hook definition's hash, so any change to the command + * (e.g. the binary path after a release) needs a re-trust. Untrusted + * hooks are skipped silently — `codex exec` has + * `--dangerously-bypass-hook-trust` for automation. + * - MCP tool approval. Without `default_tools_approval_mode = "approve"` + * on the server table, every axme_* call prompts in the TUI and fails + * outright under `codex exec` ("requires approval, but approval policy + * is never"). "auto" is NOT enough — verified live, it still prompted. + * The writer sets "approve": AXME tools only touch .axme-code/. + * * Codex's hooks.json is the Claude Code settings.json `hooks` shape * verbatim (PascalCase events, `{ matcher?, hooks: [{ type, command, * timeout }] }` groups), so the writer mirrors configureHooks() in cli.ts @@ -140,6 +153,9 @@ function mcpServerTable(serve: ServeCommand): string { "[mcp_servers.axme]", `command = ${tomlString(serve.command)}`, `args = [${serve.args.map(tomlString).join(", ")}]`, + "# AXME tools only read/write this project's .axme-code/ knowledge base:", + "# run them without a per-call approval prompt (required under `codex exec`).", + 'default_tools_approval_mode = "approve"', "", ].join("\n"); } diff --git a/test/codex-hooks-e2e.test.ts b/test/codex-hooks-e2e.test.ts index 9c7c96a..96c3fdc 100644 --- a/test/codex-hooks-e2e.test.ts +++ b/test/codex-hooks-e2e.test.ts @@ -127,6 +127,8 @@ describe("codex hooks end-to-end", () => { assert.equal(r.status, 0, `stderr: ${r.stderr}`); // ROOT has .axme-code/ but no config.yaml → "not initialized" branch; the // real repo (REPO_ROOT) is initialized → ready branch. Check both texts. - assert.match(r.stdout, /\[AXME Code\] Project not initialized/); + const parsed = JSON.parse(r.stdout) as { hookSpecificOutput: { hookEventName: string; additionalContext: string } }; + assert.equal(parsed.hookSpecificOutput.hookEventName, "SessionStart"); + assert.match(parsed.hookSpecificOutput.additionalContext, /\[AXME Code\] Project not initialized/); }); }); diff --git a/test/codex-setup-writers.test.ts b/test/codex-setup-writers.test.ts index 743acdb..3c3bc3b 100644 --- a/test/codex-setup-writers.test.ts +++ b/test/codex-setup-writers.test.ts @@ -91,7 +91,9 @@ describe("writeCodexConfigToml", () => { it("creates .codex/config.toml with the axme table (absolute command + args)", () => { writeCodexConfigToml(ROOT, SERVE); const text = readFileSync(join(ROOT, ".codex", "config.toml"), "utf-8"); - assert.equal(text, '[mcp_servers.axme]\ncommand = "/bin/node"\nargs = ["/usr/local/bin/axme-code", "serve"]\n'); + assert.ok(text.startsWith('[mcp_servers.axme]\ncommand = "/bin/node"\nargs = ["/usr/local/bin/axme-code", "serve"]\n'), text); + // Without this every axme_* call prompts in the TUI and fails under codex exec. + assert.match(text, /^default_tools_approval_mode = "approve"$/m); }); it("appends to an existing config without touching other tables or comments", () => {