PRD: Codex adapter
Published issue: #13
Problem Statement
Aftertone ships working post-reply speech for Cursor and Claude Code, but Codex is still listed as not shipped. Users who work in Codex cannot get the same local spoken summary after a Codex turn, even though Codex now exposes lifecycle hooks that can run deterministic command scripts at Stop and receive the final assistant message on stdin.
Solution
Ship a thin Codex adapter that reuses the existing Aftertone runtime. Codex should call an Aftertone Stop hook after each turn, pass the hook JSON through the shared hook_run --stdin pipeline, and let the existing prepare/extract/session/daemon path decide whether to speak.
The adapter should feel like the Claude adapter:
- one global install wires hooks and model guidance;
- daily usage is normal
codex;
/aftertone-on and /aftertone-off semantics stay per chat through the shared session allowlist;
- the daemon, config, voice, language, speed, and mode remain shared across adapters;
- MCP remains an optional control plane, not the speech trigger.
The official Codex hooks docs are the contract for this work: https://developers.openai.com/codex/hooks
User Stories
- As a Codex user, I want Aftertone to speak after a Codex answer, so that I can keep momentum without staring at the screen.
- As a Codex user, I want the spoken text to come from
<spoken_summary>, so that the audio is brief and intentional.
- As a Codex user, I want no spoken output when a reply omits
<spoken_summary> in tag-only mode, so that noisy fallback speech does not surprise me.
- As a Codex user, I want
/aftertone-on and /aftertone-off behavior to stay per chat, so that one session can speak while another stays silent.
- As a Codex user, I want global install to register the Codex hook, so that I do not hand-edit hook JSON.
- As a Codex user, I want the hook to start or find the daemon, so that speech still works after reboot.
- As a Codex user on Windows, I want a Windows command path or documented limitation, so that the adapter is not Linux-only by accident.
- As a maintainer, I want Codex to reuse the existing hook JSON preparation path, so that Cursor, Claude, and Codex do not drift.
- As a maintainer, I want unit tests around Codex hook payloads, so that future extract/session changes do not break Codex.
- As a maintainer, I want install and uninstall tests, so that user config files are merged and cleaned up predictably.
- As a contributor, I want
docs/adapters/codex.md, so that the supported hook contract and troubleshooting path are explicit.
- As a contributor, I want README and site status to change only after the hook path is proven, so that the public adapter table stays honest.
- As a power user, I want an optional Codex MCP config snippet, so that CLI controls can be exposed where Codex supports MCP.
- As a maintainer, I want smoke-test instructions that do not require cloud TTS or API keys, so that local verification remains private.
Implementation Decisions
- Treat Codex as a host adapter, not a new speech system.
- Use Codex
Stop as the required post-reply trigger. Codex docs say Stop runs at turn scope, receives hook JSON on stdin, includes last_assistant_message, and expects JSON or empty output on stdout.
- Reuse the existing
hook_run --stdin path. It already ensures the daemon, posts /hook, falls back to prepare plus /say, traces pipeline state, and returns {}.
- Extend shared extraction only where needed.
last_assistant_message, transcript_path, session_id, and hook_event_name already match the shape Aftertone needs.
- Register user-level Codex hooks under
~/.codex/hooks.json for global install. Codex also supports repo-local hooks, config TOML hooks, and plugin-bundled hooks, but user-level hooks best match Aftertone's global Cursor and Claude installs.
- Keep hook commands synchronous. Codex parses but does not support async command hooks yet.
- Avoid plain text stdout from Codex hooks.
Stop treats plain text stdout as invalid, so wrappers must print JSON or nothing.
- Keep shared config under the install root
.cursor/hooks/speak_summary.toml until the repo deliberately renames adapter-neutral state later.
- Put Codex model guidance in an AGENTS-compatible place and document how it gets loaded. The guidance must preserve the current rule: the spoken tag language follows TOML
lang, not the user's message language.
- Keep MCP optional. It may expose Aftertone control commands, but it must not be required for post-reply speech.
Testing Decisions
- Test external behavior with Codex-shaped hook JSON:
Stop with last_assistant_message containing <spoken_summary> produces a /say payload.
- Test no-speech cases: disabled config, disallowed session, quiet hours, unsupported event, and missing tag in tag-only mode.
- Test that hook wrappers produce valid JSON-or-empty stdout for Codex
Stop.
- Test installer merge behavior with temporary
~/.codex/hooks.json: existing hooks remain, duplicate Aftertone entries are replaced, and Windows command overrides are preserved.
- Test uninstall removes only Aftertone Codex entries.
- Add smoke-test docs that use a local synthetic Codex hook payload before asking users to verify a real Codex turn.
Out of Scope
- Replacing Supertonic ONNX or changing the daemon.
- Moving shared config out of
.cursor/hooks/.
- Making MCP the speech trigger.
- Building a Codex plugin marketplace package before the plain global hook path is proven.
- Supporting every Codex surface if the official hook docs only prove CLI behavior.
Further Notes
Codex hook docs confirm the important integration details: hooks are enabled by default, ~/.codex/hooks.json is a supported location, non-managed hooks require review/trust, Stop receives last_assistant_message, and Stop expects JSON on stdout when exiting successfully.
PRD: Codex adapter
Published issue: #13
Problem Statement
Aftertone ships working post-reply speech for Cursor and Claude Code, but Codex is still listed as not shipped. Users who work in Codex cannot get the same local spoken summary after a Codex turn, even though Codex now exposes lifecycle hooks that can run deterministic command scripts at
Stopand receive the final assistant message on stdin.Solution
Ship a thin Codex adapter that reuses the existing Aftertone runtime. Codex should call an Aftertone
Stophook after each turn, pass the hook JSON through the sharedhook_run --stdinpipeline, and let the existing prepare/extract/session/daemon path decide whether to speak.The adapter should feel like the Claude adapter:
codex;/aftertone-onand/aftertone-offsemantics stay per chat through the shared session allowlist;The official Codex hooks docs are the contract for this work: https://developers.openai.com/codex/hooks
User Stories
<spoken_summary>, so that the audio is brief and intentional.<spoken_summary>in tag-only mode, so that noisy fallback speech does not surprise me./aftertone-onand/aftertone-offbehavior to stay per chat, so that one session can speak while another stays silent.docs/adapters/codex.md, so that the supported hook contract and troubleshooting path are explicit.Implementation Decisions
Stopas the required post-reply trigger. Codex docs sayStopruns at turn scope, receives hook JSON on stdin, includeslast_assistant_message, and expects JSON or empty output on stdout.hook_run --stdinpath. It already ensures the daemon, posts/hook, falls back to prepare plus/say, traces pipeline state, and returns{}.last_assistant_message,transcript_path,session_id, andhook_event_namealready match the shape Aftertone needs.~/.codex/hooks.jsonfor global install. Codex also supports repo-local hooks, config TOML hooks, and plugin-bundled hooks, but user-level hooks best match Aftertone's global Cursor and Claude installs.Stoptreats plain text stdout as invalid, so wrappers must print JSON or nothing..cursor/hooks/speak_summary.tomluntil the repo deliberately renames adapter-neutral state later.lang, not the user's message language.Testing Decisions
Stopwithlast_assistant_messagecontaining<spoken_summary>produces a/saypayload.Stop.~/.codex/hooks.json: existing hooks remain, duplicate Aftertone entries are replaced, and Windows command overrides are preserved.Out of Scope
.cursor/hooks/.Further Notes
Codex hook docs confirm the important integration details: hooks are enabled by default,
~/.codex/hooks.jsonis a supported location, non-managed hooks require review/trust,Stopreceiveslast_assistant_message, andStopexpects JSON on stdout when exiting successfully.