Documentation
¶
Overview ¶
Package codex provides hook-based capture for OpenAI Codex sessions.
One provider covers both distributions of the product: the standalone `codex` CLI (Homebrew, npm) and the Codex desktop app, which embeds the same runtime over stdio JSON-RPC. Both honor user-global hook configuration at ~/.codex/hooks.json and the `[features] hooks = true` flag in ~/.codex/config.toml.
Index ¶
- type Provider
- func (p *Provider) AreHooksInstalled(ctx context.Context, repoRoot string) bool
- func (p *Provider) BuildHookEvents(ctx context.Context, event *hooks.Event, bs api.BlobPutter) ([]broker.RawEvent, error)
- func (p *Provider) DisplayName() string
- func (p *Provider) HookBinary(ctx context.Context, repoRoot string) (string, error)
- func (p *Provider) InstallHooks(ctx context.Context, repoRoot string, binaryPath string) (int, error)
- func (p *Provider) IsAvailable() bool
- func (p *Provider) Name() string
- func (p *Provider) ParseHookEvent(ctx context.Context, hookName string, stdin io.Reader) (*hooks.Event, error)
- func (p *Provider) ReadFromOffset(ctx context.Context, transcriptRef string, offset int, bs api.BlobPutter) ([]broker.RawEvent, int, error)
- func (p *Provider) ShouldCapture(ctx context.Context, payload []byte, activeRepos []broker.RegisteredRepo) (bool, error)
- func (p *Provider) TranscriptOffset(ctx context.Context, transcriptRef string) (int, error)
- func (p *Provider) UninstallHooks(ctx context.Context, repoRoot string) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Provider ¶
type Provider struct{}
Provider implements hooks.HookProvider for OpenAI Codex.
func New ¶ added in v0.5.1
func New() *Provider
New returns the Codex hook provider for explicit registration via providers.NewHookRegistry().
func (*Provider) AreHooksInstalled ¶
AreHooksInstalled reports whether ~/.codex/hooks.json contains at least one Semantica-owned entry. Detection runs on the file alone - trust state is best-effort and a missing trust entry only means the user has not yet acknowledged the hooks, not that they are absent.
func (*Provider) BuildHookEvents ¶
func (p *Provider) BuildHookEvents(ctx context.Context, event *hooks.Event, bs api.BlobPutter) ([]broker.RawEvent, error)
BuildHookEvents implements hooks.DirectHookEmitter for Codex.
The dispatcher routes only PromptSubmitted and ToolStepCompleted through this path. Codex lifecycle events (SessionStart -> SessionOpened, Stop -> AgentCompleted) reach the dispatcher through ParseHookEvent and are handled by the dispatcher's own lifecycle cases.
func (*Provider) DisplayName ¶
func (*Provider) HookBinary ¶
HookBinary returns the binary path Codex would execute for any one of our installed hooks. Health checks use this to verify `semantica` is still reachable via exec.LookPath on the user's machine.
func (*Provider) InstallHooks ¶
func (p *Provider) InstallHooks(ctx context.Context, repoRoot string, binaryPath string) (int, error)
InstallHooks writes the Semantica hook configuration into Codex's user-global config directory. The repoRoot argument is unused for the install itself (Codex hooks are not per-repo) but kept to satisfy the HookProvider contract; the cwd preflight gates per-repo behavior at capture time instead.
On success, the user has:
- ~/.codex/hooks.json with four entries (SessionStart, UserPromptSubmit, PostToolUse, Stop) pointing at the Semantica binary. Existing user hook entries under the same events are preserved; Semantica's entries are appended after them.
- ~/.codex/config.toml updated with [features] hooks = true and one [hooks.state.*] trusted_hash per installed hook, so Codex does not prompt for hook review on the next session. Trust keys reflect the actual (groupIndex, hookIndex) where Semantica's entries written, which can be non-zero when the file already contained unrelated hooks for the same event.
User configuration in config.toml (model pins, plugin blocks, marketplace declarations, project trust levels) is preserved across the round-trip. Comments and original key ordering are not retained because the TOML round-trip rewrites the file through a map; callers that need a comment-preserving editor should add one upstream and share it across providers.
Re-running is safe: identical commands at identical positions produce identical content and identical trust hashes, so the file ends up byte-equivalent.
func (*Provider) IsAvailable ¶
IsAvailable reports whether Codex is present on this machine. The standalone CLI installs `codex` on PATH; the desktop app drops a bundle at /Applications/Codex.app on macOS plus a per-user state directory at ~/.codex. Either signal is enough to surface Codex as a known provider.
func (*Provider) ParseHookEvent ¶
func (p *Provider) ParseHookEvent(ctx context.Context, hookName string, stdin io.Reader) (*hooks.Event, error)
ParseHookEvent translates Codex hook stdin into a normalized Event. Returns nil for hook names that have no capture analogue today so the dispatcher skips them quietly.
Hook event mapping:
- session_start -> SessionOpened (lifecycle no-op in the dispatcher)
- user_prompt_submit -> PromptSubmitted
- post_tool_use -> ToolStepCompleted
- stop -> AgentCompleted
Codex does not currently emit a session-end signal. Capture state is removed at every AgentCompleted (turn end), matching how Claude Code's Stop is handled.
func (*Provider) ReadFromOffset ¶
func (p *Provider) ReadFromOffset(ctx context.Context, transcriptRef string, offset int, bs api.BlobPutter) ([]broker.RawEvent, int, error)
ReadFromOffset is a no-op. Codex rollout files (~/.codex/sessions/...) are not a stable contract and have observed non-strict JSONL where record strings embed raw newlines. The provider relies exclusively on hook payloads. If a fallback read path becomes useful, it should use a streaming JSON decoder rather than line-based splitting.
func (*Provider) ShouldCapture ¶
func (p *Provider) ShouldCapture(ctx context.Context, payload []byte, activeRepos []broker.RegisteredRepo) (bool, error)
ShouldCapture decides whether a Codex hook invocation should run any downstream side effects, based on the session's working directory.
User-global hooks at ~/.codex/hooks.json fire for every Codex session on the machine, including sessions in repositories the user has not registered with Semantica. The capture entrypoint's default broker-wide gate ("any active repo?") is too permissive for that surface: a Codex session in /tmp would otherwise leak prompts, file edits, and shell commands into capture state belonging to an unrelated registered repo.
We require the session's cwd to resolve to a git repository root that matches one of the active broker entries by canonical path. If either resolution fails - no enclosing repo, no matching canonical entry - ShouldCapture returns false and the caller exits before parsing stdin, opening the blob store, writing to the broker, or appending to the hook-error log.
func (*Provider) TranscriptOffset ¶
TranscriptOffset returns 0 unconditionally. Codex's rollout files are not used for capture today; hooks supply every event we need.
func (*Provider) UninstallHooks ¶
UninstallHooks removes Semantica's entries from the user-global configuration. Other tools' hooks - including any non-Semantica entries the user may have added by hand - are preserved.
The function reads the current hooks.json to recover the exact command strings and positions of Semantica's entries before pruning the file, then removes only the trust entries whose hash matches what those commands produced. Trust entries whose hash differs (e.g. modified by the user or written by a different tool) are left untouched.
Codex hooks are user-global, so a disable on any one repo removes the hooks for all repos. Re-running enable on another registered repo restores them. This is a deliberate trade for keeping the install surface symmetric across CLI and desktop sessions; users with multiple active repos who only want to disable Codex on one should rely on the cwd preflight (a deregistered repo will not produce capture events) rather than running `disable --providers codex`.