Compare commits

..

3 Commits

Author SHA1 Message Date
Drew Ritter
00720702e4 docs: re-anchor Shape A examples away from Codex 2026-06-30 17:10:01 -07:00
Drew Ritter
398f67a8b7 chore(codex): remove orphaned session-start-codex hook + refresh hook docs
hooks/session-start-codex has had no caller since "Remove Codex hooks"
(#1845) deleted hooks-codex.json and its manifest registration; the
Codex manifest now declares an empty hooks object so Codex registers no
session-start hook at all. The script is Codex-specific dead code —
nothing executes it on Codex or any other harness.

- Delete hooks/session-start-codex.
- tests/hooks/test-session-start.sh: drop the two Codex cases that are
  redundant with the generic session-start tests (nested-format and the
  legacy-warning omission are already covered by the Claude Code cases).
  Re-point the "wrapper dispatches" case to the live `session-start`
  script so run-hook.cmd dispatch coverage — used by Claude Code and
  Cursor in production — is preserved rather than lost.
- docs/porting-to-a-new-harness.md: Codex is no longer a Shape A
  (shell-hook) harness, so re-anchor that worked example to Cursor (a
  live shell-hook harness that demonstrates the same per-harness field,
  schema, and matcher variance) and mark Codex as native skill discovery
  with no session-start hook. Clears the references to the deleted
  hooks-codex.json.
- docs/windows/polyglot-hooks.md: the "check hooks-codex.json" pointer
  referenced a file deleted in #1845; re-point to hooks-cursor.json.

RELEASE-NOTES.md keeps its historical mention of hooks-codex.json (it
accurately records what that release did). The tests/codex-plugin-sync
fixtures build their own synthetic session-start-codex and test the sync
mechanism generically, so they are intentionally left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:01:15 -07:00
Drew Ritter
b15ef6ebbe fix(codex): suppress SessionStart hook auto-discovery with empty hooks object
Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex
manifest has no `hooks` field: load_plugin_hooks falls back to a
hardcoded DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers
it. hooks/hooks.json is the Claude Code SessionStart hook, it is tracked
in this repo, and the Codex marketplace installs the whole repo root
(source url "./"), so the fallback re-registered the SessionStart hook
and its install-time trust prompt on Codex.

Removing the Codex hook file and the manifest `hooks` pointer (commit
"Remove Codex hooks") did not disable the hook on Codex — it removed the
explicit declaration that was overriding the fallback, so the fallback
took over and found the Claude hooks/hooks.json.

Declare an empty inline hooks object ({}) in .codex-plugin/plugin.json.
It parses as an empty inline hook set and stops Codex reaching the
auto-discovery fallback. An absent field, an empty array ([]), and an
empty inline list all collapse back to the fallback, so the value must
be exactly {}.

Update the test to assert the manifest declares hooks: {} (and that
hooks/hooks.json exists, which is what makes the declaration necessary),
replacing the prior assertion that the field was absent — which passed
while the hook was still being auto-discovered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:52:20 -07:00
4 changed files with 30 additions and 91 deletions

View File

@@ -90,7 +90,7 @@ every session, with no per-session opt-in by your human partner.** This is the
one non-negotiable capability. It can take any form: one non-negotiable capability. It can take any form:
- a **hook/event system** that runs a shell command at session start and reads - a **hook/event system** that runs a shell command at session start and reads
its stdout (Claude Code, Codex, Cursor, Copilot CLI), or its stdout (Claude Code, Cursor, Copilot CLI), or
- an **in-process plugin/extension** with a session-start or message lifecycle - an **in-process plugin/extension** with a session-start or message lifecycle
callback that can mutate the message array (OpenCode, pi), or callback that can mutate the message array (OpenCode, pi), or
- an **instructions-file** convention where the harness loads a context file that - an **instructions-file** convention where the harness loads a context file that
@@ -227,18 +227,20 @@ you may **not** do is bridge a gap by editing the user's global config.
The harness has a hook system that runs a shell command at session start and The harness has a hook system that runs a shell command at session start and
reads JSON from its stdout. The configured command runs `run-hook.cmd`, a reads JSON from its stdout. The configured command runs `run-hook.cmd`, a
polyglot wrapper that just locates bash and dispatches the named script; the polyglot wrapper that just locates bash and dispatches the named script; the
script (`hooks/session-start`, or a harness-specific variant like script (`hooks/session-start`, or a harness-specific variant) is what reads
`hooks/session-start-codex`) is what reads `using-superpowers/SKILL.md` and `using-superpowers/SKILL.md` and prints a JSON object whose **field name and
prints a JSON object whose **field name and nesting differ per harness**. nesting differ per harness**.
- Reference: `hooks/session-start` (and `hooks/session-start-codex`), - Reference: `hooks/session-start`, `hooks/run-hook.cmd`, and the per-harness
`hooks/run-hook.cmd`, and the per-harness hook config `hooks/hooks.json` hook config `hooks/hooks.json` (Claude Code) and `hooks/hooks-cursor.json`
(Claude Code), `hooks/hooks-codex.json` (Codex), `hooks/hooks-cursor.json`
(Cursor). (Cursor).
- Manifests: `.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json` point the - Manifests: `.cursor-plugin/plugin.json` is the Shape A manifest example that
harness at `./skills/` and the right `hooks-*.json`. (Claude Code's points the harness at `./skills/` and the right `hooks-*.json`. Claude Code's
`.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/` `.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/`
and `hooks/hooks.json` by convention.) and `hooks/hooks.json` by convention. Do **not** copy Codex's
`.codex-plugin/plugin.json` for Shape A: it declares an empty `hooks` object
specifically to suppress Codex's `hooks/hooks.json` auto-discovery, because
Codex surfaces skills natively and runs no session-start hook.
> **A hook *system* is not a session-start *event*.** A harness can have a > **A hook *system* is not a session-start *event*.** A harness can have a
> `hooks.json` mechanism — and even contain the literal string `SessionStart` in > `hooks.json` mechanism — and even contain the literal string `SessionStart` in
@@ -287,7 +289,7 @@ part of the installed extension** — never substitute "edit the user's global
| If the harness… | Use shape | Copy from | | If the harness… | Use shape | Copy from |
|---|---|---| |---|---|---|
| runs a shell command at session start and reads its stdout | A (shell-hook) | Codex (`hooks/session-start-codex` + `hooks/hooks-codex.json` + `.codex-plugin/`) | | runs a shell command at session start and reads its stdout | A (shell-hook) | Cursor (`hooks/session-start` + `hooks/hooks-cursor.json` + `.cursor-plugin/`) |
| is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool | | is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool |
| ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) | | ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) |
| has a plugin install command and a manifest `contextFileName` (or equivalent) the installer keeps | C via the plugin installer | Antigravity (`.antigravity-plugin/``agy plugin install` ships a generated context file; verify the installer preserves it — Part 6) | | has a plugin install command and a manifest `contextFileName` (or equivalent) the installer keeps | C via the plugin installer | Antigravity (`.antigravity-plugin/``agy plugin install` ships a generated context file; verify the installer preserves it — Part 6) |
@@ -309,7 +311,7 @@ patterns below are summaries; the code is the spec.
Create whatever the harness uses to recognize the plugin. Match the existing Create whatever the harness uses to recognize the plugin. Match the existing
ones in spirit: ones in spirit:
- **Shape A:** a `*-plugin/plugin.json` (see `.codex-plugin/plugin.json`) with - **Shape A:** a `*-plugin/plugin.json` (see `.cursor-plugin/plugin.json`) with
`name`, `version`, `description`, author/license/keywords, `"skills": `name`, `version`, `description`, author/license/keywords, `"skills":
"./skills/"`, and `"hooks": "./hooks/hooks-<harness>.json"`. Plus the "./skills/"`, and `"hooks": "./hooks/hooks-<harness>.json"`. Plus the
`hooks-<harness>.json` itself, registering a session-start hook whose command `hooks-<harness>.json` itself, registering a session-start hook whose command
@@ -375,25 +377,24 @@ both double-injects). Find the
exact field, nesting, and event-matcher values your harness expects. Then exact field, nesting, and event-matcher values your harness expects. Then
decide: add a fourth branch to `hooks/session-start`, or — if the harness needs decide: add a fourth branch to `hooks/session-start`, or — if the harness needs
a different bootstrap message or env contract — add a dedicated a different bootstrap message or env contract — add a dedicated
`hooks/session-start-<harness>` script, the way Codex did. If you add a branch `hooks/session-start-<harness>` script. If you add a branch
and your harness *also* sets an env var an earlier branch keys on (some harnesses and your harness *also* sets an env var an earlier branch keys on (some harnesses
set `CLAUDE_PLUGIN_ROOT` too), order your branch before the one that would set `CLAUDE_PLUGIN_ROOT` too), order your branch before the one that would
otherwise shadow it. Match the harness's otherwise shadow it. Match the harness's
own event-matcher strings (Claude Code uses `startup|clear|compact`, Codex own event-matcher strings (Claude Code uses `startup|clear|compact`, Cursor
`startup|resume|clear`, Cursor `sessionStart`); wrong matchers mean the hook `sessionStart`); wrong matchers mean the hook silently never fires.
silently never fires.
The **hook-config schema itself varies per harness** — don't assume the The **hook-config schema itself varies per harness** — don't assume the
Claude/Codex shape is universal. Compare `hooks/hooks.json`, Claude Code shape is universal. Compare `hooks/hooks.json` and
`hooks/hooks-codex.json`, and `hooks/hooks-cursor.json`: Cursor's uses `hooks/hooks-cursor.json`: Cursor's uses
`"version": 1`, a lowercase `sessionStart` key, a relative `"version": 1`, a lowercase `sessionStart` key, a relative
`./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields the `./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields
others use. Match your `hooks-<harness>.json` to whichever existing file is Claude Code uses. Match your `hooks-<harness>.json` to whichever existing file is
closest, not to a single canonical template. closest, not to a single canonical template.
The hook **command string references a harness-provided plugin-root variable**, The hook **command string references a harness-provided plugin-root variable**,
and its name differs per harness: `hooks.json` uses `${CLAUDE_PLUGIN_ROOT}`, and its name differs per harness: `hooks.json` uses `${CLAUDE_PLUGIN_ROOT}`,
`hooks-codex.json` uses `${PLUGIN_ROOT}`, Cursor uses a relative path. Use `hooks-cursor.json` uses a relative path. Use
whatever your harness exports. (The `session-start` script re-derives the root whatever your harness exports. (The `session-start` script re-derives the root
itself via `dirname`, so the script body doesn't depend on this — but the itself via `dirname`, so the script body doesn't depend on this — but the
command in the manifest does.) command in the manifest does.)
@@ -784,7 +785,7 @@ Use this as the live index; when in doubt, read the files, not this table.
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution | | Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace | | Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace |
| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | shell hook → `hooks/session-start-codex` | `references/codex-tools.md` | `tests/codex-plugin-sync/`, `tests/hooks/` | fork sync (`scripts/sync-to-codex-plugin.sh`) | | Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored | | Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — | | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
@@ -799,10 +800,10 @@ Use this as the live index; when in doubt, read the files, not this table.
- **Wrong JSON field → silent failure or double injection.** Shape A only. - **Wrong JSON field → silent failure or double injection.** Shape A only.
Confirm the exact field/nesting; Claude Code reads two fields without dedup. Confirm the exact field/nesting; Claude Code reads two fields without dedup.
- **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json` - **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json`
looks nothing like the Claude/Codex one (`version`, lowercase `sessionStart`, looks nothing like the Claude Code one (`version`, lowercase `sessionStart`,
relative command, no `matcher`/`type`/`async`). Match the closest existing file. relative command, no `matcher`/`type`/`async`). Match the closest existing file.
- **Plugin-root env var differs per harness.** Shape A. The hook command uses - **Plugin-root env var differs per harness.** Shape A. The hook command uses
`${CLAUDE_PLUGIN_ROOT}` (Claude), `${PLUGIN_ROOT}` (Codex), or a relative path `${CLAUDE_PLUGIN_ROOT}` (Claude) or a relative path
(Cursor). Use what your harness exports; the script re-derives the root itself. (Cursor). Use what your harness exports; the script re-derives the root itself.
- **System-message injection.** Shape B injects a *user* message on purpose - **System-message injection.** Shape B injects a *user* message on purpose
(#750, #894). Don't "fix" it to a system message. (#750, #894). Don't "fix" it to a system message.

View File

@@ -140,7 +140,7 @@ Check that the script filename is **extensionless** in `hooks.json`. A command l
### Hook doesn't fire at all ### Hook doesn't fire at all
Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Codex uses `startup|resume|clear`. Check `hooks-codex.json` for the Codex variant. Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Cursor uses `sessionStart`. Check `hooks-cursor.json` for the Cursor variant.
## Related Issues ## Related Issues

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# Codex SessionStart hook for superpowers plugin
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
exit 0

View File

@@ -4,7 +4,6 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start" HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start"
CODEX_HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd" WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd"
FAILURES=0 FAILURES=0
@@ -154,35 +153,15 @@ assert_command_output \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST" bash "$HOOK_UNDER_TEST"
codex_home="$(make_home codex-plugin-hooks)" wrapper_home="$(make_home run-hook-wrapper)"
codex_data="$TEST_ROOT/codex-plugin-hooks/data"
mkdir -p "$codex_data"
assert_command_output \ assert_command_output \
"Codex plugin hooks use dedicated script and emit nested SessionStart additionalContext" \ "run-hook.cmd wrapper dispatches to the named session-start script" \
"nested" \ "nested" \
"" \ "" \
"" \ "" \
"$codex_home" \ "$wrapper_home" \
PLUGIN_DATA="$codex_data" \
CLAUDE_PLUGIN_DATA="$codex_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$CODEX_HOOK_UNDER_TEST" bash "$WRAPPER_UNDER_TEST" session-start
codex_wrapper_home="$(make_home codex-wrapper)"
codex_wrapper_data="$TEST_ROOT/codex-wrapper/data"
mkdir -p "$codex_wrapper_data"
assert_command_output \
"Codex wrapper path dispatches to dedicated script" \
"nested" \
"" \
"" \
"$codex_wrapper_home" \
PLUGIN_DATA="$codex_wrapper_data" \
CLAUDE_PLUGIN_DATA="$codex_wrapper_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$WRAPPER_UNDER_TEST" session-start-codex
cursor_home="$(make_home cursor)" cursor_home="$(make_home cursor)"
assert_command_output \ assert_command_output \
@@ -217,21 +196,6 @@ assert_command_output \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST" bash "$HOOK_UNDER_TEST"
codex_legacy_home="$(make_home codex-legacy-warning-removed)"
codex_legacy_data="$TEST_ROOT/codex-legacy-warning-removed/data"
mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data"
assert_command_output \
"Codex SessionStart omits obsolete legacy custom-skill warning" \
"nested" \
"" \
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
"$codex_legacy_home" \
PLUGIN_DATA="$codex_legacy_data" \
CLAUDE_PLUGIN_DATA="$codex_legacy_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$CODEX_HOOK_UNDER_TEST"
if [[ "$FAILURES" -gt 0 ]]; then if [[ "$FAILURES" -gt 0 ]]; then
echo "STATUS: FAILED ($FAILURES failure(s))" echo "STATUS: FAILED ($FAILURES failure(s))"
exit 1 exit 1