Repository navigation
Wire and verify automatic capture hooks on Codex CLI (currently skills-only) #47
Description
Context
PR #46 shipped Codex CLI as a delivery format for throughline's 4 skills (handoff,
onboard, consolidate, consolidate-memory) via .codex-plugin/plugin.json +
.agents/plugins/marketplace.json. That half is verified: codex plugin marketplace add / plugin add throughline@throughline install correctly and skills load.
Automatic capture (the 5 hooks) was deliberately left out, for two reasons found
during /review-pr on that PR:
-
Unverified firing. I ran each hook script directly (piped synthetic
Claude-Code-shaped JSON atsession-prompt.shetc. withCLAUDE_PLUGIN_ROOT
unset) and confirmed the scripts' own logic works correctly outside
CLAUDE_PLUGIN_ROOT. But that only proves the shell logic is portable - it does
not prove Codex actually invokes these hooks at all, with what cwd, or with
what env vars set. The reviewer independently tried to get a hook to fire under
realcodex exec(withhooksdeclared various ways,--enable hooks --enable plugin_hooks, absolute command paths, a user-level$CODEX_HOME/hooks.json)
and could not get any hook to fire either. Hook execution under Codex is
unverified in both directions as of this issue. -
A plausible double-registration risk. Codex's own hook-discovery code carries
a diagnostic:loading hooks from both <path> and <path>/; prefer a single representation for this layer. throughline's plugin root has both a file
(whatever hooks.json would live there) and a directory (hooks/, which contains
its ownhooks.jsonfor Claude Code's convention). If Codex's discovery treats
these as two representations of the same layer, every hook could register twice- and
session-capture.shhas no idempotency guard on capture lines (only
session-precompact.shandsession-flush.shguard their stamps), so a
double-registration would silently double-log every captured action.
Separately worth checking: whether Codex actually sets
CLAUDE_PLUGIN_ROOT(or a
PLUGIN_ROOT/CLAUDE_PLUGIN_ROOTpair) for hook-invoked processes - if so, the
existinghooks/hooks.json(unmodified, same one Claude Code uses) might just
work if pointed at directly, with no second config file and no drift risk at all.
Also worth checking: whether Codex's manifesthooksfield is real -
validate_plugin.py(Codex 0.149.0) empirically rejects ahookskey in
plugin.jsoneven though the plugin-json-spec.md reference doc both shows it in
an example ("hooks": "./hooks.json") and separately states the opposite
("Validation rejects unsupported manifest fields such ashooks"). That
reference doc is internally contradictory on this point and needs resolving
empirically, not by reading it again. - and
What to do
Needs a real Codex session where a hook can actually be observed firing (or not) -
not another static read of the manifest spec. Once that's possible:
- Confirm whether Codex sets
CLAUDE_PLUGIN_ROOTfor hook processes at all - Confirm whether a bare root-level
hooks.jsonis discovered by convention, or
whether the manifest needs some declaration - and if so, what actually gets
accepted (validate_plugin.pysayshooksthe field is rejected; empirically
test whether hooks fire anyway via convention-based discovery) - If (and only if) a single unambiguous discovery path is confirmed, wire it -
reusing the existinghooks/hooks.jsondirectly ifCLAUDE_PLUGIN_ROOTis
set by Codex (zero drift risk), or add idempotency tosession-capture.sh
first if a duplicate-file approach is unavoidable - Add a
.local-ci.jsoncheck keeping any new hook config in sync with
hooks/hooks.jsonif a second file ends up being necessary
Until this is resolved, Codex users get the 4 skills on-demand but not automatic
session capture - document that clearly in the README's Codex section rather than
implying parity with the Claude Code / OpenCode delivery formats.
Activity
fix: drop unverified Codex hook wiring, keep skills-only delivery
feat: Codex CLI plugin as a 4th delivery format (skills only)
jsirish commentedon Aug 25, 2026
Empirical findings (2026-08-25, codex-cli 0.149.0)
Ran a live, throwaway probe hook (never throughline's real scripts) through real
codex execsessions — both the primary ChatGPT-authenticated account and a separate Qwen/Bailian-backed profile once the primary hit its monthly usage limit mid-investigation. Full methodology: a probe plugin installed viacodex plugin marketplace add <local-path>→codex plugin add, with ahooks.jsonthat appends a timestamped, env-dumping line to a file outside the sandbox on every fire — so firing counts and env vars are directly observable, not inferred from trace logs (which turned out to be misleading — see below).What actually works
Discovery requires an explicit manifest declaration. A bare root-level
hooks.json(convention only, noplugin.jsonkey) is never discovered — confirmed with--enable plugin_hooks --dangerously-bypass-hook-trustand no probe firing. The only working path:"hooks": "./relative/path.json"in.codex-plugin/plugin.json.validate_plugin.py's rejection of thehookskey does not apply tocodex plugin add. Installed cleanly with the key present (v0.0.2 and v0.0.3 of the probe plugin). Whatever validator rejects it (used bygh skill publish --dry-run?) is a different code path from the live installer. This reverses the previous finding.CLAUDE_PLUGIN_ROOTandPLUGIN_ROOTare both set for hook processes, pointing at the installed plugin cache (~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/).CLAUDE_PLUGIN_DATA/PLUGIN_DATAare also set, pointing at a plugin-scoped data dir Codex provides (unused today). This confirms the best-case outcome flagged in the original issue:hooks/_lib.sh's existing fallback (${CLAUDE_PLUGIN_ROOT:-$DIR/..}) works with zero modification if.codex-plugin/plugin.jsondeclares"hooks": "./hooks/hooks.json"— reusing the exact file Claude Code uses, no duplicate config, no drift risk.No double-registration in this exact layout. With
.codex-plugin/plugin.jsonpointing"hooks"at./hooks/hooks.json(mirroring this repo's real structure — ahooks/directory containinghooks.json, no root-level file), each event fired exactly once. The "loading hooks from both<path>and<path>/" collision diagnostic never appeared. That risk is specific to ambiguous convention-based auto-discovery (which doesn't even work per throughline v0.2.0: robustness, secret redaction, compaction-survival #1), not to an explicit single declaration — sosession-capture.sh's missing idempotency guard is not a live risk for this recommended pattern.Event coverage, confirmed by direct firing (not just wire-schema strings):
SessionStart✓,UserPromptSubmit✓,PostToolUse✓ (fired once on an actual Bash-tool-call turn),SessionEnd✓ (fired once — this reverses my own binary-string search, which only foundSessionStart/PostToolUse/UserPromptSubmitembedded;SessionEndfires despite that).PreCompactremains unconfirmed — no compaction was reachable in a shortexecsession and no wire-schema string was found for it; treat as unsupported until someone can actually trigger compaction under Codex.Correction to the JSON payload claim above: the "hook: X" / "hook: X Completed" trace lines in
--print-logs/RUST_LOG=debugoutput are boilerplate lifecycle logging that fires regardless of whether any hook config exists — confirmed by uninstalling the probe plugin entirely and seeing the same trace lines. Only the probe's own append-to-file side effect is real evidence of firing; don't trust the trace lines alone in any future investigation here.The actual blocker: a two-part opt-in gate, not a discovery problem
plugin_hooksis a feature flag, off by default. Without--enable plugin_hooks(or a persistedfeatures.plugin_hooks = true), hooks never fire — not even the boilerplate trace lines.codex execsession there is no way to grant it — it silently no-ops with zero diagnostic output. The only way to make it fire inexecmode is--dangerously-bypass-hook-trust, which the CLI's own help text scopes to "automation that already vets hook sources" — not something to bake into a plugin's shipped default or ask ordinary users to pass routinely.stringson the binary: trust is granted through an interactive TUI review flow only (hooks/listis a real app-server JSON-RPC method; trust is persisted as atrusted_hashin ahooks.statefile, written viaconfig/batchWrite, and invalidated onaccount change). There's nocodex plugin trustCLI subcommand and no config.toml key to pre-grant it. I did not verify this interactive flow actually works end-to-end — driving the real TUI requires a PTY this investigation didn't have, and the underlyingapp-serverJSON-RPC protocol is explicitly[experimental], undocumented, not something to build a shipped feature's trust story on even if reverse-engineered.Decision
Not wiring Codex hooks. Discovery is solved and double-registration is ruled out for the recommended pattern — the plan's original decision gate is technically satisfied — but the newly-found trust gate means "automatic capture" would not actually be automatic for any realistic usage: headless/CI use needs the dangerous bypass flag, and interactive use needs an unverified manual trust step per install. That undermines the entire point of throughline's hooks (zero-action continuous capture). Shipping the
hookskey now would create a feature that appears wired but silently does nothing for the vast majority of real sessions — worse than the current honest "skills only" framing.If revisited later
.codex-plugin/plugin.json+"hooks": "./hooks/hooks.json", zero changes tohooks/hooks.jsonorhooks/_lib.shneeded.PreCompactneeds a real long-context test to confirm one way or the other.plugin_hooksgraduates out of feature-flag status, if it does — 0.149.0 was current as of this investigation, 0.149.1 was already available but untested.Closing with these findings; skills-only remains correct and is now backed by verified evidence rather than an inconclusive prior attempt.
jsirish commentedon Aug 25, 2026
Closing per the investigation above — skills-only remains correct, now with empirical evidence. Reopen if Codex ever exposes a non-interactive hook-trust grant.
feat(#47): validate Codex/agents manifests in CI, close hooks investi…
docs(#45): redesign promo site, restructure README for all 4 harnesses