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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

### 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.
- **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

Reliability release for the `axme_save_memory` / `axme_save_decision` write tools, driven by two independent agent sessions that hit the same failure mode in production.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ AXME Code is a [stdio MCP server](https://modelcontextprotocol.io/) — every MC
| Client | MCP Tools | Safety Hooks | Auto Auditor |
|---|---|---|---|
| **Claude Code** (CLI / VS Code) | ✅ Full | ✅ Full | ✅ Yes |
| **Codex** (CLI / IDE extension) — `axme-code setup --ide=codex` | ✅ Full | ✅ Full | ✅ Yes |
| **Cursor** | ✅ Full | ❌ | ❌ |
| **Windsurf** | ✅ Full | ❌ | ❌ |
| **Cline** (VS Code) | ✅ Full | ❌ | ❌ |
Expand All @@ -229,7 +230,7 @@ Just place it in your client's MCP config file:
- **Cline:** VS Code Settings → Cline MCP → `cline_mcp_settings.json`
- **Claude Desktop:** `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or equivalent

Pre-execution safety hooks, the post-tool-use file tracker, and the background session auditor are Claude Code-specific (they require Claude Code's hook system). In other clients, the agent must call AXME tools explicitly — same knowledge base, same `.axme-code/` storage, just no automatic enforcement layer.
Pre-execution safety hooks, the post-tool-use file tracker, and the background session auditor need a hook system: Claude Code has one, and so does Codex (`axme-code setup --ide=codex` writes `.codex/config.toml`, `.codex/hooks.json` and the `AGENTS.md` section — see [docs/MULTI_CLIENT.md](docs/MULTI_CLIENT.md#codex-openai)). In other clients, the agent must call AXME tools explicitly — same knowledge base, same `.axme-code/` storage, just no automatic enforcement layer.

See [docs/MULTI_CLIENT.md](docs/MULTI_CLIENT.md) for full per-client setup, hook workarounds, and concurrent-client semantics.

Expand Down
31 changes: 28 additions & 3 deletions docs/MULTI_CLIENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ AXME Code is built as a [stdio MCP server](https://modelcontextprotocol.io/) —
| Client | MCP Tools | Safety Hooks | Session-end Auditor | File-change Tracking |
|--------|-----------|--------------|---------------------|----------------------|
| **Claude Code** (CLI / VS Code) | ✅ Full | ✅ Full | ✅ Automatic | ✅ Automatic |
| **Codex** (CLI / IDE extension) | ✅ Full | ✅ Full (`--ide=codex`) | ✅ Automatic¹ | ✅ Automatic |
| **Cursor** | ✅ Full | ❌ | ❌ | ❌ |
| **Windsurf** | ✅ Full | ❌ | ❌ | ❌ |
| **Cline** (VS Code) | ✅ Full | ❌ | ❌ | ❌ |
Expand All @@ -15,7 +16,9 @@ AXME Code is built as a [stdio MCP server](https://modelcontextprotocol.io/) —

**MCP tools available everywhere** — the full set of 19 tools (`axme_context`, `axme_save_memory`, `axme_save_decision`, `axme_safety`, `axme_status`, etc.) works in any MCP client. Your agent can read and write the knowledge base.

**Hooks are Claude Code-only** — pre-execution safety hooks, post-tool-use file tracking, and the session-end background auditor depend on Claude Code's hook system. In other clients, the agent must explicitly call AXME tools (e.g., to save memories at session close).
**Hooks need a hook system** — pre-execution safety hooks, post-tool-use file tracking, and the session-end background auditor run on Claude Code and on Codex (whose hook protocol is Claude-Code-compatible). In other clients, the agent must explicitly call AXME tools (e.g., to save memories at session close).

¹ The background auditor still runs on the Claude Agent SDK, so it needs a `claude` login or `ANTHROPIC_API_KEY` even when the session itself ran in Codex. Codex's own transcript is not yet fed to the auditor; the audit sees the files changed, the worklog, and whatever the agent saved during the session.

## Quick Start (any client)

Expand Down Expand Up @@ -57,6 +60,28 @@ Just place it in the right config file for your client.

Already done by `axme-code setup` — `.mcp.json` is created in your project root. Just run `claude` and start using it.

### Codex (OpenAI)

Full support — MCP tools, safety hooks, file tracking and the session-end auditor:

```bash
cd your-project
axme-code setup --ide=codex
```

This writes three project-level files and leaves Claude Code's files alone:

- `.codex/config.toml` — `[mcp_servers.axme]` pointing at the installed binary (absolute path, so it also works when Codex is launched from the IDE extension or the desktop app).
- `.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:

- **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."<absolute path>"]` with `trust_level = "trusted"` to `~/.codex/config.toml`.
- **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.

### Cursor

Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per-project):
Expand Down Expand Up @@ -165,8 +190,8 @@ The knowledge base is the single source of truth regardless of which client crea

## Limitations

- **Hooks are not portable** — Claude Code's hook system is unique. Other clients will not get automatic safety blocking, file tracking, or session-end audits.
- **CLAUDE.md auto-injection** — only Claude Code reads `CLAUDE.md`. Other clients ignore it; you may need to provide instructions to your agent another way (e.g., a system prompt).
- **Hooks are not portable** — only Claude Code and Codex have a hook system AXME can plug into. Other clients will not get automatic safety blocking, file tracking, or session-end audits.
- **CLAUDE.md auto-injection** — only Claude Code reads `CLAUDE.md`; Codex reads `AGENTS.md` (written by `--ide=codex`). Other clients ignore both; you may need to provide instructions to your agent another way (e.g., a system prompt).
- **`.mcp.json` is Claude Code-specific** — `axme-code setup` writes this file, but other clients use different config locations (see above).
- **Session-to-session linking** — AXME tracks Claude Code's session IDs to link conversations across VS Code windows. Other clients don't expose session IDs the same way, so cross-window session linking may not work; sessions will still be tracked individually.

Expand Down
52 changes: 44 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* axme-code setup [path] - Full init (LLM if API key available) + .mcp.json + CLAUDE.md
* axme-code serve - Start MCP server (stdio, used by .mcp.json)
* axme-code status [path] - Show project status
* axme-code hook <name> <json> - Run hook (pre-tool-use, post-tool-use, session-end)
* axme-code hook <name> <json> - Run hook (session-start, pre-tool-use, post-tool-use, session-end)
*/

import { resolve, join, basename } from "node:path";
Expand Down Expand Up @@ -251,6 +251,17 @@ function buildHookCommand(hookName: string, projectPath: string): string {
return `${q(nodeExec)} ${q(self)} hook ${hookName} --workspace ${q(projectPath)}`;
}

/**
* MCP server launch spec for config writers that need an absolute,
* PATH-independent command (Codex's config.toml — Codex launched from the
* IDE extension or the desktop app does not necessarily see the login
* shell's PATH). Same ingredients as buildHookCommand(): the running node
* binary plus the running axme-code entry file.
*/
function buildServeCommand(): { command: string; args: string[] } {
return { command: process.execPath, args: [resolve(process.argv[1] ?? "axme-code"), "serve"] };
}

function configureHooks(projectPath: string): void {
const claudeDir = join(projectPath, ".claude");
const settingsPath = join(claudeDir, "settings.json");
Expand Down Expand Up @@ -355,10 +366,11 @@ function usage(): void {
console.log(`AXME Code - Persistent memory, decisions, and safety guardrails for Claude Code

Usage:
axme-code setup [path] [--force] [--ide=<claude-code|cursor>]
axme-code setup [path] [--force] [--ide=<claude-code|cursor|codex>]
Initialize project (LLM scan + .mcp.json + CLAUDE.md
for Claude Code, or .cursor/{mcp,hooks}.json + rules
/axme-code.mdc for Cursor). Defaults to claude-code.
for Claude Code; .cursor/{mcp,hooks}.json + rules
/axme-code.mdc for Cursor; .codex/{config.toml,
hooks.json} + AGENTS.md for Codex). Defaults to claude-code.
axme-code serve Start MCP server (stdio transport)
axme-code self-test Run local healthcheck (storage write,
hook parse, MCP boot). Exits 0 on
Expand Down Expand Up @@ -413,9 +425,9 @@ async function main() {
const pluginMode = args.includes("--plugin") || !!process.env.CLAUDE_PLUGIN_ROOT;
const { parseIdeFlag } = await import("./utils/ide-detect.js");
const ide = parseIdeFlag(args) ?? "claude-code";
if (ide === "cursor" && pluginMode) {
console.error("Error: --ide=cursor is not supported with --plugin in this release.");
console.error("Run setup without --plugin (Cursor plugin packaging is a Phase 2 follow-up).");
if (ide !== "claude-code" && pluginMode) {
console.error(`Error: --ide=${ide} is not supported with --plugin in this release.`);
console.error("Run setup without --plugin (per-IDE plugin packaging is a later follow-up).");
process.exit(1);
}
// Strip --force, --plugin, --ide and its value from positional args.
Expand All @@ -431,7 +443,7 @@ async function main() {
if (a === "--help" || a === "-h") { usage(); process.exit(0); }
if (a.startsWith("-")) {
console.error(`Error: unknown flag for setup: ${a}`);
console.error("Usage: axme-code setup [path] [--force] [--plugin] [--ide=<claude-code|cursor>]");
console.error("Usage: axme-code setup [path] [--force] [--plugin] [--ide=<claude-code|cursor|codex>]");
process.exit(2);
}
setupArgs.push(a);
Expand Down Expand Up @@ -630,6 +642,21 @@ async function main() {
console.log(" .cursor/hooks.json: hooks configured (preToolUse + postToolUse + sessionEnd)");
writeCursorRulesMdc(projectPath, isWorkspace);
console.log(" .cursor/rules/axme-code.mdc: created");
} else if (ide === "codex") {
// Codex branch: .codex/config.toml + .codex/hooks.json + AGENTS.md.
// Skip CLAUDE.md and .claude/settings.json (Codex reads neither).
const { writeCodexConfigToml, writeCodexHooksJson, writeAgentsMdSection, buildAgentsMdSection } =
await import("./setup/codex-writers.js");
const codexPaths = [projectPath];
if (isWorkspace) {
for (const p of ws.projects) codexPaths.push(join(projectPath, p.path));
}
for (const dir of codexPaths) writeCodexConfigToml(dir, buildServeCommand());
console.log(` .codex/config.toml: mcp_servers.axme updated (${codexPaths.length} locations)`);
writeCodexHooksJson(projectPath, buildHookCommand);
console.log(" .codex/hooks.json: hooks configured (SessionStart + PreToolUse + PostToolUse + SessionEnd)");
const section = buildAgentsMdSection(isWorkspace ? WORKSPACE_CLAUDE_MD : SINGLE_REPO_CLAUDE_MD);
console.log(` AGENTS.md: ${writeAgentsMdSection(projectPath, section)} AXME Code section`);
} else {
// Claude Code branch (unchanged): CLAUDE.md + .claude/settings.json
generateClaudeMd(projectPath, isWorkspace);
Expand Down Expand Up @@ -697,6 +724,11 @@ async function main() {
// Tell each user what their next step actually is.
if (ide === "cursor") {
console.log("\nDone! Open a new chat in Cursor — AXME tools are now available.");
} else if (ide === "codex") {
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.\"<path>\".trust_level = \"trusted\"");
console.log(" in ~/.codex/config.toml), then start a new session.");
} else {
console.log("\nDone! Run 'claude' to start using AXME tools.");
}
Expand Down Expand Up @@ -741,6 +773,10 @@ async function main() {
} else if (hookName === "session-end") {
const { runSessionEndHook } = await import("./hooks/session-end.js");
await runSessionEndHook(workspacePath, ide);
} else if (hookName === "session-start") {
// Codex only: stdout becomes developer context for the session.
const { runSessionStartHook } = await import("./hooks/session-start.js");
await runSessionStartHook(workspacePath);
}
break;
}
Expand Down
104 changes: 104 additions & 0 deletions src/hooks/adapters/codex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Codex CLI hook adapter.
*
* Stdin shape (per learn.chatgpt.com/docs/hooks, checked against codex-cli
* 0.166, 2026-11): the common base carries session_id, transcript_path
* (string | null), cwd, hook_event_name, model, permission_mode, turn_id.
* PreToolUse / PostToolUse add tool_name, tool_input, tool_use_id (and
* tool_response on Post). SessionEnd adds reason (always "other").
*
* The protocol is deliberately Claude-Code-compatible: PascalCase event
* names, the same hooks.json shape, and the same deny envelope
* (`hookSpecificOutput.permissionDecision`, exit 0). The output adapter is
* therefore the Claude Code one, re-exported under a Codex name.
*
* What differs for the IDE-agnostic hook cores:
* - Shell commands arrive as tool_name "Bash" with tool_input.command,
* exactly like Claude Code.
* - File edits arrive as tool_name "apply_patch" with the whole patch in
* tool_input.command (Codex has no Edit / Write / Read tools). We
* normalize the name to "Edit" and collect every touched path from the
* patch header lines (`*** Add File:`, `*** Update File:`,
* `*** Delete File:`, `*** Move to:`) into `filePaths`, resolved
* against the payload's cwd when relative.
* - MCP tool calls arrive as "mcp__<server>__<tool>" and carry no shell
* or file payload, so they fall through the safety switch untouched.
*/

import { isAbsolute, resolve } from "node:path";
import { claudeCodeOutputAdapter } from "./claude-code.js";
import type { HookInputAdapter, HookOutputAdapter, NormalizedHookEvent } from "./types.js";

function asString(v: unknown): string | undefined {
return typeof v === "string" ? v : undefined;
}

function asObject(v: unknown): Record<string, unknown> | undefined {
return v && typeof v === "object" && !Array.isArray(v)
? v as Record<string, unknown>
: undefined;
}

const PATCH_HEADER = /^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/;

/**
* Extract every path named in an apply_patch envelope, in order of first
* appearance, deduplicated. Relative paths are resolved against `cwd`
* when it is known; otherwise they are returned as written.
*/
export function extractPatchPaths(patch: string, cwd?: string): string[] {
const out: string[] = [];
for (const line of patch.split(/\r?\n/)) {
const m = PATCH_HEADER.exec(line);
if (!m) continue;
const raw = m[1].trim();
if (!raw) continue;
const abs = isAbsolute(raw) || !cwd ? raw : resolve(cwd, raw);
if (!out.includes(abs)) out.push(abs);
}
return out;
}

export const codexInputAdapter: HookInputAdapter = {
parse(raw, kind): NormalizedHookEvent {
const obj = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
const rawToolName = asString(obj.tool_name);
const toolInput = asObject(obj.tool_input);
const cwd = asString(obj.cwd);
const sessionId = asString(obj.session_id);

// transcript_path is documented as string | null. Preserve null so
// callers can tell "no transcript yet" from "field absent".
let transcriptPath: string | null | undefined;
if (obj.transcript_path === null) transcriptPath = null;
else transcriptPath = asString(obj.transcript_path);

const reason = kind === "sessionEnd" ? asString(obj.reason) : undefined;

let toolName = rawToolName;
let filePaths: string[] | undefined;
if (rawToolName === "apply_patch") {
toolName = "Edit";
// The docs name `command`; the underlying custom tool call uses
// `input`. Accept both so a payload-shape drift degrades to "no
// paths found" rather than a parse failure.
const patch = asString(toolInput?.command) ?? asString(toolInput?.input) ?? "";
filePaths = extractPatchPaths(patch, cwd);
}

return {
kind,
ide: "codex",
toolName,
toolInput,
filePaths,
sessionId,
transcriptPath,
reason,
raw: obj,
};
},
};

/** Codex accepts the Claude Code deny envelope verbatim (exit 0 + JSON). */
export const codexOutputAdapter: HookOutputAdapter = claudeCodeOutputAdapter;
9 changes: 9 additions & 0 deletions src/hooks/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export interface NormalizedHookEvent {
toolName?: string;
/** PreToolUse / PostToolUse only — the arguments the agent passed. */
toolInput?: Record<string, unknown>;
/**
* Every file path this tool call touches, for IDEs that report edits in
* a shape other than Claude Code's single `tool_input.file_path`. Codex
* delivers one `apply_patch` call that may add, update or delete several
* files at once; its adapter extracts the paths here so the safety and
* file-tracking cores stay IDE-agnostic. Undefined on Claude Code and
* Cursor — handlers then fall back to `tool_input.file_path`.
*/
filePaths?: string[];
/** IDE's own session id (Claude Code session_id, Cursor session_id, etc.). */
sessionId?: string;
/** Path to the IDE's transcript file, if any. May be null on Cursor. */
Expand Down
Loading
Loading