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
2 changes: 1 addition & 1 deletion adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ file you need into your project and edit it.
|---|---|---|
| [claude-code/](claude-code/) | Anthropic's Claude Code CLI | `.mcp.json` snippet, `CLAUDE.md` excerpt |
| [cursor/](cursor/) | Cursor IDE | `mcp.json` snippet |
| [codex/](codex/) | OpenAI's Codex CLI | `config.toml` snippet |
| [codex/](codex/) | OpenAI's Codex CLI | tiered install: `.codex/config.toml` merge, `AGENTS.md` excerpt, skills, capture hook |
| [continue/](continue/) | Continue.dev | `config.json` snippet |
| [openclaw/](openclaw/) | OpenClaw plugin host | `.openclaw/plugins.json`, `AGENTS.md` excerpt |
| [generic-mcp/](generic-mcp/) | Any MCP-speaking host | annotated reference |
Expand Down
96 changes: 69 additions & 27 deletions adapters/codex/README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
# Codex CLI adapter

Wires `vouch serve` into [OpenAI's Codex CLI][codex] as an MCP server.
Wires vouch into [OpenAI's Codex CLI][codex]: the MCP server, standing
`AGENTS.md` instructions, the vouch guided flows as skills, and
automatic session capture.

[codex]: https://github.com/openai/codex

## Setup
## Install

```bash
vouch install-mcp codex
vouch install-mcp codex # everything (T1–T4)
vouch install-mcp codex --tier T1 # just the MCP wire
```

This writes the `vouch` MCP entry into the *project-local*
`<project>/.codex/config.toml` (deep-merged into any existing one, so
your other servers and settings are preserved) — never into
`~/.codex/config.toml`, per the project-scoped install rule. Codex
loads project-local `.codex/` config for trusted projects, so trust
the project when codex asks.
Everything lands in the *project*, never in `~/.codex` — the same
scope rule every adapter follows. Codex loads project-local `.codex/`
config for trusted projects, so trust the project when codex asks.

Prefer a user-global setup, or no installer? Add the entry to
`~/.codex/config.toml` (or `<project>/.codex/config.toml`) by hand:
What each tier adds (tiers stack):

```toml
[mcp_servers.vouch]
command = "vouch"
args = ["serve"]
| Tier | File | What it does |
|---|---|---|
| T1 | `.codex/config.toml` | registers the `vouch` MCP server (`kb_search`, `kb_propose_claim`, …) with `VOUCH_AGENT=codex` for audit attribution |
| T2 | `AGENTS.md` | fenced snippet with the standing rules: recall first, all writes via proposals, review stays human |
| T3 | `.codex/skills/vouch-*/SKILL.md` | the nine vouch guided flows as project-local skills |
| T4 | `.codex/hooks.json` | `Stop` hook that auto-captures each session into a pending, review-gated summary |

[mcp_servers.vouch.env]
VOUCH_AGENT = "codex"
```

Restart any running `codex` session.
The install is idempotent and merge-safe: an existing
`.codex/config.toml` or `.codex/hooks.json` is deep-merged into (your
entries always win on conflict), an existing `AGENTS.md` gets the
snippet appended inside fence markers, and re-runs are a flat no-op.

## Skills (T3)

`vouch install-mcp codex --tier T3` also drops the vouch guided flows
(`vouch-recall`, `vouch-status`, `vouch-resolve-issue`,
`vouch-propose-from-pr`, plus the company-brain set) into the
project-local `.codex/skills/` directory. Codex discovers skills from
`<project>/.codex/skills/` in trusted projects, so they surface in the
session automatically — ask for a skill by name (e.g. "use the
vouch-recall skill for X") or let codex pick them up from context.
Codex discovers skills from `<project>/.codex/skills/` in trusted
projects, so the flows (`vouch-recall`, `vouch-status`,
`vouch-resolve-issue`, `vouch-propose-from-pr`, plus the company-brain
set) surface in the session automatically — ask for one by name or let
codex pick them up from context.

Why skills and not custom prompts: codex loads custom prompts only
from `~/.codex/prompts/` (user-global) and has deprecated them in
Expand All @@ -52,6 +50,50 @@ yourself.
The skill bodies are identical to the claude-code slash commands
(enforced by a sync test), so the flows behave the same on every host.

## Automatic session capture (T4)

`.codex/hooks.json` registers a `Stop` hook that runs `vouch capture
ingest-codex --hook` when a turn completes. The handler reads the hook
payload, resolves the session's rollout file, and rolls it into ONE
pending session-summary proposal — the same review-gated summary a
claude-code session produces. Because `Stop` fires per turn, re-ingest
is idempotent: an unchanged session is a no-op, a session that grew
refreshes its pending proposal in place, and a proposal you've already
reviewed is never resurrected.

Failure semantics match `capture observe`: the `--hook` mode exits 0
no matter what, so a capture problem can never break your codex turn.
Nothing is auto-approved — review with `vouch review`.

Past sessions can be ingested by hand too:

```bash
vouch capture ingest-codex --latest # newest rollout for this project
vouch capture ingest-codex <rollout.jsonl> # a specific one
```

Why not codex's `notify` setting: codex honours `notify` only in
user-global config (`~/.codex/config.toml`), which a project-scoped
install never touches. If you prefer notify anyway, point it at a
wrapper that calls `vouch capture ingest-codex --hook` yourself.

## Manual fallback

No installer, or a user-global setup on purpose? Add the entry to
`~/.codex/config.toml` (or `<project>/.codex/config.toml`) by hand:

```toml
[mcp_servers.vouch]
command = "vouch"
args = ["serve"]

[mcp_servers.vouch.env]
VOUCH_AGENT = "codex"
```

Restart any running `codex` session, then confirm with
`codex mcp list`.

## Notes

- Codex respects MCP tool naming verbatim, so the tools appear as
Expand Down
15 changes: 15 additions & 0 deletions adapters/codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "vouch capture ingest-codex --hook",
"statusMessage": "vouch capture"
}
]
}
]
}
}
11 changes: 11 additions & 0 deletions adapters/codex/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ tiers:
- { src: ../openclaw/skills/vouch-record/SKILL.md, dst: .codex/skills/vouch-record/SKILL.md }
- { src: ../openclaw/skills/vouch-followup/SKILL.md, dst: .codex/skills/vouch-followup/SKILL.md }
- { src: ../openclaw/skills/vouch-standup/SKILL.md, dst: .codex/skills/vouch-standup/SKILL.md }
# T4 = automatic session capture -- see issue #388. codex's hooks system
# fires Stop when a turn completes; the handler re-ingests the session's
# rollout idempotently (`vouch capture ingest-codex --hook` exits 0 even
# on failure, so capture can never break a codex turn), updating the
# session's single PENDING summary proposal as the session grows.
# hooks live in their own `.codex/hooks.json` file (project-local in
# trusted projects) and json_merge preserves any hooks the user already
# has. note: the legacy `notify` setting can't be used here -- codex only
# honours it in user-global config, which the #179 rule forbids touching.
T4:
- { src: hooks.json, dst: .codex/hooks.json, json_merge: true }
14 changes: 11 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,17 @@ Add `-e VOUCH_AGENT=claude-code` to attribute the agent's proposals to it
rather than your shell user. Confirm with `claude mcp list` (look for
`vouch … ✓ Connected`).

Prefer a config file, or want the brain-first `CLAUDE.md`, slash commands, and
hooks too? Run `vouch install-mcp claude-code` — or drop this into `.mcp.json`
at the project root by hand:
Prefer a config file, or want the brain-first instructions, guided flows, and
capture hooks too? The installer ships the full tiered setup for either host:

```bash
vouch install-mcp claude-code # .mcp.json + CLAUDE.md + slash commands + hooks
vouch install-mcp codex # .codex/config.toml + AGENTS.md + skills + capture
```

Both are idempotent and merge-safe — existing config files are deep-merged
into, never clobbered. Or drop this into `.mcp.json` at the project root by
hand:

```json
{
Expand Down
34 changes: 28 additions & 6 deletions src/vouch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2079,22 +2079,44 @@ def capture_finalize_all_cmd(session_id: str | None, max_age_seconds: float) ->
"--latest", is_flag=True,
help="Resolve the newest codex rollout recorded for this project (by cwd).",
)
@click.option(
"--hook", "hook_mode", is_flag=True,
help="Read a codex Stop-hook payload from stdin; never fails the host "
"(exits 0 even on errors, like `capture observe`).",
)
@click.option(
"--codex-home", type=click.Path(file_okay=False, path_type=Path), default=None,
help="Codex state dir holding sessions/ (default: $CODEX_HOME or ~/.codex).",
)
def capture_ingest_codex_cmd(
rollout: Path | None, latest: bool, codex_home: Path | None
rollout: Path | None, latest: bool, hook_mode: bool, codex_home: Path | None
) -> None:
"""Ingest one codex session rollout into a PENDING summary proposal.

Codex has no live hook stream; it persists each session as a rollout
jsonl instead. This maps the rollout's tool calls into the same
observation shape `capture observe` produces and reuses the existing
rollup, so the result is the same review-gated summary a claude
session yields. Re-ingesting a session is a no-op; review with
Codex has no live notify stream vouch can use project-locally; it
persists each session as a rollout jsonl instead. This maps the
rollout's tool calls into the same observation shape `capture observe`
produces and reuses the existing rollup, so the result is the same
review-gated summary a claude session yields. Re-ingesting an
unchanged session is a no-op; a session that grew since the last
ingest refreshes its one PENDING proposal in place. Review with
`vouch review`.
"""
if hook_mode:
# Hook wire (codex `Stop` event): parse the stdin payload, resolve
# the session's rollout, ingest idempotently. Exits 0 no matter
# what — capture must never break the user's codex turn.
try:
raw = "" if sys.stdin.isatty() else sys.stdin.read()
payload = json.loads(raw) if raw.strip() else {}
if isinstance(payload, dict):
codex_rollout_mod.ingest_hook_payload(
_capture_store(), payload, codex_home=codex_home
)
except Exception:
# the hook contract is exit 0 — never surface an error here.
pass
return
if (rollout is None) == (not latest):
raise click.ClickException("pass exactly one of ROLLOUT or --latest")
store = _load_store()
Expand Down
Loading
Loading