Compare commits

..

1 Commits

Author SHA1 Message Date
Drew Ritter
ded04a41e3 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:38:20 -07:00
2 changed files with 19 additions and 25 deletions

View File

@@ -21,12 +21,13 @@
"workflow" "workflow"
], ],
"skills": "./skills/", "skills": "./skills/",
"hooks": {},
"interface": { "interface": {
"displayName": "Superpowers", "displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents", "shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.", "longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
"developerName": "Jesse Vincent", "developerName": "Jesse Vincent",
"category": "Developer Tools", "category": "Coding",
"capabilities": [ "capabilities": [
"Interactive", "Interactive",
"Read", "Read",

View File

@@ -51,32 +51,25 @@ if not plugin_manifest.exists():
manifest = json.loads(plugin_manifest.read_text(encoding="utf-8")) manifest = json.loads(plugin_manifest.read_text(encoding="utf-8"))
assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name") assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name")
supported_categories = {
"Productivity", # Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex manifest
"Creativity", # has no `hooks` field: load_plugin_hooks falls back to a hardcoded
"Developer Tools", # DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers it. That file is
"Business & Operations", # the Claude Code SessionStart hook, it is tracked in this repo, and this
"Data & Analytics", # marketplace installs the whole repo root (source url "./"), so on Codex the
"Communication", # fallback re-registers the SessionStart hook and its install-time trust prompt.
"Education & Research", # Declaring an empty inline hooks object ({}) parses as an empty inline hook set
"Security", # and suppresses the auto-discovery. An absent field, an empty array ([]), and
"Finance", # an empty inline list all collapse back to the fallback, so the value must be
"Healthcare", # exactly an empty object.
"Travel", hooks_config = repo_root / "hooks" / "hooks.json"
"Entertainment", if not hooks_config.exists():
"Other", raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)")
}
manifest_category = manifest.get("interface", {}).get("category")
if manifest_category not in supported_categories:
raise AssertionError(
f"plugin manifest category: expected one of {sorted(supported_categories)!r}, "
f"got {manifest_category!r}"
)
assert_equal(manifest_category, plugin.get("category"), "plugin manifest category")
assert_equal( assert_equal(
manifest.get("hooks"), manifest.get("hooks"),
None, {},
"Codex manifest ships no hooks", "Codex manifest must declare empty hooks {} to suppress hooks/hooks.json auto-discovery",
) )
print("Codex marketplace manifest looks good") print("Codex marketplace manifest looks good")